diff --git a/.env b/.env index 71a63171e7..c761dd0fd0 100644 --- a/.env +++ b/.env @@ -145,7 +145,7 @@ SENTRY_RELEASE=local NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT} #### Prowler release version #### -NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.16.0 +NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.24.0 # Social login credentials SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google" diff --git a/.github/actions/setup-python-poetry/action.yml b/.github/actions/setup-python-poetry/action.yml index 505ba86da6..fd96796b9b 100644 --- a/.github/actions/setup-python-poetry/action.yml +++ b/.github/actions/setup-python-poetry/action.yml @@ -13,11 +13,15 @@ inputs: poetry-version: description: 'Poetry version to install' required: false - default: '2.1.1' + default: '2.3.4' install-dependencies: description: 'Install Python dependencies with Poetry' required: false default: 'true' + update-lock: + description: 'Run `poetry lock` during setup. Only enable when a prior step mutates pyproject.toml (e.g. API `@master` VCS rewrite). Default: false.' + required: false + default: 'false' runs: using: 'composite' @@ -74,7 +78,7 @@ runs: grep -A2 -B2 "resolved_reference" poetry.lock - name: Update poetry.lock (prowler repo only) - if: github.repository == 'prowler-cloud/prowler' + if: github.repository == 'prowler-cloud/prowler' && inputs.update-lock == 'true' shell: bash working-directory: ${{ inputs.working-directory }} run: poetry lock diff --git a/.github/workflows/api-bump-version.yml b/.github/workflows/api-bump-version.yml index 7ddbf195f5..6abdc8c84c 100644 --- a/.github/workflows/api-bump-version.yml +++ b/.github/workflows/api-bump-version.yml @@ -13,6 +13,8 @@ env: PROWLER_VERSION: ${{ github.event.release.tag_name }} BASE_BRANCH: master +permissions: {} + jobs: detect-release-type: runs-on: ubuntu-latest diff --git a/.github/workflows/api-code-quality.yml b/.github/workflows/api-code-quality.yml index 68928833a2..1724e51d70 100644 --- a/.github/workflows/api-code-quality.yml +++ b/.github/workflows/api-code-quality.yml @@ -17,6 +17,8 @@ concurrency: env: API_WORKING_DIR: ./api +permissions: {} + jobs: api-code-quality: runs-on: ubuntu-latest @@ -50,7 +52,7 @@ jobs: - name: Check for API changes id: check-changes - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | api/** @@ -67,6 +69,7 @@ jobs: with: python-version: ${{ matrix.python-version }} working-directory: ./api + update-lock: 'true' - name: Poetry check if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.github/workflows/api-codeql.yml b/.github/workflows/api-codeql.yml index 3832900a89..915a5b2722 100644 --- a/.github/workflows/api-codeql.yml +++ b/.github/workflows/api-codeql.yml @@ -24,6 +24,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: api-analyze: name: CodeQL Security Analysis diff --git a/.github/workflows/api-container-build-push.yml b/.github/workflows/api-container-build-push.yml index 0cd3b82071..e6403ed689 100644 --- a/.github/workflows/api-container-build-push.yml +++ b/.github/workflows/api-container-build-push.yml @@ -33,6 +33,8 @@ env: PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-api +permissions: {} + jobs: setup: if: github.repository == 'prowler-cloud/prowler' @@ -137,18 +139,18 @@ jobs: sed -i "s|prowler-cloud/prowler.git@master|prowler-cloud/prowler.git@${LATEST_SHA}|" api/pyproject.toml - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build and push API container for ${{ matrix.arch }} id: container-push if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: ${{ env.WORKING_DIRECTORY }} push: true @@ -178,7 +180,7 @@ jobs: auth.docker.io:443 production.cloudflare.docker.com:443 - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/api-container-checks.yml b/.github/workflows/api-container-checks.yml index 14c163e89a..5b59939db9 100644 --- a/.github/workflows/api-container-checks.yml +++ b/.github/workflows/api-container-checks.yml @@ -18,6 +18,8 @@ env: API_WORKING_DIR: ./api IMAGE_NAME: prowler-api +permissions: {} + jobs: api-dockerfile-lint: if: github.repository == 'prowler-cloud/prowler' @@ -42,7 +44,7 @@ jobs: - name: Check if Dockerfile changed id: dockerfile-changed - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: api/Dockerfile @@ -104,7 +106,7 @@ jobs: - name: Check for API changes id: check-changes - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: api/** files_ignore: | @@ -115,11 +117,11 @@ jobs: - name: Set up Docker Buildx if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build container for ${{ matrix.arch }} if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: ${{ env.API_WORKING_DIR }} push: false diff --git a/.github/workflows/api-security.yml b/.github/workflows/api-security.yml index 9cd6b14623..9323df97c3 100644 --- a/.github/workflows/api-security.yml +++ b/.github/workflows/api-security.yml @@ -17,6 +17,8 @@ concurrency: env: API_WORKING_DIR: ./api +permissions: {} + jobs: api-security-scans: runs-on: ubuntu-latest @@ -53,7 +55,7 @@ jobs: - name: Check for API changes id: check-changes - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | api/** @@ -70,6 +72,7 @@ jobs: with: python-version: ${{ matrix.python-version }} working-directory: ./api + update-lock: 'true' - name: Bandit if: steps.check-changes.outputs.any_changed == 'true' @@ -77,9 +80,10 @@ jobs: - name: Safety if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run safety check --ignore 79023,79027,86217 + run: poetry run safety check --ignore 79023,79027,86217,71600 # TODO: 79023 & 79027 knack ReDoS until `azure-cli-core` (via `cartography`) allows `knack` >=0.13.0 # TODO: 86217 because `alibabacloud-tea-openapi == 0.4.3` don't let us upgrade `cryptography >= 46.0.0` + # TODO: 71600 CVE-2024-1135 false positive - fixed in gunicorn 22.0.0, project uses 23.0.0 - 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 ce00b03108..3d4e7e1799 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -30,6 +30,8 @@ env: VALKEY_DB: 0 API_WORKING_DIR: ./api +permissions: {} + jobs: api-tests: runs-on: ubuntu-latest @@ -99,7 +101,7 @@ jobs: - name: Check for API changes id: check-changes - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | api/** @@ -116,6 +118,7 @@ jobs: with: python-version: ${{ matrix.python-version }} working-directory: ./api + update-lock: 'true' - name: Run tests with pytest if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index feb8c7c9bf..5895188634 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -17,6 +17,8 @@ env: BACKPORT_LABEL_PREFIX: backport-to- BACKPORT_LABEL_IGNORE: was-backported +permissions: {} + jobs: backport: if: github.event.pull_request.merged == true && !(contains(github.event.pull_request.labels.*.name, 'backport')) && !(contains(github.event.pull_request.labels.*.name, 'was-backported')) @@ -46,7 +48,7 @@ jobs: - name: Backport PR if: steps.label_check.outputs.label_check == 'success' - uses: sorenlouv/backport-github-action@516854e7c9f962b9939085c9a92ea28411d1ae90 # v10.2.0 + uses: sorenlouv/backport-github-action@9460b7102fea25466026ce806c9ebf873ac48721 # v11.0.0 with: github_token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} auto_backport_label_prefix: ${{ env.BACKPORT_LABEL_PREFIX }} diff --git a/.github/workflows/ci-zizmor.yml b/.github/workflows/ci-zizmor.yml index 5962b01efd..a66af9684c 100644 --- a/.github/workflows/ci-zizmor.yml +++ b/.github/workflows/ci-zizmor.yml @@ -21,6 +21,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: zizmor: if: github.repository == 'prowler-cloud/prowler' @@ -49,6 +51,6 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0 + uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 with: token: ${{ github.token }} diff --git a/.github/workflows/comment-label-update.yml b/.github/workflows/comment-label-update.yml index 3f5f3bb01f..9649d18cfc 100644 --- a/.github/workflows/comment-label-update.yml +++ b/.github/workflows/comment-label-update.yml @@ -9,6 +9,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: false +permissions: {} + jobs: update-labels: if: contains(github.event.issue.labels.*.name, 'status/awaiting-response') diff --git a/.github/workflows/conventional-commit.yml b/.github/workflows/conventional-commit.yml index a1195ef389..5fc31003a4 100644 --- a/.github/workflows/conventional-commit.yml +++ b/.github/workflows/conventional-commit.yml @@ -16,6 +16,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true +permissions: {} + jobs: conventional-commit-check: runs-on: ubuntu-latest diff --git a/.github/workflows/create-backport-label.yml b/.github/workflows/create-backport-label.yml index 334406f76f..d7aa5709ae 100644 --- a/.github/workflows/create-backport-label.yml +++ b/.github/workflows/create-backport-label.yml @@ -13,6 +13,8 @@ env: BACKPORT_LABEL_PREFIX: backport-to- BACKPORT_LABEL_COLOR: B60205 +permissions: {} + jobs: create-label: runs-on: ubuntu-latest diff --git a/.github/workflows/docs-bump-version.yml b/.github/workflows/docs-bump-version.yml index e8c33ae53e..c1e3f6afb4 100644 --- a/.github/workflows/docs-bump-version.yml +++ b/.github/workflows/docs-bump-version.yml @@ -13,6 +13,8 @@ env: PROWLER_VERSION: ${{ github.event.release.tag_name }} BASE_BRANCH: master +permissions: {} + jobs: detect-release-type: runs-on: ubuntu-latest diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index af0a9c7e78..0aa955413f 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -14,6 +14,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: scan-secrets: runs-on: ubuntu-latest diff --git a/.github/workflows/helm-chart-checks.yml b/.github/workflows/helm-chart-checks.yml index a0a24c2516..66f5954f69 100644 --- a/.github/workflows/helm-chart-checks.yml +++ b/.github/workflows/helm-chart-checks.yml @@ -21,6 +21,8 @@ concurrency: env: CHART_PATH: contrib/k8s/helm/prowler-app +permissions: {} + jobs: helm-lint: if: github.repository == 'prowler-cloud/prowler' @@ -36,12 +38,12 @@ jobs: egress-policy: audit - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Helm - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 + uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 - name: Update chart dependencies run: helm dependency update ${{ env.CHART_PATH }} diff --git a/.github/workflows/helm-chart-release.yml b/.github/workflows/helm-chart-release.yml index c0c5773bdb..43dcf4d355 100644 --- a/.github/workflows/helm-chart-release.yml +++ b/.github/workflows/helm-chart-release.yml @@ -13,6 +13,8 @@ concurrency: env: CHART_PATH: contrib/k8s/helm/prowler-app +permissions: {} + jobs: release-helm-chart: if: github.repository == 'prowler-cloud/prowler' @@ -29,12 +31,12 @@ jobs: egress-policy: audit - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Helm - uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0 + uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 - name: Set appVersion from release tag run: | diff --git a/.github/workflows/issue-lock-on-close.yml b/.github/workflows/issue-lock-on-close.yml index 27c7da0bb9..1848585745 100644 --- a/.github/workflows/issue-lock-on-close.yml +++ b/.github/workflows/issue-lock-on-close.yml @@ -9,6 +9,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: false +permissions: {} + jobs: lock: if: | diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index fcc6f1e363..b694e8ecef 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -772,7 +772,7 @@ jobs: SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Safe Outputs if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: safe-output path: ${{ env.GH_AW_SAFE_OUTPUTS }} @@ -793,13 +793,13 @@ jobs: await main(); - name: Upload sanitized agent output if: always() && env.GH_AW_AGENT_OUTPUT - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: agent-output path: ${{ env.GH_AW_AGENT_OUTPUT }} if-no-files-found: warn - name: Upload engine output files - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: agent_outputs path: | @@ -839,7 +839,7 @@ jobs: - name: Upload agent artifacts if: always() continue-on-error: true - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: agent-artifacts path: | @@ -880,7 +880,7 @@ jobs: destination: /opt/gh-aw/actions - name: Download agent output artifact continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: agent-output path: /tmp/gh-aw/safeoutputs/ @@ -992,13 +992,13 @@ jobs: destination: /opt/gh-aw/actions - name: Download agent artifacts continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: agent-artifacts path: /tmp/gh-aw/threat-detection/ - name: Download agent output artifact continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: agent-output path: /tmp/gh-aw/threat-detection/ @@ -1071,7 +1071,7 @@ jobs: await main(); - name: Upload threat detection log if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: threat-detection.log path: /tmp/gh-aw/threat-detection/detection.log @@ -1174,7 +1174,7 @@ jobs: destination: /opt/gh-aw/actions - name: Download agent output artifact continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: agent-output path: /tmp/gh-aw/safeoutputs/ diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index a42d6e1f73..621a763da5 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -15,6 +15,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true +permissions: {} + jobs: labeler: runs-on: ubuntu-latest diff --git a/.github/workflows/mcp-container-build-push.yml b/.github/workflows/mcp-container-build-push.yml index 3f59a455db..90157db9ab 100644 --- a/.github/workflows/mcp-container-build-push.yml +++ b/.github/workflows/mcp-container-build-push.yml @@ -32,6 +32,8 @@ env: PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-mcp +permissions: {} + jobs: setup: if: github.repository == 'prowler-cloud/prowler' @@ -123,18 +125,18 @@ jobs: persist-credentials: false - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build and push MCP container for ${{ matrix.arch }} id: container-push if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: ${{ env.WORKING_DIRECTORY }} push: true @@ -173,7 +175,7 @@ jobs: release-assets.githubusercontent.com:443 - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/mcp-container-checks.yml b/.github/workflows/mcp-container-checks.yml index f8fbfca12f..b205232b0c 100644 --- a/.github/workflows/mcp-container-checks.yml +++ b/.github/workflows/mcp-container-checks.yml @@ -18,6 +18,8 @@ env: MCP_WORKING_DIR: ./mcp_server IMAGE_NAME: prowler-mcp +permissions: {} + jobs: mcp-dockerfile-lint: if: github.repository == 'prowler-cloud/prowler' @@ -42,7 +44,7 @@ jobs: - name: Check if Dockerfile changed id: dockerfile-changed - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: mcp_server/Dockerfile @@ -87,6 +89,9 @@ jobs: api.github.com:443 mirror.gcr.io:443 check.trivy.dev:443 + get.trivy.dev:443 + release-assets.githubusercontent.com:443 + objects.githubusercontent.com:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -96,7 +101,7 @@ jobs: - name: Check for MCP changes id: check-changes - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: mcp_server/** files_ignore: | @@ -105,11 +110,11 @@ jobs: - name: Set up Docker Buildx if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build MCP container for ${{ matrix.arch }} if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: ${{ env.MCP_WORKING_DIR }} push: false diff --git a/.github/workflows/mcp-pypi-release.yml b/.github/workflows/mcp-pypi-release.yml index d6272034ec..cccda1f664 100644 --- a/.github/workflows/mcp-pypi-release.yml +++ b/.github/workflows/mcp-pypi-release.yml @@ -14,6 +14,8 @@ env: PYTHON_VERSION: "3.12" WORKING_DIRECTORY: ./mcp_server +permissions: {} + jobs: validate-release: if: github.repository == 'prowler-cloud/prowler' diff --git a/.github/workflows/pr-check-changelog.yml b/.github/workflows/pr-check-changelog.yml index c729875843..fa01ba4cbe 100644 --- a/.github/workflows/pr-check-changelog.yml +++ b/.github/workflows/pr-check-changelog.yml @@ -16,6 +16,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true +permissions: {} + jobs: check-changelog: if: contains(github.event.pull_request.labels.*.name, 'no-changelog') == false @@ -45,7 +47,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | api/** diff --git a/.github/workflows/pr-check-compliance-mapping.yml b/.github/workflows/pr-check-compliance-mapping.yml index dcf61602ba..1ed78995bb 100644 --- a/.github/workflows/pr-check-compliance-mapping.yml +++ b/.github/workflows/pr-check-compliance-mapping.yml @@ -16,6 +16,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true +permissions: {} + jobs: check-compliance-mapping: if: contains(github.event.pull_request.labels.*.name, 'no-compliance-check') == false @@ -43,7 +45,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | prowler/providers/**/services/**/*.metadata.json diff --git a/.github/workflows/pr-conflict-checker.yml b/.github/workflows/pr-conflict-checker.yml index 330a038fa0..667f807be8 100644 --- a/.github/workflows/pr-conflict-checker.yml +++ b/.github/workflows/pr-conflict-checker.yml @@ -15,6 +15,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true +permissions: {} + jobs: check-conflicts: runs-on: ubuntu-latest @@ -39,7 +41,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: '**' diff --git a/.github/workflows/pr-merged.yml b/.github/workflows/pr-merged.yml index 62eca02e58..4a98ac70c4 100644 --- a/.github/workflows/pr-merged.yml +++ b/.github/workflows/pr-merged.yml @@ -12,6 +12,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: false +permissions: {} + jobs: trigger-cloud-pull-request: if: | diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index f0aee83174..4ff8fc049c 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -17,6 +17,8 @@ concurrency: env: PROWLER_VERSION: ${{ inputs.prowler_version }} +permissions: {} + jobs: prepare-release: if: github.event_name == 'workflow_dispatch' && github.repository == 'prowler-cloud/prowler' @@ -38,15 +40,11 @@ jobs: token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} persist-credentials: false - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - name: Setup Python with Poetry + uses: ./.github/actions/setup-python-poetry with: python-version: '3.12' - - - name: Install Poetry - run: | - python3 -m pip install --user poetry==2.1.1 - echo "$HOME/.local/bin" >> $GITHUB_PATH + install-dependencies: 'false' - name: Configure Git run: | @@ -380,7 +378,7 @@ jobs: no-changelog - name: Create draft release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 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 99010c6af3..f899a84d1d 100644 --- a/.github/workflows/sdk-bump-version.yml +++ b/.github/workflows/sdk-bump-version.yml @@ -13,6 +13,8 @@ env: PROWLER_VERSION: ${{ github.event.release.tag_name }} BASE_BRANCH: master +permissions: {} + jobs: detect-release-type: runs-on: ubuntu-latest diff --git a/.github/workflows/sdk-check-duplicate-test-names.yml b/.github/workflows/sdk-check-duplicate-test-names.yml index ac8a01d117..17c595ca11 100644 --- a/.github/workflows/sdk-check-duplicate-test-names.yml +++ b/.github/workflows/sdk-check-duplicate-test-names.yml @@ -10,6 +10,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: check-duplicate-test-names: if: github.repository == 'prowler-cloud/prowler' diff --git a/.github/workflows/sdk-code-quality.yml b/.github/workflows/sdk-code-quality.yml index b854e9ddeb..b777ee1657 100644 --- a/.github/workflows/sdk-code-quality.yml +++ b/.github/workflows/sdk-code-quality.yml @@ -14,6 +14,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: sdk-code-quality: if: github.repository == 'prowler-cloud/prowler' @@ -46,7 +48,7 @@ jobs: - name: Check for SDK changes id: check-changes - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: ./** files_ignore: | @@ -69,22 +71,11 @@ jobs: contrib/** **/AGENTS.md - - name: Install Poetry + - name: Setup Python with Poetry if: steps.check-changes.outputs.any_changed == 'true' - run: pipx install poetry==2.1.1 - - - name: Set up Python ${{ matrix.python-version }} - if: steps.check-changes.outputs.any_changed == 'true' - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: ./.github/actions/setup-python-poetry with: python-version: ${{ matrix.python-version }} - cache: 'poetry' - - - name: Install dependencies - if: steps.check-changes.outputs.any_changed == 'true' - run: | - poetry install --no-root - poetry run pip list - name: Check Poetry lock file if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.github/workflows/sdk-codeql.yml b/.github/workflows/sdk-codeql.yml index fa5056c2d6..0985ec1228 100644 --- a/.github/workflows/sdk-codeql.yml +++ b/.github/workflows/sdk-codeql.yml @@ -30,6 +30,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: sdk-analyze: if: github.repository == 'prowler-cloud/prowler' diff --git a/.github/workflows/sdk-container-build-push.yml b/.github/workflows/sdk-container-build-push.yml index 4a0563b89f..6d606f21be 100644 --- a/.github/workflows/sdk-container-build-push.yml +++ b/.github/workflows/sdk-container-build-push.yml @@ -47,6 +47,8 @@ env: # AWS configuration (for ECR) AWS_REGION: us-east-1 +permissions: {} + jobs: setup: if: github.repository == 'prowler-cloud/prowler' @@ -74,15 +76,14 @@ jobs: with: persist-credentials: false - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - name: Setup Python with Poetry + uses: ./.github/actions/setup-python-poetry with: python-version: ${{ env.PYTHON_VERSION }} + install-dependencies: 'false' - - name: Install Poetry - run: | - pipx install poetry==2.1.1 - pipx inject poetry poetry-bumpversion + - name: Inject poetry-bumpversion plugin + run: pipx inject poetry poetry-bumpversion - name: Get Prowler version and set tags id: get-prowler-version @@ -197,13 +198,13 @@ jobs: persist-credentials: false - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to Public ECR - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: public.ecr.aws username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }} @@ -212,12 +213,12 @@ jobs: AWS_REGION: ${{ env.AWS_REGION }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build and push SDK container for ${{ matrix.arch }} id: container-push if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . file: ${{ env.DOCKERFILE_PATH }} @@ -252,13 +253,13 @@ jobs: - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to Public ECR - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: public.ecr.aws username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }} @@ -295,7 +296,7 @@ jobs: # Push to toniblyx/prowler only for current version (latest/stable/release tags) - name: Login to DockerHub (toniblyx) if: needs.setup.outputs.latest_tag == 'latest' - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.TONIBLYX_DOCKERHUB_USERNAME }} password: ${{ secrets.TONIBLYX_DOCKERHUB_PASSWORD }} @@ -320,7 +321,7 @@ jobs: # Re-login as prowlercloud for cleanup of intermediate tags - name: Login to DockerHub (prowlercloud) if: always() - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/sdk-container-checks.yml b/.github/workflows/sdk-container-checks.yml index dacf160a86..3474df676e 100644 --- a/.github/workflows/sdk-container-checks.yml +++ b/.github/workflows/sdk-container-checks.yml @@ -17,6 +17,8 @@ concurrency: env: IMAGE_NAME: prowler +permissions: {} + jobs: sdk-dockerfile-lint: if: github.repository == 'prowler-cloud/prowler' @@ -41,7 +43,7 @@ jobs: - name: Check if Dockerfile changed id: dockerfile-changed - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: Dockerfile @@ -85,6 +87,7 @@ jobs: check.trivy.dev:443 debian.map.fastlydns.net:80 release-assets.githubusercontent.com:443 + objects.githubusercontent.com:443 pypi.org:443 files.pythonhosted.org:443 www.powershellgallery.com:443 @@ -102,7 +105,7 @@ jobs: - name: Check for SDK changes id: check-changes - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: ./** files_ignore: | @@ -127,11 +130,11 @@ jobs: - name: Set up Docker Buildx if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build SDK container for ${{ matrix.arch }} if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . push: false diff --git a/.github/workflows/sdk-pypi-release.yml b/.github/workflows/sdk-pypi-release.yml index 5173760205..1932e84ed7 100644 --- a/.github/workflows/sdk-pypi-release.yml +++ b/.github/workflows/sdk-pypi-release.yml @@ -13,6 +13,8 @@ env: RELEASE_TAG: ${{ github.event.release.tag_name }} PYTHON_VERSION: '3.12' +permissions: {} + jobs: validate-release: if: github.repository == 'prowler-cloud/prowler' @@ -73,13 +75,11 @@ jobs: with: persist-credentials: false - - name: Install Poetry - run: pipx install poetry==2.1.1 - - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - name: Setup Python with Poetry + uses: ./.github/actions/setup-python-poetry with: python-version: ${{ env.PYTHON_VERSION }} + install-dependencies: 'false' - name: Build Prowler package run: poetry build @@ -111,13 +111,11 @@ jobs: with: persist-credentials: false - - name: Install Poetry - run: pipx install poetry==2.1.1 - - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - name: Setup Python with Poetry + uses: ./.github/actions/setup-python-poetry with: python-version: ${{ env.PYTHON_VERSION }} + install-dependencies: 'false' - name: Install toml package run: pip install toml diff --git a/.github/workflows/sdk-refresh-aws-services-regions.yml b/.github/workflows/sdk-refresh-aws-services-regions.yml index ce8d1c8292..3ece09e8a5 100644 --- a/.github/workflows/sdk-refresh-aws-services-regions.yml +++ b/.github/workflows/sdk-refresh-aws-services-regions.yml @@ -13,6 +13,8 @@ env: PYTHON_VERSION: '3.12' AWS_REGION: 'us-east-1' +permissions: {} + jobs: refresh-aws-regions: if: github.repository == 'prowler-cloud/prowler' diff --git a/.github/workflows/sdk-refresh-oci-regions.yml b/.github/workflows/sdk-refresh-oci-regions.yml index 67f87076b7..958d5beb80 100644 --- a/.github/workflows/sdk-refresh-oci-regions.yml +++ b/.github/workflows/sdk-refresh-oci-regions.yml @@ -12,6 +12,8 @@ concurrency: env: PYTHON_VERSION: '3.12' +permissions: {} + jobs: refresh-oci-regions: if: github.repository == 'prowler-cloud/prowler' diff --git a/.github/workflows/sdk-security.yml b/.github/workflows/sdk-security.yml index 2229568b3f..c13061ff1b 100644 --- a/.github/workflows/sdk-security.yml +++ b/.github/workflows/sdk-security.yml @@ -14,6 +14,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: sdk-security-scans: if: github.repository == 'prowler-cloud/prowler' @@ -44,7 +46,7 @@ jobs: - name: Check for SDK changes id: check-changes - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: ./** @@ -69,20 +71,11 @@ jobs: contrib/** **/AGENTS.md - - name: Install Poetry + - name: Setup Python with Poetry if: steps.check-changes.outputs.any_changed == 'true' - run: pipx install poetry==2.1.1 - - - name: Set up Python 3.12 - if: steps.check-changes.outputs.any_changed == 'true' - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: ./.github/actions/setup-python-poetry with: python-version: '3.12' - cache: 'poetry' - - - name: Install dependencies - if: steps.check-changes.outputs.any_changed == 'true' - run: poetry install --no-root - name: Security scan with Bandit if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index b649db4f5a..d63ed38834 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -14,6 +14,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: sdk-tests: if: github.repository == 'prowler-cloud/prowler' @@ -67,7 +69,7 @@ jobs: - name: Check for SDK changes id: check-changes - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: ./** files_ignore: | @@ -90,26 +92,17 @@ jobs: contrib/** **/AGENTS.md - - name: Install Poetry + - name: Setup Python with Poetry if: steps.check-changes.outputs.any_changed == 'true' - run: pipx install poetry==2.1.1 - - - name: Set up Python ${{ matrix.python-version }} - if: steps.check-changes.outputs.any_changed == 'true' - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: ./.github/actions/setup-python-poetry with: python-version: ${{ matrix.python-version }} - cache: 'poetry' - - - name: Install dependencies - if: steps.check-changes.outputs.any_changed == 'true' - run: poetry install --no-root # AWS Provider - name: Check if AWS files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-aws - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/aws/** @@ -239,7 +232,7 @@ jobs: - name: Check if Azure files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-azure - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/azure/** @@ -263,7 +256,7 @@ jobs: - name: Check if GCP files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-gcp - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/gcp/** @@ -287,7 +280,7 @@ jobs: - name: Check if Kubernetes files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-kubernetes - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/kubernetes/** @@ -311,7 +304,7 @@ jobs: - name: Check if GitHub files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-github - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/github/** @@ -335,7 +328,7 @@ jobs: - name: Check if NHN files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-nhn - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/nhn/** @@ -359,7 +352,7 @@ jobs: - name: Check if M365 files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-m365 - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/m365/** @@ -383,7 +376,7 @@ jobs: - name: Check if IaC files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-iac - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/iac/** @@ -407,7 +400,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@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/mongodbatlas/** @@ -431,7 +424,7 @@ jobs: - name: Check if OCI files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-oraclecloud - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/oraclecloud/** @@ -455,7 +448,7 @@ jobs: - name: Check if OpenStack files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-openstack - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/openstack/** @@ -479,7 +472,7 @@ jobs: - name: Check if Google Workspace files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-googleworkspace - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/googleworkspace/** @@ -503,7 +496,7 @@ jobs: - name: Check if Vercel files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-vercel - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/**/vercel/** @@ -527,7 +520,7 @@ jobs: - name: Check if Lib files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-lib - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/lib/** @@ -551,7 +544,7 @@ jobs: - name: Check if Config files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-config - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ./prowler/config/** diff --git a/.github/workflows/test-impact-analysis.yml b/.github/workflows/test-impact-analysis.yml index f03f99d0fc..88500551f2 100644 --- a/.github/workflows/test-impact-analysis.yml +++ b/.github/workflows/test-impact-analysis.yml @@ -31,6 +31,8 @@ on: description: "Whether there are UI E2E tests to run" value: ${{ jobs.analyze.outputs.has-ui-e2e }} +permissions: {} + jobs: analyze: runs-on: ubuntu-latest @@ -66,7 +68,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/ui-bump-version.yml b/.github/workflows/ui-bump-version.yml index 9566c53006..273778e1c2 100644 --- a/.github/workflows/ui-bump-version.yml +++ b/.github/workflows/ui-bump-version.yml @@ -13,6 +13,8 @@ env: PROWLER_VERSION: ${{ github.event.release.tag_name }} BASE_BRANCH: master +permissions: {} + jobs: detect-release-type: runs-on: ubuntu-latest diff --git a/.github/workflows/ui-codeql.yml b/.github/workflows/ui-codeql.yml index c4ab182e92..c8b65e77d3 100644 --- a/.github/workflows/ui-codeql.yml +++ b/.github/workflows/ui-codeql.yml @@ -26,6 +26,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: ui-analyze: if: github.repository == 'prowler-cloud/prowler' diff --git a/.github/workflows/ui-container-build-push.yml b/.github/workflows/ui-container-build-push.yml index c8886a5ae9..6fab2d1efb 100644 --- a/.github/workflows/ui-container-build-push.yml +++ b/.github/workflows/ui-container-build-push.yml @@ -35,6 +35,8 @@ env: # Build args NEXT_PUBLIC_API_BASE_URL: http://prowler-api:8080/api/v1 +permissions: {} + jobs: setup: if: github.repository == 'prowler-cloud/prowler' @@ -127,18 +129,18 @@ jobs: persist-credentials: false - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build and push UI container for ${{ matrix.arch }} id: container-push if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: ${{ env.WORKING_DIRECTORY }} build-args: | @@ -172,7 +174,7 @@ jobs: production.cloudflare.docker.com:443 - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/ui-container-checks.yml b/.github/workflows/ui-container-checks.yml index 10a910ace4..eb7b508b58 100644 --- a/.github/workflows/ui-container-checks.yml +++ b/.github/workflows/ui-container-checks.yml @@ -18,6 +18,8 @@ env: UI_WORKING_DIR: ./ui IMAGE_NAME: prowler-ui +permissions: {} + jobs: ui-dockerfile-lint: if: github.repository == 'prowler-cloud/prowler' @@ -42,7 +44,7 @@ jobs: - name: Check if Dockerfile changed id: dockerfile-changed - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: ui/Dockerfile @@ -89,6 +91,8 @@ jobs: mirror.gcr.io:443 check.trivy.dev:443 get.trivy.dev:443 + release-assets.githubusercontent.com:443 + objects.githubusercontent.com:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -98,7 +102,7 @@ jobs: - name: Check for UI changes id: check-changes - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: ui/** files_ignore: | @@ -108,11 +112,11 @@ jobs: - name: Set up Docker Buildx if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build UI container for ${{ matrix.arch }} if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: ${{ env.UI_WORKING_DIR }} target: prod diff --git a/.github/workflows/ui-e2e-tests-v2.yml b/.github/workflows/ui-e2e-tests-v2.yml index 53533474fa..2a91e460be 100644 --- a/.github/workflows/ui-e2e-tests-v2.yml +++ b/.github/workflows/ui-e2e-tests-v2.yml @@ -15,6 +15,8 @@ on: - 'ui/**' - 'api/**' # API changes can affect UI E2E +permissions: {} + jobs: # First, analyze which tests need to run impact-analysis: @@ -158,12 +160,12 @@ jobs: ' - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '24.13.0' - name: Setup pnpm - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 with: package_json_file: ui/package.json run_install: false @@ -172,7 +174,7 @@ jobs: run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm and Next.js cache - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: | ${{ env.STORE_PATH }} @@ -192,7 +194,7 @@ jobs: run: pnpm run build - name: Cache Playwright browsers - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 id: playwright-cache with: path: ~/.cache/ms-playwright @@ -259,7 +261,7 @@ jobs: fi - name: Upload test reports - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: playwright-report diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 57cb149523..c11e90b8f4 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -18,6 +18,8 @@ env: UI_WORKING_DIR: ./ui NODE_VERSION: '24.13.0' +permissions: {} + jobs: ui-tests: runs-on: ubuntu-latest @@ -49,7 +51,7 @@ jobs: - name: Check for UI changes id: check-changes - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ui/** @@ -62,7 +64,7 @@ jobs: - name: Get changed source files for targeted tests id: changed-source if: steps.check-changes.outputs.any_changed == 'true' - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ui/**/*.ts @@ -78,7 +80,7 @@ jobs: - name: Check for critical path changes (run all tests) id: critical-changes if: steps.check-changes.outputs.any_changed == 'true' - uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 with: files: | ui/lib/** @@ -90,13 +92,13 @@ jobs: - name: Setup Node.js ${{ env.NODE_VERSION }} if: steps.check-changes.outputs.any_changed == 'true' - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ env.NODE_VERSION }} - name: Setup pnpm if: steps.check-changes.outputs.any_changed == 'true' - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 with: package_json_file: ui/package.json run_install: false @@ -108,7 +110,7 @@ jobs: - name: Setup pnpm and Next.js cache if: steps.check-changes.outputs.any_changed == 'true' - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: | ${{ env.STORE_PATH }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a45f436284..633c10a80f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,7 +70,7 @@ repos: args: ["--ignore=E266,W503,E203,E501,W605"] - repo: https://github.com/python-poetry/poetry - rev: 2.1.1 + rev: 2.3.4 hooks: - id: poetry-check name: API - poetry-check @@ -128,7 +128,8 @@ repos: # TODO: Botocore needs urllib3 1.X so we need to ignore these vulnerabilities 77744,77745. Remove this once we upgrade to urllib3 2.X # TODO: 79023 & 79027 knack ReDoS until `azure-cli-core` (via `cartography`) allows `knack` >=0.13.0 # TODO: 86217 because `alibabacloud-tea-openapi == 0.4.3` don't let us upgrade `cryptography >= 46.0.0` - entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745,79023,79027,86217' + # TODO: 71600 CVE-2024-1135 false positive - fixed in gunicorn 22.0.0, project uses 23.0.0 + entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745,79023,79027,86217,71600' language: system - id: vulture diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 17d338d2e9..5d375cd0df 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -13,7 +13,7 @@ build: post_create_environment: # Install poetry # https://python-poetry.org/docs/#installing-manually - - python -m pip install poetry + - python -m pip install poetry==2.3.4 post_install: # Install dependencies with 'docs' dependency group # https://python-poetry.org/docs/managing-dependencies/#dependency-groups diff --git a/AGENTS.md b/AGENTS.md index 5302e38f52..1d7a36c667 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -140,7 +140,7 @@ Prowler is an open-source cloud security assessment tool supporting AWS, Azure, | Component | Location | Tech Stack | |-----------|----------|------------| -| SDK | `prowler/` | Python 3.10+, Poetry | +| SDK | `prowler/` | Python 3.10+, Poetry 2.3+ | | API | `api/` | Django 5.1, DRF, Celery | | UI | `ui/` | Next.js 15, React 19, Tailwind 4 | | MCP Server | `mcp_server/` | FastMCP, Python 3.12+ | @@ -153,12 +153,12 @@ Prowler is an open-source cloud security assessment tool supporting AWS, Azure, ```bash # Setup poetry install --with dev -poetry run pre-commit install +poetry run prek install # Code quality poetry run make lint poetry run make format -poetry run pre-commit run --all-files +poetry run prek run --all-files ``` --- diff --git a/Dockerfile b/Dockerfile index 0dbe63ece0..e8a0d19a56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,7 +68,7 @@ ENV HOME='/home/prowler' ENV PATH="${HOME}/.local/bin:${PATH}" #hadolint ignore=DL3013 RUN pip install --no-cache-dir --upgrade pip && \ - pip install --no-cache-dir poetry + pip install --no-cache-dir poetry==2.3.4 RUN poetry install --compile && \ rm -rf ~/.cache/pip diff --git a/README.md b/README.md index 13ec785662..07e7a8e09d 100644 --- a/README.md +++ b/README.md @@ -246,14 +246,7 @@ Some pre-commit hooks require tools installed on your system: 1. **Install [TruffleHog](https://github.com/trufflesecurity/trufflehog#install)** (secret scanning) — see the [official installation options](https://github.com/trufflesecurity/trufflehog#install). -2. **Install [Safety](https://github.com/pyupio/safety)** (dependency vulnerability checking): - - ```console - # Requires a Python environment (e.g. via pyenv) - pip install safety - ``` - -3. **Install [Hadolint](https://github.com/hadolint/hadolint#install)** (Dockerfile linting) — see the [official installation options](https://github.com/hadolint/hadolint#install). +2. **Install [Hadolint](https://github.com/hadolint/hadolint#install)** (Dockerfile linting) — see the [official installation options](https://github.com/hadolint/hadolint#install). ## Prowler CLI ### Pip package diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 54b589e496..892cd0cd88 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -2,23 +2,44 @@ All notable changes to the **Prowler API** are documented in this file. -## [1.24.0] (Prowler UNRELEASED) - -### 🚀 Added - -- Pin all unpinned dependencies to exact versions to prevent supply chain attacks and ensure reproducible builds [(#10469)](https://github.com/prowler-cloud/prowler/pull/10469) -- Filter RBAC role lookup by `tenant_id` to prevent cross-tenant privilege leak [(#10491)](https://github.com/prowler-cloud/prowler/pull/10491) -- `VALKEY_SCHEME`, `VALKEY_USERNAME`, and `VALKEY_PASSWORD` environment variables to configure Celery broker TLS/auth connection details for Valkey/ElastiCache [(#10420)](https://github.com/prowler-cloud/prowler/pull/10420) -- `Vercel` provider support [(#10190)](https://github.com/prowler-cloud/prowler/pull/10190) -- Finding groups list and latest endpoints support `sort=delta`, ordering by `new_count` then `changed_count` so groups with the most new findings rank highest [(#10606)](https://github.com/prowler-cloud/prowler/pull/10606) +## [1.25.0] (Prowler UNRELEASED) ### 🔄 Changed +- Bump Poetry to `2.3.4` in Dockerfile and pre-commit hooks. Regenerate `api/poetry.lock` [(#10681)](https://github.com/prowler-cloud/prowler/pull/10681) + +### 🐞 Fixed + +- Worker-beat race condition on cold start: replaced `sleep 15` with API service healthcheck dependency (Docker Compose) and init containers (Helm), aligned Gunicorn default port to `8080` [(#10603)](https://github.com/prowler-cloud/prowler/pull/10603) +- API container startup crash on Linux due to root-owned bind-mount preventing JWT key generation [(#10646)](https://github.com/prowler-cloud/prowler/pull/10646) + +### 🔐 Security + +- `pytest` from 8.2.2 to 9.0.3 to fix CVE-2025-71176 [(#10678)](https://github.com/prowler-cloud/prowler/pull/10678) + +--- + +## [1.24.0] (Prowler v5.23.0) + +### 🚀 Added + +- RBAC role lookup filtered by `tenant_id` to prevent cross-tenant privilege leak [(#10491)](https://github.com/prowler-cloud/prowler/pull/10491) +- `VALKEY_SCHEME`, `VALKEY_USERNAME`, and `VALKEY_PASSWORD` environment variables to configure Celery broker TLS/auth connection details for Valkey/ElastiCache [(#10420)](https://github.com/prowler-cloud/prowler/pull/10420) +- `Vercel` provider support [(#10190)](https://github.com/prowler-cloud/prowler/pull/10190) +- Finding groups list and latest endpoints support `sort=delta`, ordering by `new_count` then `changed_count` so groups with the most new findings rank highest [(#10606)](https://github.com/prowler-cloud/prowler/pull/10606) +- Finding group resources endpoints (`/finding-groups/{check_id}/resources` and `/finding-groups/latest/{check_id}/resources`) now expose `finding_id` per row, pointing to the most recent matching Finding for each resource. UUIDv7 ordering guarantees `Max(finding__id)` resolves to the latest snapshot [(#10630)](https://github.com/prowler-cloud/prowler/pull/10630) +- Handle CIS and CISA SCuBA compliance framework from google workspace [(#10629)](https://github.com/prowler-cloud/prowler/pull/10629) +- Sort support for all finding group counter fields: `pass_muted_count`, `fail_muted_count`, `manual_muted_count`, and all `new_*`/`changed_*` status-mute breakdown counters [(#10655)](https://github.com/prowler-cloud/prowler/pull/10655) + +### 🔄 Changed + +- Finding groups list/latest/resources now expose `status` ∈ `{FAIL, PASS, MANUAL}` and `muted: bool` as orthogonal fields. The aggregated `status` reflects the underlying check outcome regardless of mute state, and `muted=true` signals that every finding in the group/resource is muted. New `manual_count` is exposed alongside `pass_count`/`fail_count`, plus `pass_muted_count`/`fail_muted_count`/`manual_muted_count` siblings so clients can isolate the muted half of each status. The `new_*`/`changed_*` deltas are now broken down by status and mute state via 12 new counters (`new_fail_count`, `new_fail_muted_count`, `new_pass_count`, `new_pass_muted_count`, `new_manual_count`, `new_manual_muted_count` and the matching `changed_*` set). New `filter[muted]=true|false` and `sort=status` (FAIL > PASS > MANUAL) / `sort=muted` are supported. `filter[status]=MUTED` is no longer accepted [(#10630)](https://github.com/prowler-cloud/prowler/pull/10630) - Attack Paths: Periodic cleanup of stale scans with dead-worker detection via Celery inspect, marking orphaned `EXECUTING` scans as `FAILED` and recovering `graph_data_ready` [(#10387)](https://github.com/prowler-cloud/prowler/pull/10387) - Attack Paths: Replace `_provider_id` property with `_Provider_{uuid}` label for provider isolation, add regex-based label injection for custom queries [(#10402)](https://github.com/prowler-cloud/prowler/pull/10402) ### 🐞 Fixed +- `reaggregate_all_finding_group_summaries_task` now refreshes finding group daily summaries for every `(provider, day)` combination instead of only the latest scan per provider, matching the unbounded scope of `mute_historical_findings_task`. Mute rule operations no longer leave older daily summaries drifting from the underlying muted findings [(#10630)](https://github.com/prowler-cloud/prowler/pull/10630) - Finding groups list/latest now apply computed status/severity filters and finding-level prefilters (delta, region, service, category, resource group, scan, resource type), plus `check_title` support for sort/filter consistency [(#10428)](https://github.com/prowler-cloud/prowler/pull/10428) - Populate compliance data inside `check_metadata` for findings, which was always returned as `null` [(#10449)](https://github.com/prowler-cloud/prowler/pull/10449) - 403 error for admin users listing tenants due to roles query not using the admin database connection [(#10460)](https://github.com/prowler-cloud/prowler/pull/10460) @@ -36,6 +57,7 @@ All notable changes to the **Prowler API** are documented in this file. - Pin all unpinned dependencies to exact versions to prevent supply chain attacks and ensure reproducible builds [(#10469)](https://github.com/prowler-cloud/prowler/pull/10469) - `authlib` bumped from 1.6.6 to 1.6.9 to fix CVE-2026-28802 (JWT `alg: none` validation bypass) [(#10579)](https://github.com/prowler-cloud/prowler/pull/10579) +- `aiohttp` bumped from 3.13.3 to 3.13.5 to fix CVE-2026-34520 (the C parser accepted null bytes and control characters in response headers) [(#10538)](https://github.com/prowler-cloud/prowler/pull/10538) --- diff --git a/api/Dockerfile b/api/Dockerfile index ffa12c6f88..07f69d0b0f 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -71,7 +71,7 @@ RUN mkdir -p /tmp/prowler_api_output COPY pyproject.toml ./ RUN pip install --no-cache-dir --upgrade pip && \ - pip install --no-cache-dir poetry + pip install --no-cache-dir poetry==2.3.4 ENV PATH="/home/prowler/.local/bin:$PATH" diff --git a/api/docker-entrypoint.sh b/api/docker-entrypoint.sh index a980595af2..fb1e1693b4 100755 --- a/api/docker-entrypoint.sh +++ b/api/docker-entrypoint.sh @@ -56,7 +56,6 @@ start_worker() { start_worker_beat() { echo "Starting the worker-beat..." - sleep 15 poetry run python -m celery -A config.celery beat -l "${DJANGO_LOGGING_LEVEL:-info}" --scheduler django_celery_beat.schedulers:DatabaseScheduler } diff --git a/api/poetry.lock b/api/poetry.lock index 95f7cce24e..5e0ca3cb09 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. [[package]] name = "about-time" @@ -103,132 +103,132 @@ files = [ [[package]] name = "aiohttp" -version = "3.13.3" +version = "3.13.5" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {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"}, + {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b"}, + {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5"}, + {file = "aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7"}, + {file = "aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9"}, + {file = "aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76"}, + {file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6"}, + {file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d"}, + {file = "aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3"}, + {file = "aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06"}, + {file = "aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8"}, + {file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9"}, + {file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416"}, + {file = "aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14"}, + {file = "aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3"}, + {file = "aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1"}, + {file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61"}, + {file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832"}, + {file = "aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c"}, + {file = "aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc"}, + {file = "aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83"}, + {file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c"}, + {file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be"}, + {file = "aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b"}, + {file = "aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3"}, + {file = "aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162"}, + {file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a"}, + {file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254"}, + {file = "aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9"}, + {file = "aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8"}, + {file = "aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9"}, + {file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:347542f0ea3f95b2a955ee6656461fa1c776e401ac50ebce055a6c38454a0adf"}, + {file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:178c7b5e62b454c2bc790786e6058c3cc968613b4419251b478c153a4aec32b1"}, + {file = "aiohttp-3.13.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af545c2cffdb0967a96b6249e6f5f7b0d92cdfd267f9d5238d5b9ca63e8edb10"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:206b7b3ef96e4ce211754f0cd003feb28b7d81f0ad26b8d077a5d5161436067f"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ee5e86776273de1795947d17bddd6bb19e0365fd2af4289c0d2c5454b6b1d36b"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95d14ca7abefde230f7639ec136ade282655431fd5db03c343b19dda72dd1643"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:912d4b6af530ddb1338a66229dac3a25ff11d4448be3ec3d6340583995f56031"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e999f0c88a458c836d5fb521814e92ed2172c649200336a6df514987c1488258"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39380e12bd1f2fdab4285b6e055ad48efbaed5c836433b142ed4f5b9be71036a"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9efcc0f11d850cefcafdd9275b9576ad3bfb539bed96807663b32ad99c4d4b88"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:147b4f501d0292077f29d5268c16bb7c864a1f054d7001c4c1812c0421ea1ed0"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d147004fede1b12f6013a6dbb2a26a986a671a03c6ea740ddc76500e5f1c399f"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9277145d36a01653863899c665243871434694bcc3431922c3b35c978061bdb8"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4e704c52438f66fdd89588346183d898bb42167cf88f8b7ff1c0f9fc957c348f"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8a4d3427e8de1312ddf309cc482186466c79895b3a139fed3259fc01dfa9a5b"}, + {file = "aiohttp-3.13.5-cp39-cp39-win32.whl", hash = "sha256:6f497a6876aa4b1a102b04996ce4c1170c7040d83faa9387dd921c16e30d5c83"}, + {file = "aiohttp-3.13.5-cp39-cp39-win_amd64.whl", hash = "sha256:cb979826071c0986a5f08333a36104153478ce6018c58cba7f9caddaf63d5d67"}, + {file = "aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1"}, ] [package.dependencies] @@ -2961,7 +2961,7 @@ files = [ [package.dependencies] autopep8 = "*" Django = ">=4.2" -gprof2dot = ">=2017.09.19" +gprof2dot = ">=2017.9.19" sqlparse = "*" [[package]] @@ -4569,7 +4569,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -4777,7 +4777,7 @@ librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] mongodb = ["pymongo (==4.15.3)"] msgpack = ["msgpack (==1.1.2)"] pyro = ["pyro4 (==4.82)"] -qpid = ["qpid-python (==1.36.0-1)", "qpid-tools (==1.36.0-1)"] +qpid = ["qpid-python (==1.36.0.post1)", "qpid-tools (==1.36.0.post1)"] redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2,<6.5)"] slmq = ["softlayer_messaging (>=1.0.3)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] @@ -4798,7 +4798,7 @@ files = [ ] [package.dependencies] -certifi = ">=14.05.14" +certifi = ">=14.5.14" durationpy = ">=0.7" google-auth = ">=1.0.1" oauthlib = ">=3.2.2" @@ -6445,6 +6445,33 @@ docs = ["sphinx (>=1.7.1)"] redis = ["redis"] tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"] +[[package]] +name = "prek" +version = "0.3.9" +description = "A Git hook manager written in Rust, designed as a drop-in alternative to pre-commit." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "prek-0.3.9-py3-none-linux_armv6l.whl", hash = "sha256:3ed793d51bfaa27bddb64d525d7acb77a7c8644f549412d82252e3eb0b88aad8"}, + {file = "prek-0.3.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:399c58400c0bd0b82a93a3c09dc1bfd88d8d0cfb242d414d2ed247187b06ead1"}, + {file = "prek-0.3.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ea1ffb124e92f081b8e2ca5b5a623a733efb3be0c5b1f4b7ffe2ee17d1f20c"}, + {file = "prek-0.3.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:aaf639f95b7301639298311d8d44aad0d0b4864e9736083ad3c71ce9765d37ab"}, + {file = "prek-0.3.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff104863b187fa443ea8451ca55d51e2c6e94f99f00d88784b5c3c4c623f1ebe"}, + {file = "prek-0.3.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:039ecaf87c63a3e67cca645ebd5bc5eb6aafa6c9d929e9a27b2921e7849d7ef9"}, + {file = "prek-0.3.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bde2a3d045705095983c7f78ba04f72a7565fe1c2b4e85f5628502a254754ff"}, + {file = "prek-0.3.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0960a21543563e2c8e19aaad176cc8423a87aac3c914d0f313030d7a9244a"}, + {file = "prek-0.3.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb5d5171d7523271909246ee306b4dc3d5b63752e7dd7c7e8a8908fc9490d1"}, + {file = "prek-0.3.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82b791bd36c1430c84d3ae7220a85152babc7eaf00f70adcb961bd594e756ba3"}, + {file = "prek-0.3.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:6eac6d2f736b041118f053a1487abed468a70dd85a8688eaf87bb42d3dcecf20"}, + {file = "prek-0.3.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5517e46e761367a3759b3168eabc120840ffbca9dfbc53187167298a98f87dc4"}, + {file = "prek-0.3.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:92024778cf78683ca32687bb249ab6a7d5c33887b5ee1d1a9f6d0c14228f4cf3"}, + {file = "prek-0.3.9-py3-none-win32.whl", hash = "sha256:7f89c55e5f480f5d073769e319924ad69d4bf9f98c5cb46a83082e26e634c958"}, + {file = "prek-0.3.9-py3-none-win_amd64.whl", hash = "sha256:7722f3372eaa83b147e70a43cb7b9fe2128c13d0c78d8a1cdbf2a8ec2ee071eb"}, + {file = "prek-0.3.9-py3-none-win_arm64.whl", hash = "sha256:0bced6278d6cc8a4b46048979e36bc9da034611dc8facd77ab123177b833a929"}, + {file = "prek-0.3.9.tar.gz", hash = "sha256:f82b92d81f42f1f90a47f5fbbf492373e25ef1f790080215b2722dd6da66510e"}, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -7129,14 +7156,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.11.0" +version = "2.12.1" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469"}, - {file = "pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623"}, + {file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"}, + {file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"}, ] [package.dependencies] @@ -7161,7 +7188,7 @@ files = [ ] [package.dependencies] -astroid = ">=3.2.2,<=3.3.0-dev0" +astroid = ">=3.2.2,<=3.3.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, @@ -7324,24 +7351,25 @@ files = [ [[package]] name = "pytest" -version = "8.2.2" +version = "9.0.3" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, + {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, + {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2.0" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-celery" @@ -8174,10 +8202,10 @@ files = [ ] [package.dependencies] -botocore = ">=1.37.4,<2.0a.0" +botocore = ">=1.37.4,<2.0a0" [package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] [[package]] name = "safety" @@ -9372,4 +9400,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "167d4549788b8bc8bb7772b9a81ade1eab73d8f354251a8d6af4901223cc7f67" +content-hash = "077e89853cfe3a6d934841488cfa5a98ff6c92b71f74b817b71387d11559f143" diff --git a/api/pyproject.toml b/api/pyproject.toml index 45e7899f7a..19827f2480 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -50,7 +50,7 @@ name = "prowler-api" package-mode = false # Needed for the SDK compatibility requires-python = ">=3.11,<3.13" -version = "1.24.0" +version = "1.25.0" [project.scripts] celery = "src.backend.config.settings.celery" @@ -65,7 +65,7 @@ freezegun = "1.5.1" marshmallow = "==3.26.2" mypy = "1.10.1" pylint = "3.2.5" -pytest = "8.2.2" +pytest = "9.0.3" pytest-cov = "5.0.0" pytest-django = "4.8.0" pytest-env = "1.1.3" @@ -75,3 +75,4 @@ ruff = "0.5.0" safety = "3.7.0" tqdm = "4.67.1" vulture = "2.14" +prek = "0.3.9" diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py index a496a0bf67..fa31289964 100644 --- a/api/src/backend/api/filters.py +++ b/api/src/backend/api/filters.py @@ -1115,13 +1115,14 @@ class FindingGroupAggregatedComputedFilter(FilterSet): STATUS_CHOICES = ( ("FAIL", "Fail"), ("PASS", "Pass"), - ("MUTED", "Muted"), + ("MANUAL", "Manual"), ) status = ChoiceFilter(method="filter_status", choices=STATUS_CHOICES) status__in = CharInFilter(method="filter_status_in", lookup_expr="in") severity = ChoiceFilter(method="filter_severity", choices=SeverityChoices) severity__in = CharInFilter(method="filter_severity_in", lookup_expr="in") + muted = BooleanFilter(field_name="muted") include_muted = BooleanFilter(method="filter_include_muted") def filter_status(self, queryset, name, value): @@ -1198,7 +1199,7 @@ class FindingGroupAggregatedComputedFilter(FilterSet): if value is True: return queryset # include_muted=false: exclude fully-muted groups - return queryset.exclude(fail_count=0, pass_count=0, muted_count__gt=0) + return queryset.exclude(muted=True) class ProviderSecretFilter(FilterSet): diff --git a/api/src/backend/api/migrations/0088_finding_group_status_muted_fields.py b/api/src/backend/api/migrations/0088_finding_group_status_muted_fields.py new file mode 100644 index 0000000000..ff3b981435 --- /dev/null +++ b/api/src/backend/api/migrations/0088_finding_group_status_muted_fields.py @@ -0,0 +1,95 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0087_vercel_provider"), + ] + + operations = [ + migrations.AddField( + model_name="findinggroupdailysummary", + name="manual_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="pass_muted_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="fail_muted_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="manual_muted_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="muted", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="new_fail_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="new_fail_muted_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="new_pass_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="new_pass_muted_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="new_manual_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="new_manual_muted_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="changed_fail_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="changed_fail_muted_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="changed_pass_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="changed_pass_muted_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="changed_manual_count", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="findinggroupdailysummary", + name="changed_manual_muted_count", + field=models.IntegerField(default=0), + ), + ] diff --git a/api/src/backend/api/migrations/0089_backfill_finding_group_status_muted.py b/api/src/backend/api/migrations/0089_backfill_finding_group_status_muted.py new file mode 100644 index 0000000000..501fcf3cb4 --- /dev/null +++ b/api/src/backend/api/migrations/0089_backfill_finding_group_status_muted.py @@ -0,0 +1,31 @@ +from django.db import migrations +from tasks.tasks import backfill_finding_group_summaries_task + +from api.db_router import MainRouter +from api.rls import Tenant + + +def trigger_backfill_task(apps, schema_editor): + """ + Re-dispatch the finding-group backfill task for every tenant so the new + `manual_count` and `muted` columns added in 0088 get populated from the + last 10 days of completed scans. + + The aggregator (`aggregate_finding_group_summaries`) recomputes every + column on each call, so it back-populates the new fields without touching + the existing ones beyond a normal upsert. + """ + tenant_ids = Tenant.objects.using(MainRouter.admin_db).values_list("id", flat=True) + + for tenant_id in tenant_ids: + backfill_finding_group_summaries_task.delay(tenant_id=str(tenant_id), days=10) + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0088_finding_group_status_muted_fields"), + ] + + operations = [ + migrations.RunPython(trigger_backfill_task, migrations.RunPython.noop), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 5e0880be08..7f3131fe7b 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -1748,15 +1748,45 @@ class FindingGroupDailySummary(RowLevelSecurityProtectedModel): # Severity stored as integer for MAX aggregation (5=critical, 4=high, etc.) severity_order = models.SmallIntegerField(default=1) - # Finding counts + # Finding counts (inclusive of muted findings; use the `muted` flag to + # tell whether the group has any actionable findings). pass_count = models.IntegerField(default=0) fail_count = models.IntegerField(default=0) + manual_count = models.IntegerField(default=0) muted_count = models.IntegerField(default=0) - # Delta counts + # Status counts restricted to muted findings, so clients can isolate the + # muted half of each status (e.g. `pass_count - pass_muted_count` gives the + # actionable PASS findings). + pass_muted_count = models.IntegerField(default=0) + fail_muted_count = models.IntegerField(default=0) + manual_muted_count = models.IntegerField(default=0) + + # Whether every finding for this (provider, check, day) is muted. + muted = models.BooleanField(default=False) + + # Delta counts (non-muted, kept for convenience and as a "total" view). new_count = models.IntegerField(default=0) changed_count = models.IntegerField(default=0) + # Delta breakdown by (status, muted) so clients can answer questions like + # "how many new failing findings appeared in this scan?" without scanning + # the underlying findings table. Mirrors the existing pass/fail/manual + # naming, with `_muted_count` siblings tracking the muted half of each + # bucket explicitly. + new_fail_count = models.IntegerField(default=0) + new_fail_muted_count = models.IntegerField(default=0) + new_pass_count = models.IntegerField(default=0) + new_pass_muted_count = models.IntegerField(default=0) + new_manual_count = models.IntegerField(default=0) + new_manual_muted_count = models.IntegerField(default=0) + changed_fail_count = models.IntegerField(default=0) + changed_fail_muted_count = models.IntegerField(default=0) + changed_pass_count = models.IntegerField(default=0) + changed_pass_muted_count = models.IntegerField(default=0) + changed_manual_count = models.IntegerField(default=0) + changed_manual_muted_count = models.IntegerField(default=0) + # Resource counts resources_fail = models.IntegerField(default=0) resources_total = models.IntegerField(default=0) diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 7819f8010c..816790de07 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Prowler API - version: 1.24.0 + version: 1.25.0 description: |- Prowler API specification. diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 439a355193..2249a041ab 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -15445,10 +15445,16 @@ class TestFindingGroupViewSet: # iam_password_policy has only PASS findings assert data[0]["attributes"]["status"] == "PASS" - def test_finding_groups_status_muted_all( + def test_finding_groups_fully_muted_group_reflects_underlying_status( self, authenticated_client, finding_groups_fixture ): - """Test that MUTED status returned when all findings are muted.""" + """A fully-muted group still surfaces its underlying status (no MUTED). + + rds_encryption has 2 muted FAIL findings, so the group must report + status=FAIL (the orthogonal `muted` boolean signals it isn't actionable). + The status×muted breakdown lets clients answer 'how many failing + findings are muted in this group'. + """ response = authenticated_client.get( reverse("finding-group-list"), {"filter[inserted_at]": TODAY, "filter[check_id]": "rds_encryption"}, @@ -15456,8 +15462,21 @@ class TestFindingGroupViewSet: assert response.status_code == status.HTTP_200_OK data = response.json()["data"] assert len(data) == 1 - # rds_encryption has all muted findings - assert data[0]["attributes"]["status"] == "MUTED" + attrs = data[0]["attributes"] + assert attrs["status"] == "FAIL" + assert attrs["muted"] is True + assert attrs["fail_count"] == 2 + assert attrs["fail_muted_count"] == 2 + assert attrs["pass_muted_count"] == 0 + assert attrs["manual_muted_count"] == 0 + assert attrs["muted_count"] == 2 + # Sanity: the per-status muted counts must add up to muted_count. + assert ( + attrs["pass_muted_count"] + + attrs["fail_muted_count"] + + attrs["manual_muted_count"] + == attrs["muted_count"] + ) def test_finding_groups_status_filter( self, authenticated_client, finding_groups_fixture @@ -15949,7 +15968,7 @@ class TestFindingGroupViewSet: "extra_filters", [ {}, - {"filter[muted]": "include"}, + {"filter[delta]": "new"}, ], ids=["summary_path", "finding_level_path"], ) @@ -15967,7 +15986,8 @@ class TestFindingGroupViewSet: Parametrized to cover both aggregation paths: - summary_path: default, uses _CheckTitleToCheckIdMixin on summaries - - finding_level_path: filter[muted]=include forces CommonFindingFilters + - finding_level_path: filter[delta]=new forces _aggregate_findings via + CommonFindingFilters (delta is finding-level, not summary-level) """ params = { "filter[inserted_at]": TODAY, @@ -16885,3 +16905,338 @@ class TestFindingGroupViewSet: data = response.json()["data"] # Should still return data, not filtered by the old date assert len(data) == 5 + + def test_finding_groups_status_choices_no_muted( + self, authenticated_client, finding_groups_fixture + ): + """Every returned group must have status ∈ {FAIL, PASS, MANUAL}.""" + response = authenticated_client.get( + reverse("finding-group-list"), + {"filter[inserted_at]": TODAY}, + ) + assert response.status_code == status.HTTP_200_OK + statuses = {item["attributes"]["status"] for item in response.json()["data"]} + assert statuses, "fixture should produce at least one group" + assert statuses <= {"FAIL", "PASS", "MANUAL"} + assert "MUTED" not in statuses + + def test_finding_groups_serializer_exposes_muted_and_manual_count( + self, authenticated_client, finding_groups_fixture + ): + """The /finding-groups payload must expose `muted`, `manual_count` and + the per-status muted siblings (`pass_muted_count`/`fail_muted_count`/ + `manual_muted_count`).""" + response = authenticated_client.get( + reverse("finding-group-list"), + {"filter[inserted_at]": TODAY, "filter[check_id]": "iam_password_policy"}, + ) + assert response.status_code == status.HTTP_200_OK + attrs = response.json()["data"][0]["attributes"] + assert "muted" in attrs and isinstance(attrs["muted"], bool) + assert "manual_count" in attrs and isinstance(attrs["manual_count"], int) + assert attrs["muted"] is False # iam_password_policy has only non-muted PASS + assert attrs["manual_count"] == 0 + assert attrs["pass_muted_count"] == 0 + assert attrs["fail_muted_count"] == 0 + assert attrs["manual_muted_count"] == 0 + + @pytest.mark.parametrize( + "endpoint_name", ["finding-group-list", "finding-group-latest"] + ) + def test_finding_groups_filter_status_muted_is_rejected( + self, authenticated_client, finding_groups_fixture, endpoint_name + ): + """`filter[status]=MUTED` is no longer a valid status value.""" + params = {"filter[status]": "MUTED"} + if endpoint_name == "finding-group-list": + params["filter[inserted_at]"] = TODAY + + response = authenticated_client.get(reverse(endpoint_name), params) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @pytest.mark.parametrize( + "endpoint_name", ["finding-group-list", "finding-group-latest"] + ) + def test_finding_groups_filter_muted_true( + self, authenticated_client, finding_groups_fixture, endpoint_name + ): + """`filter[muted]=true` returns only fully-muted groups.""" + params = {"filter[muted]": "true"} + if endpoint_name == "finding-group-list": + params["filter[inserted_at]"] = TODAY + + response = authenticated_client.get(reverse(endpoint_name), params) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + check_ids = {item["id"] for item in data} + # Only rds_encryption is fully muted in the fixture + assert check_ids == {"rds_encryption"} + assert all(item["attributes"]["muted"] is True for item in data) + + @pytest.mark.parametrize( + "endpoint_name", ["finding-group-list", "finding-group-latest"] + ) + def test_finding_groups_filter_muted_false( + self, authenticated_client, finding_groups_fixture, endpoint_name + ): + """`filter[muted]=false` returns only groups with actionable findings.""" + params = {"filter[muted]": "false"} + if endpoint_name == "finding-group-list": + params["filter[inserted_at]"] = TODAY + + response = authenticated_client.get(reverse(endpoint_name), params) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + check_ids = {item["id"] for item in data} + assert "rds_encryption" not in check_ids + assert check_ids == { + "s3_bucket_public_access", + "ec2_instance_public_ip", + "iam_password_policy", + "cloudtrail_enabled", + } + assert all(item["attributes"]["muted"] is False for item in data) + + @pytest.mark.parametrize( + "endpoint_name", ["finding-group-list", "finding-group-latest"] + ) + def test_finding_groups_sort_by_status( + self, authenticated_client, finding_groups_fixture, endpoint_name + ): + """sort=status orders by aggregated status (FAIL > PASS > MANUAL).""" + priority = {"FAIL": 3, "PASS": 2, "MANUAL": 1} + params = {"sort": "-status"} + if endpoint_name == "finding-group-list": + params["filter[inserted_at]"] = TODAY + + response = authenticated_client.get(reverse(endpoint_name), params) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert data, "fixture should produce groups" + + desc_keys = [priority[item["attributes"]["status"]] for item in data] + assert desc_keys == sorted(desc_keys, reverse=True) + + params["sort"] = "status" + response = authenticated_client.get(reverse(endpoint_name), params) + assert response.status_code == status.HTTP_200_OK + asc_keys = [ + priority[item["attributes"]["status"]] for item in response.json()["data"] + ] + assert asc_keys == sorted(asc_keys) + + @pytest.mark.parametrize( + "endpoint_name", ["finding-group-list", "finding-group-latest"] + ) + def test_finding_groups_sort_by_muted( + self, authenticated_client, finding_groups_fixture, endpoint_name + ): + """sort=muted orders by the boolean muted attribute.""" + # Need include_muted=true so the fully-muted group is part of the result + params = {"sort": "-muted", "filter[include_muted]": "true"} + if endpoint_name == "finding-group-list": + params["filter[inserted_at]"] = TODAY + + response = authenticated_client.get(reverse(endpoint_name), params) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert data, "fixture should produce groups" + + muted_values = [item["attributes"]["muted"] for item in data] + # Descending boolean: True (1) before False (0) + assert muted_values == sorted(muted_values, reverse=True) + + @pytest.mark.parametrize( + "endpoint_name", ["finding-group-list", "finding-group-latest"] + ) + @pytest.mark.parametrize( + "sort_field", + [ + "pass_muted_count", + "fail_muted_count", + "manual_muted_count", + "new_fail_count", + "new_fail_muted_count", + "new_pass_count", + "new_pass_muted_count", + "new_manual_count", + "new_manual_muted_count", + "changed_fail_count", + "changed_fail_muted_count", + "changed_pass_count", + "changed_pass_muted_count", + "changed_manual_count", + "changed_manual_muted_count", + ], + ) + def test_finding_groups_sort_by_counter_fields( + self, + authenticated_client, + finding_groups_fixture, + endpoint_name, + sort_field, + ): + """All counter fields are accepted as sort parameters (asc and desc).""" + params = {"sort": f"-{sort_field}"} + if endpoint_name == "finding-group-list": + params["filter[inserted_at]"] = TODAY + + response = authenticated_client.get(reverse(endpoint_name), params) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) > 0 + + desc_values = [item["attributes"][sort_field] for item in data] + assert desc_values == sorted(desc_values, reverse=True) + + params["sort"] = sort_field + response = authenticated_client.get(reverse(endpoint_name), params) + assert response.status_code == status.HTTP_200_OK + asc_values = [ + item["attributes"][sort_field] for item in response.json()["data"] + ] + assert asc_values == sorted(asc_values) + + @pytest.mark.parametrize( + "endpoint_name", ["finding-group-list", "finding-group-latest"] + ) + def test_finding_groups_delta_status_breakdown( + self, authenticated_client, finding_groups_fixture, endpoint_name + ): + """`new_*` and `changed_*` counters split by status and mute state. + + s3_bucket_public_access has 1 new FAIL and 1 changed FAIL (both + non-muted) so the breakdown must reflect exactly that and the totals + must equal the sum of the buckets. + """ + params = {"filter[check_id]": "s3_bucket_public_access"} + if endpoint_name == "finding-group-list": + params["filter[inserted_at]"] = TODAY + + response = authenticated_client.get(reverse(endpoint_name), params) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) == 1 + attrs = data[0]["attributes"] + + assert attrs["new_fail_count"] == 1 + assert attrs["new_fail_muted_count"] == 0 + assert attrs["new_pass_count"] == 0 + assert attrs["new_pass_muted_count"] == 0 + assert attrs["new_manual_count"] == 0 + assert attrs["new_manual_muted_count"] == 0 + assert attrs["changed_fail_count"] == 1 + assert attrs["changed_fail_muted_count"] == 0 + assert attrs["changed_pass_count"] == 0 + assert attrs["changed_pass_muted_count"] == 0 + assert attrs["changed_manual_count"] == 0 + assert attrs["changed_manual_muted_count"] == 0 + + new_total = ( + attrs["new_fail_count"] + + attrs["new_fail_muted_count"] + + attrs["new_pass_count"] + + attrs["new_pass_muted_count"] + + attrs["new_manual_count"] + + attrs["new_manual_muted_count"] + ) + changed_total = ( + attrs["changed_fail_count"] + + attrs["changed_fail_muted_count"] + + attrs["changed_pass_count"] + + attrs["changed_pass_muted_count"] + + attrs["changed_manual_count"] + + attrs["changed_manual_muted_count"] + ) + # The non-muted variants of the breakdown must sum to the legacy + # totals (new_count/changed_count are stored as non-muted). + assert ( + attrs["new_fail_count"] + + attrs["new_pass_count"] + + attrs["new_manual_count"] + == attrs["new_count"] + ) + assert ( + attrs["changed_fail_count"] + + attrs["changed_pass_count"] + + attrs["changed_manual_count"] + == attrs["changed_count"] + ) + # And the *full* breakdown (including the muted halves) is exposed + # so clients can also count muted-only deltas without losing data. + assert new_total >= attrs["new_count"] + assert changed_total >= attrs["changed_count"] + + def test_finding_groups_resources_serializer_exposes_muted( + self, authenticated_client, finding_groups_fixture + ): + """The /finding-groups//resources payload must expose `muted`.""" + response = authenticated_client.get( + reverse( + "finding-group-resources", + kwargs={"pk": "rds_encryption"}, + ), + {"filter[inserted_at]": TODAY}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert data, "rds_encryption should expose its resources" + for item in data: + attrs = item["attributes"] + assert "muted" in attrs and isinstance(attrs["muted"], bool) + # rds_encryption has all muted findings + assert attrs["muted"] is True + # Status reflects the underlying check outcome (FAIL), not MUTED + assert attrs["status"] == "FAIL" + + def test_finding_groups_resources_exposes_finding_id( + self, authenticated_client, finding_groups_fixture + ): + """The /resources payload exposes the most recent matching finding_id. + + rds_encryption has 2 findings, one per resource. Each resource row must + report the UUID of its corresponding Finding (UUIDv7 ordering means + Max(finding__id) resolves to the latest snapshot in time). + """ + response = authenticated_client.get( + reverse( + "finding-group-resources", + kwargs={"pk": "rds_encryption"}, + ), + {"filter[inserted_at]": TODAY}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert data, "rds_encryption should expose its resources" + + rds_finding_ids = { + str(f.id) for f in finding_groups_fixture if f.check_id == "rds_encryption" + } + assert rds_finding_ids, "fixture sanity" + + for item in data: + attrs = item["attributes"] + assert "finding_id" in attrs + assert attrs["finding_id"] in rds_finding_ids + + def test_finding_groups_latest_resources_exposes_finding_id( + self, authenticated_client, finding_groups_fixture + ): + """The /latest/.../resources payload also exposes finding_id.""" + response = authenticated_client.get( + reverse( + "finding-group-latest_resources", + kwargs={"check_id": "rds_encryption"}, + ), + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert data, "rds_encryption should expose its resources via /latest" + + rds_finding_ids = { + str(f.id) for f in finding_groups_fixture if f.check_id == "rds_encryption" + } + for item in data: + attrs = item["attributes"] + assert "finding_id" in attrs + assert attrs["finding_id"] in rds_finding_ids diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 6af4d01000..52ec47f4f5 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -4185,6 +4185,7 @@ class FindingGroupSerializer(BaseSerializerV1): check_description = serializers.CharField(required=False, allow_null=True) severity = serializers.CharField() status = serializers.CharField() + muted = serializers.BooleanField() impacted_providers = serializers.ListField( child=serializers.CharField(), required=False ) @@ -4192,9 +4193,25 @@ class FindingGroupSerializer(BaseSerializerV1): resources_total = serializers.IntegerField() pass_count = serializers.IntegerField() fail_count = serializers.IntegerField() + manual_count = serializers.IntegerField() + pass_muted_count = serializers.IntegerField() + fail_muted_count = serializers.IntegerField() + manual_muted_count = serializers.IntegerField() muted_count = serializers.IntegerField() new_count = serializers.IntegerField() changed_count = serializers.IntegerField() + new_fail_count = serializers.IntegerField() + new_fail_muted_count = serializers.IntegerField() + new_pass_count = serializers.IntegerField() + new_pass_muted_count = serializers.IntegerField() + new_manual_count = serializers.IntegerField() + new_manual_muted_count = serializers.IntegerField() + changed_fail_count = serializers.IntegerField() + changed_fail_muted_count = serializers.IntegerField() + changed_pass_count = serializers.IntegerField() + changed_pass_muted_count = serializers.IntegerField() + changed_manual_count = serializers.IntegerField() + changed_manual_muted_count = serializers.IntegerField() first_seen_at = serializers.DateTimeField(required=False, allow_null=True) last_seen_at = serializers.DateTimeField(required=False, allow_null=True) failing_since = serializers.DateTimeField(required=False, allow_null=True) @@ -4214,8 +4231,10 @@ class FindingGroupResourceSerializer(BaseSerializerV1): id = serializers.UUIDField(source="resource_id") resource = serializers.SerializerMethodField() provider = serializers.SerializerMethodField() + finding_id = serializers.UUIDField() status = serializers.CharField() severity = serializers.CharField() + muted = serializers.BooleanField() delta = serializers.CharField(required=False, allow_null=True) first_seen_at = serializers.DateTimeField(required=False, allow_null=True) last_seen_at = serializers.DateTimeField(required=False, allow_null=True) diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 2392b818ac..0d085349cc 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -26,10 +26,11 @@ from config.settings.social_login import ( ) from dj_rest_auth.registration.views import SocialLoginView from django.conf import settings as django_settings -from django.contrib.postgres.aggregates import ArrayAgg, StringAgg +from django.contrib.postgres.aggregates import ArrayAgg, BoolAnd, StringAgg from django.contrib.postgres.search import SearchQuery from django.db import transaction from django.db.models import ( + BooleanField, Case, CharField, Count, @@ -414,7 +415,7 @@ class SchemaView(SpectacularAPIView): def get(self, request, *args, **kwargs): spectacular_settings.TITLE = "Prowler API" - spectacular_settings.VERSION = "1.24.0" + spectacular_settings.VERSION = "1.25.0" spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) @@ -7076,9 +7077,29 @@ class FindingGroupViewSet(BaseRLSViewSet): severity_order=Max("severity_order"), pass_count=Sum("pass_count"), fail_count=Sum("fail_count"), + manual_count=Sum("manual_count"), + pass_muted_count=Sum("pass_muted_count"), + fail_muted_count=Sum("fail_muted_count"), + manual_muted_count=Sum("manual_muted_count"), muted_count=Sum("muted_count"), + # The group is muted only if every contributing daily summary is + # itself fully muted. BoolAnd returns False as soon as one row has + # at least one actionable finding. + muted=BoolAnd("muted"), new_count=Sum("new_count"), changed_count=Sum("changed_count"), + new_fail_count=Sum("new_fail_count"), + new_fail_muted_count=Sum("new_fail_muted_count"), + new_pass_count=Sum("new_pass_count"), + new_pass_muted_count=Sum("new_pass_muted_count"), + new_manual_count=Sum("new_manual_count"), + new_manual_muted_count=Sum("new_manual_muted_count"), + changed_fail_count=Sum("changed_fail_count"), + changed_fail_muted_count=Sum("changed_fail_muted_count"), + changed_pass_count=Sum("changed_pass_count"), + changed_pass_muted_count=Sum("changed_pass_muted_count"), + changed_manual_count=Sum("changed_manual_count"), + changed_manual_muted_count=Sum("changed_manual_muted_count"), resources_total=Sum("resources_total"), resources_fail=Sum("resources_fail"), impacted_providers_str=StringAgg( @@ -7104,39 +7125,95 @@ class FindingGroupViewSet(BaseRLSViewSet): output_field=IntegerField(), ) - return queryset.values("check_id").annotate( - severity_order=Max(severity_case), - pass_count=Count("id", filter=Q(status="PASS", muted=False)), - fail_count=Count("id", filter=Q(status="FAIL", muted=False)), - muted_count=Count("id", filter=Q(muted=True)), - new_count=Count("id", filter=Q(delta="new", muted=False)), - changed_count=Count("id", filter=Q(delta="changed", muted=False)), - resources_total=Count("resources__id", distinct=True), - resources_fail=Count( - "resources__id", - distinct=True, - filter=Q(status="FAIL", muted=False), - ), - impacted_providers_str=StringAgg( - Cast("scan__provider__provider", CharField()), - delimiter=",", - distinct=True, - default="", - ), - agg_first_seen_at=Min("first_seen_at"), - agg_last_seen_at=Max("inserted_at"), - agg_failing_since=Min( - "first_seen_at", filter=Q(status="FAIL", muted=False) - ), - check_title=Coalesce( - Max(KeyTextTransform("checktitle", "check_metadata")), - Max(KeyTextTransform("CheckTitle", "check_metadata")), - Max(KeyTextTransform("Checktitle", "check_metadata")), - ), - check_description=Coalesce( - Max(KeyTextTransform("description", "check_metadata")), - Max(KeyTextTransform("Description", "check_metadata")), - ), + # `pass_count`, `fail_count` and `manual_count` count *every* finding + # for the check (muted or not) so the aggregated `status` reflects the + # underlying check outcome regardless of mute state. Whether the group + # is actionable is signalled by the orthogonal `muted` flag below. + return ( + queryset.values("check_id") + .annotate( + severity_order=Max(severity_case), + pass_count=Count("id", filter=Q(status="PASS")), + fail_count=Count("id", filter=Q(status="FAIL")), + manual_count=Count("id", filter=Q(status="MANUAL")), + pass_muted_count=Count("id", filter=Q(status="PASS", muted=True)), + fail_muted_count=Count("id", filter=Q(status="FAIL", muted=True)), + manual_muted_count=Count("id", filter=Q(status="MANUAL", muted=True)), + muted_count=Count("id", filter=Q(muted=True)), + nonmuted_count=Count("id", filter=Q(muted=False)), + new_count=Count("id", filter=Q(delta="new", muted=False)), + changed_count=Count("id", filter=Q(delta="changed", muted=False)), + new_fail_count=Count( + "id", filter=Q(delta="new", status="FAIL", muted=False) + ), + new_fail_muted_count=Count( + "id", filter=Q(delta="new", status="FAIL", muted=True) + ), + new_pass_count=Count( + "id", filter=Q(delta="new", status="PASS", muted=False) + ), + new_pass_muted_count=Count( + "id", filter=Q(delta="new", status="PASS", muted=True) + ), + new_manual_count=Count( + "id", filter=Q(delta="new", status="MANUAL", muted=False) + ), + new_manual_muted_count=Count( + "id", filter=Q(delta="new", status="MANUAL", muted=True) + ), + changed_fail_count=Count( + "id", filter=Q(delta="changed", status="FAIL", muted=False) + ), + changed_fail_muted_count=Count( + "id", filter=Q(delta="changed", status="FAIL", muted=True) + ), + changed_pass_count=Count( + "id", filter=Q(delta="changed", status="PASS", muted=False) + ), + changed_pass_muted_count=Count( + "id", filter=Q(delta="changed", status="PASS", muted=True) + ), + changed_manual_count=Count( + "id", filter=Q(delta="changed", status="MANUAL", muted=False) + ), + changed_manual_muted_count=Count( + "id", filter=Q(delta="changed", status="MANUAL", muted=True) + ), + resources_total=Count("resources__id", distinct=True), + resources_fail=Count( + "resources__id", + distinct=True, + filter=Q(status="FAIL", muted=False), + ), + impacted_providers_str=StringAgg( + Cast("scan__provider__provider", CharField()), + delimiter=",", + distinct=True, + default="", + ), + agg_first_seen_at=Min("first_seen_at"), + agg_last_seen_at=Max("inserted_at"), + agg_failing_since=Min( + "first_seen_at", filter=Q(status="FAIL", muted=False) + ), + check_title=Coalesce( + Max(KeyTextTransform("checktitle", "check_metadata")), + Max(KeyTextTransform("CheckTitle", "check_metadata")), + Max(KeyTextTransform("Checktitle", "check_metadata")), + ), + check_description=Coalesce( + Max(KeyTextTransform("description", "check_metadata")), + Max(KeyTextTransform("Description", "check_metadata")), + ), + ) + .annotate( + # Group is muted only if it has zero non-muted findings. + muted=Case( + When(nonmuted_count=0, then=Value(True)), + default=Value(False), + output_field=BooleanField(), + ), + ) ) def _split_computed_aggregate_filters( @@ -7148,6 +7225,7 @@ class FindingGroupViewSet(BaseRLSViewSet): "status__in", "severity", "severity__in", + "muted", "include_muted", } finding_params = QueryDict(mutable=True) @@ -7179,7 +7257,8 @@ class FindingGroupViewSet(BaseRLSViewSet): Post-process aggregation results to add computed fields. - Converts severity integer back to string - - Computes aggregated status (FAIL > PASS > MUTED) + - Computes aggregated status (FAIL > PASS > MANUAL); the orthogonal + ``muted`` boolean is already on the row from the SQL aggregation - Converts provider string to list """ results = [] @@ -7197,13 +7276,19 @@ class FindingGroupViewSet(BaseRLSViewSet): if "agg_failing_since" in row: row["failing_since"] = row.pop("agg_failing_since") - # Compute aggregated status + # Drop the helper count we use to derive `muted` in the + # finding-level aggregation path. + row.pop("nonmuted_count", None) + + # Compute aggregated status. Counts are inclusive of muted findings, + # so the underlying check outcome surfaces even when the group is + # fully muted. if row.get("fail_count", 0) > 0: row["status"] = "FAIL" elif row.get("pass_count", 0) > 0: row["status"] = "PASS" else: - row["status"] = "MUTED" + row["status"] = "MANUAL" # Convert provider string to list providers_str = row.pop("impacted_providers_str", "") or "" @@ -7219,12 +7304,30 @@ class FindingGroupViewSet(BaseRLSViewSet): "check_id": "check_id", "check_title": "check_title", "severity": "severity_order", + "status": "status_order", + "muted": "muted", "delta": "delta_order", "fail_count": "fail_count", "pass_count": "pass_count", + "manual_count": "manual_count", "muted_count": "muted_count", + "pass_muted_count": "pass_muted_count", + "fail_muted_count": "fail_muted_count", + "manual_muted_count": "manual_muted_count", "new_count": "new_count", + "new_fail_count": "new_fail_count", + "new_fail_muted_count": "new_fail_muted_count", + "new_pass_count": "new_pass_count", + "new_pass_muted_count": "new_pass_muted_count", + "new_manual_count": "new_manual_count", + "new_manual_muted_count": "new_manual_muted_count", "changed_count": "changed_count", + "changed_fail_count": "changed_fail_count", + "changed_fail_muted_count": "changed_fail_muted_count", + "changed_pass_count": "changed_pass_count", + "changed_pass_muted_count": "changed_pass_muted_count", + "changed_manual_count": "changed_manual_count", + "changed_manual_muted_count": "changed_manual_muted_count", "resources_total": "resources_total", "resources_fail": "resources_fail", "first_seen_at": "agg_first_seen_at", @@ -7276,7 +7379,7 @@ class FindingGroupViewSet(BaseRLSViewSet): return ordering def _apply_aggregated_computed_filters(self, queryset, computed_params: QueryDict): - """Apply computed filters (status/severity) on aggregated finding-group rows.""" + """Apply computed filters (status/severity/muted) on aggregated finding-group rows.""" if not computed_params: return queryset @@ -7285,14 +7388,16 @@ class FindingGroupViewSet(BaseRLSViewSet): aggregated_status=Case( When(fail_count__gt=0, then=Value("FAIL")), When(pass_count__gt=0, then=Value("PASS")), - default=Value("MUTED"), + default=Value("MANUAL"), output_field=CharField(), ) ) - # Exclude fully-muted groups by default unless include_muted is set - if "include_muted" not in computed_params: - queryset = queryset.exclude(fail_count=0, pass_count=0, muted_count__gt=0) + # Exclude fully-muted groups by default unless the caller has opted in + # via either `include_muted` or an explicit `muted` filter (the latter + # gives the caller direct control over the column). + if "include_muted" not in computed_params and "muted" not in computed_params: + queryset = queryset.exclude(muted=True) filterset = FindingGroupAggregatedComputedFilter( computed_params, queryset=queryset @@ -7347,18 +7452,14 @@ class FindingGroupViewSet(BaseRLSViewSet): provider_type=Max("resource__provider__provider"), provider_uid=Max("resource__provider__uid"), provider_alias=Max("resource__provider__alias"), + # status_order considers ALL findings (muted or not) so it + # surfaces FAIL/PASS/MANUAL based on the underlying check + # outcome. Whether the resource is actionable is signalled by + # the orthogonal `muted` flag below. status_order=Max( Case( - When( - finding__status="FAIL", - finding__muted=False, - then=Value(3), - ), - When( - finding__status="PASS", - finding__muted=False, - then=Value(2), - ), + When(finding__status="FAIL", then=Value(3)), + When(finding__status="PASS", then=Value(2)), default=Value(1), output_field=IntegerField(), ) @@ -7390,6 +7491,8 @@ class FindingGroupViewSet(BaseRLSViewSet): ), first_seen_at=Min("finding__first_seen_at"), last_seen_at=Max("finding__inserted_at"), + # True only if every finding for this resource+check is muted. + muted=BoolAnd("finding__muted"), # Max() on muted_reason / check_metadata is safe because # all findings for the same resource+check share identical # values (mute rules and metadata are applied per-check). @@ -7397,6 +7500,12 @@ class FindingGroupViewSet(BaseRLSViewSet): resource_group=Max( KeyTextTransform("resourcegroup", "finding__check_metadata") ), + # Most recent matching Finding for this (resource, check): + # Finding.id is a UUIDv7 (time-ordered in its high 48 bits). + # Cast to text first because PostgreSQL has no built-in + # `max(uuid)` aggregate; on the canonical lowercase form a + # lexicographic Max() still resolves to the latest snapshot. + finding_id=Max(Cast("finding__id", output_field=CharField())), ) .filter(resource_id__isnull=False) ) @@ -7405,8 +7514,8 @@ class FindingGroupViewSet(BaseRLSViewSet): _RESOURCE_SORT_ANNOTATIONS = { "status_order": lambda: Max( Case( - When(finding__status="FAIL", finding__muted=False, then=Value(3)), - When(finding__status="PASS", finding__muted=False, then=Value(2)), + When(finding__status="FAIL", then=Value(3)), + When(finding__status="PASS", then=Value(2)), default=Value(1), output_field=IntegerField(), ) @@ -7480,7 +7589,7 @@ class FindingGroupViewSet(BaseRLSViewSet): elif status_order == 2: status = "PASS" else: - status = "MUTED" + status = "MANUAL" delta_order = row.get("delta_order", 0) if delta_order == 2: @@ -7508,8 +7617,12 @@ class FindingGroupViewSet(BaseRLSViewSet): "delta": delta, "first_seen_at": row["first_seen_at"], "last_seen_at": row["last_seen_at"], + "muted": bool(row.get("muted", False)), "muted_reason": row.get("muted_reason"), "resource_group": row.get("resource_group", ""), + "finding_id": ( + str(row["finding_id"]) if row.get("finding_id") else None + ), } ) @@ -7570,11 +7683,21 @@ class FindingGroupViewSet(BaseRLSViewSet): sort_param, self._FINDING_GROUP_SORT_MAP ) if ordering: - # delta_order is a virtual sort field: expand it to a - # lexicographic ordering by (new_count, changed_count) so groups - # with more new findings rank higher, with changed_count as the - # tie-breaker (preserves the "new > changed" priority used by - # the resources endpoint, but driven by the actual counters). + # status_order is annotated on demand so groups can be sorted by + # their aggregated status (FAIL > PASS > MANUAL), mirroring the + # priority used in _post_process_aggregation. Counts are + # inclusive of muted findings, so the underlying check outcome + # surfaces even for fully muted groups. + if any(field.lstrip("-") == "status_order" for field in ordering): + aggregated_queryset = aggregated_queryset.annotate( + status_order=Case( + When(fail_count__gt=0, then=Value(3)), + When(pass_count__gt=0, then=Value(2)), + default=Value(1), + output_field=IntegerField(), + ) + ) + expanded_ordering = [] for field in ordering: if field.lstrip("-") == "delta_order": diff --git a/api/src/backend/config/guniconf.py b/api/src/backend/config/guniconf.py index a5b625874b..536fd97abb 100644 --- a/api/src/backend/config/guniconf.py +++ b/api/src/backend/config/guniconf.py @@ -15,7 +15,7 @@ from config.django.production import LOGGING as DJANGO_LOGGERS, DEBUG # noqa: E from config.custom_logging import BackendLogger # noqa: E402 BIND_ADDRESS = env("DJANGO_BIND_ADDRESS", default="127.0.0.1") -PORT = env("DJANGO_PORT", default=8000) +PORT = env("DJANGO_PORT", default=8080) # Server settings bind = f"{BIND_ADDRESS}:{PORT}" diff --git a/api/src/backend/tasks/jobs/export.py b/api/src/backend/tasks/jobs/export.py index 4b8498f7e7..3be9b81544 100644 --- a/api/src/backend/tasks/jobs/export.py +++ b/api/src/backend/tasks/jobs/export.py @@ -32,9 +32,13 @@ from prowler.lib.outputs.compliance.cis.cis_aws import AWSCIS from prowler.lib.outputs.compliance.cis.cis_azure import AzureCIS from prowler.lib.outputs.compliance.cis.cis_gcp import GCPCIS from prowler.lib.outputs.compliance.cis.cis_github import GithubCIS +from prowler.lib.outputs.compliance.cis.cis_googleworkspace import GoogleWorkspaceCIS from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS +from prowler.lib.outputs.compliance.cisa_scuba.cisa_scuba_googleworkspace import ( + GoogleWorkspaceCISASCuBA, +) from prowler.lib.outputs.compliance.csa.csa_alibabacloud import AlibabaCloudCSA from prowler.lib.outputs.compliance.csa.csa_aws import AWSCSA from prowler.lib.outputs.compliance.csa.csa_azure import AzureCSA @@ -93,7 +97,7 @@ COMPLIANCE_CLASS_MAP = { (lambda name: name.startswith("iso27001_"), AWSISO27001), (lambda name: name.startswith("kisa"), AWSKISAISMSP), (lambda name: name == "prowler_threatscore_aws", ProwlerThreatScoreAWS), - (lambda name: name == "ccc_aws", CCC_AWS), + (lambda name: name.startswith("ccc_"), CCC_AWS), (lambda name: name.startswith("c5_"), AWSC5), (lambda name: name.startswith("csa_"), AWSCSA), ], @@ -102,7 +106,7 @@ COMPLIANCE_CLASS_MAP = { (lambda name: name == "mitre_attack_azure", AzureMitreAttack), (lambda name: name.startswith("ens_"), AzureENS), (lambda name: name.startswith("iso27001_"), AzureISO27001), - (lambda name: name == "ccc_azure", CCC_Azure), + (lambda name: name.startswith("ccc_"), CCC_Azure), (lambda name: name == "prowler_threatscore_azure", ProwlerThreatScoreAzure), (lambda name: name == "c5_azure", AzureC5), (lambda name: name.startswith("csa_"), AzureCSA), @@ -113,7 +117,7 @@ COMPLIANCE_CLASS_MAP = { (lambda name: name.startswith("ens_"), GCPENS), (lambda name: name.startswith("iso27001_"), GCPISO27001), (lambda name: name == "prowler_threatscore_gcp", ProwlerThreatScoreGCP), - (lambda name: name == "ccc_gcp", CCC_GCP), + (lambda name: name.startswith("ccc_"), CCC_GCP), (lambda name: name == "c5_gcp", GCPC5), (lambda name: name.startswith("csa_"), GCPCSA), ], @@ -133,6 +137,10 @@ COMPLIANCE_CLASS_MAP = { "github": [ (lambda name: name.startswith("cis_"), GithubCIS), ], + "googleworkspace": [ + (lambda name: name.startswith("cis_"), GoogleWorkspaceCIS), + (lambda name: name.startswith("cisa_scuba_"), GoogleWorkspaceCISASCuBA), + ], "iac": [ # IaC provider doesn't have specific compliance frameworks yet # Trivy handles its own compliance checks diff --git a/api/src/backend/tasks/jobs/scan.py b/api/src/backend/tasks/jobs/scan.py index 364b12d146..2c73c97f2f 100644 --- a/api/src/backend/tasks/jobs/scan.py +++ b/api/src/backend/tasks/jobs/scan.py @@ -1803,7 +1803,12 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str): output_field=IntegerField(), ) - # Aggregate findings by check_id for this scan + # Aggregate findings by check_id for this scan. + # `pass_count`, `fail_count` and `manual_count` count *every* finding + # in this group, regardless of mute state, so the aggregated `status` + # always reflects the underlying check outcome (FAIL > PASS > MANUAL) + # even when the group is fully muted. The orthogonal `muted` flag is + # what tells whether the group has any actionable (non-muted) findings. aggregated = ( Finding.objects.filter( tenant_id=tenant_id, @@ -1812,11 +1817,52 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str): .values("check_id") .annotate( severity_order=Max(severity_case), - pass_count=Count("id", filter=Q(status="PASS", muted=False)), - fail_count=Count("id", filter=Q(status="FAIL", muted=False)), + pass_count=Count("id", filter=Q(status="PASS")), + fail_count=Count("id", filter=Q(status="FAIL")), + manual_count=Count("id", filter=Q(status="MANUAL")), + pass_muted_count=Count("id", filter=Q(status="PASS", muted=True)), + fail_muted_count=Count("id", filter=Q(status="FAIL", muted=True)), + manual_muted_count=Count("id", filter=Q(status="MANUAL", muted=True)), muted_count=Count("id", filter=Q(muted=True)), + nonmuted_count=Count("id", filter=Q(muted=False)), new_count=Count("id", filter=Q(delta="new", muted=False)), changed_count=Count("id", filter=Q(delta="changed", muted=False)), + new_fail_count=Count( + "id", filter=Q(delta="new", status="FAIL", muted=False) + ), + new_fail_muted_count=Count( + "id", filter=Q(delta="new", status="FAIL", muted=True) + ), + new_pass_count=Count( + "id", filter=Q(delta="new", status="PASS", muted=False) + ), + new_pass_muted_count=Count( + "id", filter=Q(delta="new", status="PASS", muted=True) + ), + new_manual_count=Count( + "id", filter=Q(delta="new", status="MANUAL", muted=False) + ), + new_manual_muted_count=Count( + "id", filter=Q(delta="new", status="MANUAL", muted=True) + ), + changed_fail_count=Count( + "id", filter=Q(delta="changed", status="FAIL", muted=False) + ), + changed_fail_muted_count=Count( + "id", filter=Q(delta="changed", status="FAIL", muted=True) + ), + changed_pass_count=Count( + "id", filter=Q(delta="changed", status="PASS", muted=False) + ), + changed_pass_muted_count=Count( + "id", filter=Q(delta="changed", status="PASS", muted=True) + ), + changed_manual_count=Count( + "id", filter=Q(delta="changed", status="MANUAL", muted=False) + ), + changed_manual_muted_count=Count( + "id", filter=Q(delta="changed", status="MANUAL", muted=True) + ), resources_total=Count("resources__id", distinct=True), resources_fail=Count( "resources__id", @@ -1895,9 +1941,26 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str): severity_order=row["severity_order"] or 1, pass_count=row["pass_count"], fail_count=row["fail_count"], + manual_count=row["manual_count"], + pass_muted_count=row["pass_muted_count"], + fail_muted_count=row["fail_muted_count"], + manual_muted_count=row["manual_muted_count"], muted_count=row["muted_count"], + muted=row["nonmuted_count"] == 0, new_count=row["new_count"], changed_count=row["changed_count"], + new_fail_count=row["new_fail_count"], + new_fail_muted_count=row["new_fail_muted_count"], + new_pass_count=row["new_pass_count"], + new_pass_muted_count=row["new_pass_muted_count"], + new_manual_count=row["new_manual_count"], + new_manual_muted_count=row["new_manual_muted_count"], + changed_fail_count=row["changed_fail_count"], + changed_fail_muted_count=row["changed_fail_muted_count"], + changed_pass_count=row["changed_pass_count"], + changed_pass_muted_count=row["changed_pass_muted_count"], + changed_manual_count=row["changed_manual_count"], + changed_manual_muted_count=row["changed_manual_muted_count"], resources_total=row["resources_total"], resources_fail=row["resources_fail"], first_seen_at=row["agg_first_seen_at"], @@ -1917,9 +1980,26 @@ def aggregate_finding_group_summaries(tenant_id: str, scan_id: str): "severity_order", "pass_count", "fail_count", + "manual_count", + "pass_muted_count", + "fail_muted_count", + "manual_muted_count", "muted_count", + "muted", "new_count", "changed_count", + "new_fail_count", + "new_fail_muted_count", + "new_pass_count", + "new_pass_muted_count", + "new_manual_count", + "new_manual_muted_count", + "changed_fail_count", + "changed_fail_muted_count", + "changed_pass_count", + "changed_pass_muted_count", + "changed_manual_count", + "changed_manual_muted_count", "resources_total", "resources_fail", "first_seen_at", diff --git a/api/src/backend/tasks/tasks.py b/api/src/backend/tasks/tasks.py index fbd0440af8..bbf4d89772 100644 --- a/api/src/backend/tasks/tasks.py +++ b/api/src/backend/tasks/tasks.py @@ -771,26 +771,49 @@ def aggregate_finding_group_summaries_task(tenant_id: str, scan_id: str): ) @set_tenant(keep_tenant=True) def reaggregate_all_finding_group_summaries_task(tenant_id: str): - """Reaggregate finding group summaries for all providers' latest completed scans.""" - latest_scan_ids = list( - Scan.objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED) - .order_by("provider_id", "-completed_at", "-inserted_at") - .distinct("provider_id") - .values_list("id", flat=True) + """Reaggregate finding group summaries for every (provider, day) combination. + + Mirrors the unbounded scope of `mute_historical_findings_task`: that task + rewrites every Finding row whose UID matches a mute rule, with no time + limit. To keep the daily summaries consistent with that update, this task + re-runs the aggregator on the latest completed scan of every (provider, + day) pair that exists in the database. Tasks are dispatched in parallel + via a Celery group so the wallclock scales with the worker pool, not with + the number of pairs. + """ + completed_scans = list( + Scan.objects.filter( + tenant_id=tenant_id, + state=StateChoices.COMPLETED, + completed_at__isnull=False, + ) + .order_by("-completed_at") + .values("id", "completed_at", "provider_id") ) - if latest_scan_ids: + + # Keep the latest scan per (provider, day) pair so the daily summary row + # the aggregator writes is the most recent snapshot of that day for that + # provider. Iterating from most recent to oldest means the first scan we + # see for a given key wins. + latest_scans: dict[tuple, str] = {} + for scan in completed_scans: + key = (scan["provider_id"], scan["completed_at"].date()) + if key not in latest_scans: + latest_scans[key] = str(scan["id"]) + + scan_ids = list(latest_scans.values()) + if scan_ids: logger.info( - "Reaggregating finding group summaries for %d scans: %s", - len(latest_scan_ids), - latest_scan_ids, + "Reaggregating finding group summaries for %d scans (provider x day)", + len(scan_ids), ) group( aggregate_finding_group_summaries_task.si( - tenant_id=tenant_id, scan_id=str(scan_id) + tenant_id=tenant_id, scan_id=scan_id ) - for scan_id in latest_scan_ids + for scan_id in scan_ids ).apply_async() - return {"scans_reaggregated": len(latest_scan_ids)} + return {"scans_reaggregated": len(scan_ids)} @shared_task(base=RLSTask, name="lighthouse-connection-check") diff --git a/api/src/backend/tasks/tests/test_tasks.py b/api/src/backend/tasks/tests/test_tasks.py index 8469b7db09..4a4108607e 100644 --- a/api/src/backend/tasks/tests/test_tasks.py +++ b/api/src/backend/tasks/tests/test_tasks.py @@ -1,6 +1,6 @@ import uuid from contextlib import contextmanager -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock, patch import openai @@ -2362,35 +2362,96 @@ class TestReaggregateAllFindingGroupSummaries: @patch("tasks.tasks.group") @patch("tasks.tasks.aggregate_finding_group_summaries_task") @patch("tasks.tasks.Scan.objects.filter") - def test_dispatches_subtasks_for_each_provider( + def test_dispatches_subtasks_for_each_provider_per_day( self, mock_scan_filter, mock_agg_task, mock_group ): - scan_id_1 = uuid.uuid4() - scan_id_2 = uuid.uuid4() + provider_id_1 = uuid.uuid4() + provider_id_2 = uuid.uuid4() + scan_id_today_p1 = uuid.uuid4() + scan_id_yesterday_p1 = uuid.uuid4() + scan_id_today_p2 = uuid.uuid4() + today = datetime.now(tz=timezone.utc) + yesterday = today - timedelta(days=1) + mock_group_result = MagicMock() mock_group.side_effect = lambda gen: (list(gen), mock_group_result)[1] - mock_scan_filter.return_value.order_by.return_value.distinct.return_value.values_list.return_value = [ - scan_id_1, - scan_id_2, + mock_scan_filter.return_value.order_by.return_value.values.return_value = [ + { + "id": scan_id_today_p1, + "completed_at": today, + "provider_id": provider_id_1, + }, + { + "id": scan_id_today_p2, + "completed_at": today, + "provider_id": provider_id_2, + }, + { + "id": scan_id_yesterday_p1, + "completed_at": yesterday, + "provider_id": provider_id_1, + }, ] result = reaggregate_all_finding_group_summaries_task(tenant_id=self.tenant_id) - assert result == {"scans_reaggregated": 2} - assert mock_agg_task.si.call_count == 2 + assert result == {"scans_reaggregated": 3} + assert mock_agg_task.si.call_count == 3 mock_agg_task.si.assert_any_call( - tenant_id=self.tenant_id, scan_id=str(scan_id_1) + tenant_id=self.tenant_id, scan_id=str(scan_id_today_p1) ) mock_agg_task.si.assert_any_call( - tenant_id=self.tenant_id, scan_id=str(scan_id_2) + tenant_id=self.tenant_id, scan_id=str(scan_id_today_p2) + ) + mock_agg_task.si.assert_any_call( + tenant_id=self.tenant_id, scan_id=str(scan_id_yesterday_p1) + ) + mock_group_result.apply_async.assert_called_once() + + @patch("tasks.tasks.group") + @patch("tasks.tasks.aggregate_finding_group_summaries_task") + @patch("tasks.tasks.Scan.objects.filter") + def test_dedupes_scans_to_latest_per_provider_per_day( + self, mock_scan_filter, mock_agg_task, mock_group + ): + """When several scans run on the same day for the same provider, only + the latest one is dispatched (matching the daily summary unique key).""" + provider_id = uuid.uuid4() + latest_scan_today = uuid.uuid4() + earlier_scan_today = uuid.uuid4() + today_late = datetime.now(tz=timezone.utc) + today_early = today_late - timedelta(hours=4) + + mock_group_result = MagicMock() + mock_group.side_effect = lambda gen: (list(gen), mock_group_result)[1] + + # Returned ordered by `-completed_at`, so the most recent comes first. + mock_scan_filter.return_value.order_by.return_value.values.return_value = [ + { + "id": latest_scan_today, + "completed_at": today_late, + "provider_id": provider_id, + }, + { + "id": earlier_scan_today, + "completed_at": today_early, + "provider_id": provider_id, + }, + ] + + result = reaggregate_all_finding_group_summaries_task(tenant_id=self.tenant_id) + + assert result == {"scans_reaggregated": 1} + mock_agg_task.si.assert_called_once_with( + tenant_id=self.tenant_id, scan_id=str(latest_scan_today) ) mock_group_result.apply_async.assert_called_once() @patch("tasks.tasks.group") @patch("tasks.tasks.Scan.objects.filter") def test_no_completed_scans_skips_dispatch(self, mock_scan_filter, mock_group): - mock_scan_filter.return_value.order_by.return_value.distinct.return_value.values_list.return_value = [] + mock_scan_filter.return_value.order_by.return_value.values.return_value = [] result = reaggregate_all_finding_group_summaries_task(tenant_id=self.tenant_id) diff --git a/contrib/k8s/helm/prowler-app/templates/worker/deployment.yaml b/contrib/k8s/helm/prowler-app/templates/worker/deployment.yaml index 6c11a28f9b..88f034fc96 100644 --- a/contrib/k8s/helm/prowler-app/templates/worker/deployment.yaml +++ b/contrib/k8s/helm/prowler-app/templates/worker/deployment.yaml @@ -34,6 +34,10 @@ spec: securityContext: {{- toYaml . | nindent 8 }} {{- end }} + {{- with .Values.worker.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} containers: - name: worker {{- with .Values.worker.securityContext }} diff --git a/contrib/k8s/helm/prowler-app/templates/worker_beat/deployment.yaml b/contrib/k8s/helm/prowler-app/templates/worker_beat/deployment.yaml index 749ea946fd..c1ef9ebf0c 100644 --- a/contrib/k8s/helm/prowler-app/templates/worker_beat/deployment.yaml +++ b/contrib/k8s/helm/prowler-app/templates/worker_beat/deployment.yaml @@ -32,6 +32,10 @@ spec: securityContext: {{- toYaml . | nindent 8 }} {{- end }} + {{- with .Values.worker_beat.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} containers: - name: worker-beat {{- with .Values.worker_beat.securityContext }} diff --git a/contrib/wazuh/prowler-wrapper.py b/contrib/wazuh/prowler-wrapper.py index e7e7faa9af..b9b25a7ebf 100644 --- a/contrib/wazuh/prowler-wrapper.py +++ b/contrib/wazuh/prowler-wrapper.py @@ -220,7 +220,7 @@ def _send_prowler_results(prowler_results, _prowler_version, options): try: _debug("RESULT MSG --- {0}".format(_check_result), 2) _check_result = json.loads(TEMPLATE_CHECK.format(_check_result)) - except: + except Exception: _debug( "INVALID JSON --- {0}".format(TEMPLATE_CHECK.format(_check_result)), 1 ) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 554177bc5c..b95945e7cb 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -1,4 +1,11 @@ services: + api-dev-init: + image: busybox:1.37.0 + volumes: + - ./_data/api:/data + command: ["sh", "-c", "chown -R 1000:1000 /data"] + restart: "no" + api-dev: hostname: "prowler-api" image: prowler-api-dev @@ -21,12 +28,20 @@ services: - ./_data/api:/home/prowler/.config/prowler-api - outputs:/tmp/prowler_api_output depends_on: + api-dev-init: + condition: service_completed_successfully postgres: condition: service_healthy valkey: condition: service_healthy neo4j: condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:${DJANGO_PORT:-8080}/api/v1/ || exit 1"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 60s entrypoint: - "/home/prowler/docker-entrypoint.sh" - "dev" @@ -139,11 +154,7 @@ services: - ./api/docker-entrypoint.sh:/home/prowler/docker-entrypoint.sh - outputs:/tmp/prowler_api_output depends_on: - valkey: - condition: service_healthy - postgres: - condition: service_healthy - neo4j: + api-dev: condition: service_healthy ulimits: nofile: @@ -165,11 +176,7 @@ services: - path: ./.env required: false depends_on: - valkey: - condition: service_healthy - postgres: - condition: service_healthy - neo4j: + api-dev: condition: service_healthy ulimits: nofile: diff --git a/docker-compose.yml b/docker-compose.yml index 4112624dc2..5a4514546a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,13 @@ # docker compose -f docker-compose-dev.yml up # services: + api-init: + image: busybox:1.37.0 + volumes: + - ./_data/api:/data + command: ["sh", "-c", "chown -R 1000:1000 /data"] + restart: "no" + api: hostname: "prowler-api" image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable} @@ -17,12 +24,20 @@ services: - ./_data/api:/home/prowler/.config/prowler-api - output:/tmp/prowler_api_output depends_on: + api-init: + condition: service_completed_successfully postgres: condition: service_healthy valkey: condition: service_healthy neo4j: condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:${DJANGO_PORT:-8080}/api/v1/ || exit 1"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 60s entrypoint: - "/home/prowler/docker-entrypoint.sh" - "prod" @@ -114,9 +129,7 @@ services: volumes: - "output:/tmp/prowler_api_output" depends_on: - valkey: - condition: service_healthy - postgres: + api: condition: service_healthy ulimits: nofile: @@ -132,9 +145,7 @@ services: - path: ./.env required: false depends_on: - valkey: - condition: service_healthy - postgres: + api: condition: service_healthy ulimits: nofile: diff --git a/docs/developer-guide/introduction.mdx b/docs/developer-guide/introduction.mdx index 2a4aa3abe1..03b3bd27bd 100644 --- a/docs/developer-guide/introduction.mdx +++ b/docs/developer-guide/introduction.mdx @@ -118,18 +118,22 @@ In case you have any doubts, consult the [Poetry environment activation guide](h ### Pre-Commit Hooks -This repository uses Git pre-commit hooks managed by the [pre-commit](https://pre-commit.com/) tool, it is installed with `poetry install --with dev`. Next, run the following command in the root of this repository: +This repository uses Git pre-commit hooks managed by the [prek](https://prek.j178.dev/) tool, it is installed with `poetry install --with dev`. Next, run the following command in the root of this repository: ```shell -pre-commit install +prek install ``` Successful installation should produce the following output: ```shell -pre-commit installed at .git/hooks/pre-commit +prek installed at `.git/hooks/pre-commit` ``` + +If pre-commit hooks were previously installed, run `prek install --overwrite` to replace the existing hook. Otherwise, both tools will run on each commit. + + ### Code Quality and Security Checks Before merging pull requests, several automated checks and utilities ensure code security and updated dependencies: @@ -163,6 +167,8 @@ These resources help ensure that AI-assisted contributions maintain consistency All dependencies are listed in the `pyproject.toml` file. +The SDK keeps direct dependencies pinned to exact versions, while `poetry.lock` records the full resolved dependency tree and the artifact hashes for every package. Use `poetry install` from the lock file instead of ad-hoc `pip` installs when you need a reproducible environment. + For proper code documentation, refer to the following and follow the code documentation practices presented there: [Google Python Style Guide - Comments and Docstrings](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings). diff --git a/docs/docs.json b/docs/docs.json index 2e39c4beac..c76b7174ef 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -98,6 +98,7 @@ ] }, "user-guide/tutorials/prowler-app-rbac", + "user-guide/tutorials/prowler-app-multi-tenant", "user-guide/tutorials/prowler-app-api-keys", "user-guide/tutorials/prowler-app-import-findings", { diff --git a/docs/getting-started/installation/prowler-app.mdx b/docs/getting-started/installation/prowler-app.mdx index 681034885c..ea4029c234 100644 --- a/docs/getting-started/installation/prowler-app.mdx +++ b/docs/getting-started/installation/prowler-app.mdx @@ -121,8 +121,8 @@ To update the environment file: Edit the `.env` file and change version values: ```env -PROWLER_UI_VERSION="5.22.0" -PROWLER_API_VERSION="5.22.0" +PROWLER_UI_VERSION="5.23.0" +PROWLER_API_VERSION="5.23.0" ``` diff --git a/docs/images/providers/select-vercel-prowler-cloud.png b/docs/images/providers/select-vercel-prowler-cloud.png new file mode 100644 index 0000000000..b332103e1f Binary files /dev/null and b/docs/images/providers/select-vercel-prowler-cloud.png differ diff --git a/docs/images/providers/vercel-launch-scan.png b/docs/images/providers/vercel-launch-scan.png new file mode 100644 index 0000000000..4e7dd36a25 Binary files /dev/null and b/docs/images/providers/vercel-launch-scan.png differ diff --git a/docs/images/providers/vercel-team-id-form.png b/docs/images/providers/vercel-team-id-form.png new file mode 100644 index 0000000000..fad53fe017 Binary files /dev/null and b/docs/images/providers/vercel-team-id-form.png differ diff --git a/docs/images/providers/vercel-token-form.png b/docs/images/providers/vercel-token-form.png new file mode 100644 index 0000000000..991b9ddedc Binary files /dev/null and b/docs/images/providers/vercel-token-form.png differ diff --git a/docs/images/prowler-app/multi-tenant/create-organization-button.png b/docs/images/prowler-app/multi-tenant/create-organization-button.png new file mode 100644 index 0000000000..70675c334f Binary files /dev/null and b/docs/images/prowler-app/multi-tenant/create-organization-button.png differ diff --git a/docs/images/prowler-app/multi-tenant/create-organization-modal.png b/docs/images/prowler-app/multi-tenant/create-organization-modal.png new file mode 100644 index 0000000000..f0f932035c Binary files /dev/null and b/docs/images/prowler-app/multi-tenant/create-organization-modal.png differ diff --git a/docs/images/prowler-app/multi-tenant/delete-active-organization-modal.png b/docs/images/prowler-app/multi-tenant/delete-active-organization-modal.png new file mode 100644 index 0000000000..41f5231753 Binary files /dev/null and b/docs/images/prowler-app/multi-tenant/delete-active-organization-modal.png differ diff --git a/docs/images/prowler-app/multi-tenant/delete-organization-modal.png b/docs/images/prowler-app/multi-tenant/delete-organization-modal.png new file mode 100644 index 0000000000..dd06f937e9 Binary files /dev/null and b/docs/images/prowler-app/multi-tenant/delete-organization-modal.png differ diff --git a/docs/images/prowler-app/multi-tenant/edit-organization-modal.png b/docs/images/prowler-app/multi-tenant/edit-organization-modal.png new file mode 100644 index 0000000000..e0d28c727d Binary files /dev/null and b/docs/images/prowler-app/multi-tenant/edit-organization-modal.png differ diff --git a/docs/images/prowler-app/multi-tenant/organizations-card.png b/docs/images/prowler-app/multi-tenant/organizations-card.png new file mode 100644 index 0000000000..10ddb4b15b Binary files /dev/null and b/docs/images/prowler-app/multi-tenant/organizations-card.png differ diff --git a/docs/images/prowler-app/multi-tenant/sign-in-invitation.png b/docs/images/prowler-app/multi-tenant/sign-in-invitation.png new file mode 100644 index 0000000000..418216c09b Binary files /dev/null and b/docs/images/prowler-app/multi-tenant/sign-in-invitation.png differ diff --git a/docs/images/prowler-app/multi-tenant/switch-organization-modal.png b/docs/images/prowler-app/multi-tenant/switch-organization-modal.png new file mode 100644 index 0000000000..b9e2607a75 Binary files /dev/null and b/docs/images/prowler-app/multi-tenant/switch-organization-modal.png differ diff --git a/docs/introduction.mdx b/docs/introduction.mdx index 766f60e3c0..5747eeadb1 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -21,29 +21,57 @@ ## Supported Providers -The supported providers right now are: +Prowler supports a wide range of providers organized by category: -| 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 | -| [Alibaba Cloud](/user-guide/providers/alibabacloud/getting-started-alibabacloud) | Official | Accounts | UI, API, CLI | -| [Cloudflare](/user-guide/providers/cloudflare/getting-started-cloudflare) | Official | Accounts | 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 | -| [OpenStack](/user-guide/providers/openstack/getting-started-openstack) | Official | Projects | UI, API, CLI | -| [Vercel](/user-guide/providers/vercel/getting-started-vercel) | Official | Teams / Projects | CLI | -| [LLM](/user-guide/providers/llm/getting-started-llm) | Official | Models | CLI | -| [Image](/user-guide/providers/image/getting-started-image) | Official | Container Images | CLI, API | -| [Google Workspace](/user-guide/providers/googleworkspace/getting-started-googleworkspace) | Official | Domains | CLI | -| **NHN** | Unofficial | Tenants | CLI | +### Cloud Service Providers (Infrastructure) -For more information about the checks and compliance of each provider visit [Prowler Hub](https://hub.prowler.com). +| Provider | Support | Audit Scope/Entities | Interface | +| -------------------------------------------------------------------------------- | ---------- | ------------------------ | ------------ | +| [Alibaba Cloud](/user-guide/providers/alibabacloud/getting-started-alibabacloud) | Official | Accounts | UI, API, CLI | +| [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 | +| [Cloudflare](/user-guide/providers/cloudflare/getting-started-cloudflare) | Official | Accounts | UI, API, CLI | +| [Google Cloud](/user-guide/providers/gcp/getting-started-gcp) | Official | Projects | UI, API, CLI | +| **NHN** | Unofficial | Tenants | CLI | +| [OpenStack](/user-guide/providers/openstack/getting-started-openstack) | Official | Projects | UI, API, CLI | +| [Oracle Cloud](/user-guide/providers/oci/getting-started-oci) | Official | Tenancies / Compartments | UI, API, CLI | + +### Infrastructure as Code Providers + +| Provider | Support | Audit Scope/Entities | Interface | +| --------------------------------------------------------------------- | -------- | -------------------- | ------------ | +| [Infra as Code](/user-guide/providers/iac/getting-started-iac) | Official | Repositories | UI, API, CLI | + +### Software as a Service (SaaS) Providers + +| Provider | Support | Audit Scope/Entities | Interface | +| ----------------------------------------------------------------------------------------- | -------- | ---------------------------- | ------------ | +| [GitHub](/user-guide/providers/github/getting-started-github) | Official | Organizations / Repositories | UI, API, CLI | +| [Google Workspace](/user-guide/providers/googleworkspace/getting-started-googleworkspace) | Official | Domains | CLI | +| [LLM](/user-guide/providers/llm/getting-started-llm) | Official | Models | CLI | +| [M365](/user-guide/providers/microsoft365/getting-started-m365) | Official | Tenants | UI, API, CLI | +| [MongoDB Atlas](/user-guide/providers/mongodbatlas/getting-started-mongodbatlas) | Official | Organizations | UI, API, CLI | +| [Vercel](/user-guide/providers/vercel/getting-started-vercel) | Official | Teams / Projects | CLI | + +### Kubernetes + +| Provider | Support | Audit Scope/Entities | Interface | +| -------------------------------------------------------------------------- | -------- | -------------------- | ------------ | +| [Kubernetes](/user-guide/providers/kubernetes/getting-started-k8s) | Official | Clusters | UI, API, CLI | + +### Containers + +| Provider | Support | Audit Scope/Entities | Interface | +| ------------------------------------------------------------------- | -------- | -------------------- | --------- | +| [Image](/user-guide/providers/image/getting-started-image) | Official | Container Images / Registries | CLI, API | + +### Custom Providers (Prowler Cloud Enterprise Only) + +| Provider | Support | Audit Scope/Entities | Interface | +| -------------------- | -------- | -------------------- | --------- | +| VMware/Broadcom VCF | Official | Infrastructure | CLI | + +For more information about the checks and compliance of each provider, visit [Prowler Hub](https://hub.prowler.com). ## Where to go next? diff --git a/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx b/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx index 0a1d54b079..de8708d867 100644 --- a/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx +++ b/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx @@ -37,7 +37,7 @@ Before you begin, make sure you have: ![Get Account ID](/images/providers/alibaba-account-id.png) -### Step 2: Access Prowler Cloud or Prowler App +### Step 2: Access Prowler Cloud 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) 2. Go to "Configuration" > "Cloud Providers" diff --git a/docs/user-guide/providers/aws/getting-started-aws.mdx b/docs/user-guide/providers/aws/getting-started-aws.mdx index 7d003d9821..2c94700b15 100644 --- a/docs/user-guide/providers/aws/getting-started-aws.mdx +++ b/docs/user-guide/providers/aws/getting-started-aws.mdx @@ -2,7 +2,7 @@ title: 'Getting Started With AWS on Prowler' --- -## Prowler App +## Prowler Cloud @@ -16,7 +16,7 @@ title: 'Getting Started With AWS on Prowler' ![Account ID detail](/images/providers/aws-account-id.png) -### Step 2: Access Prowler Cloud or Prowler App +### Step 2: Access Prowler Cloud 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) 2. Go to "Configuration" > "Cloud Providers" diff --git a/docs/user-guide/providers/azure/getting-started-azure.mdx b/docs/user-guide/providers/azure/getting-started-azure.mdx index a5299c614b..456c226aab 100644 --- a/docs/user-guide/providers/azure/getting-started-azure.mdx +++ b/docs/user-guide/providers/azure/getting-started-azure.mdx @@ -2,7 +2,7 @@ title: 'Getting Started With Azure on Prowler' --- -## Prowler App +## Prowler Cloud > Walkthrough video onboarding an Azure Subscription using Service Principal. @@ -32,7 +32,7 @@ For detailed instructions on how to create the Service Principal and configure p --- -### Step 2: Access Prowler App +### Step 2: Access Prowler Cloud 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) 2. Navigate to `Configuration` > `Cloud Providers` @@ -51,7 +51,7 @@ For detailed instructions on how to create the Service Principal and configure p ![Add Subscription ID](/images/providers/add-subscription-id.png) -### Step 3: Add Credentials to Prowler App +### Step 3: Add Credentials to Prowler Cloud For Azure, Prowler App uses a service principal application to authenticate. For more information about the process of creating and adding permissions to a service principal refer to this [section](/user-guide/providers/azure/authentication). When you finish creating and adding the [Entra](/user-guide/providers/azure/create-prowler-service-principal#assigning-proper-permissions) and [Subscription](/user-guide/providers/azure/subscriptions) scope permissions to the service principal, enter the `Tenant ID`, `Client ID` and `Client Secret` of the service principal application. diff --git a/docs/user-guide/providers/gcp/getting-started-gcp.mdx b/docs/user-guide/providers/gcp/getting-started-gcp.mdx index 1cdc587d45..c70250c8a9 100644 --- a/docs/user-guide/providers/gcp/getting-started-gcp.mdx +++ b/docs/user-guide/providers/gcp/getting-started-gcp.mdx @@ -2,7 +2,7 @@ title: 'Getting Started With GCP on Prowler' --- -## Prowler App +## Prowler Cloud ### Step 1: Get the GCP Project ID @@ -11,7 +11,7 @@ title: 'Getting Started With GCP on Prowler' ![Get the Project ID](/images/providers/project-id-console.png) -### Step 2: Access Prowler Cloud or Prowler App +### Step 2: Access Prowler Cloud 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) 2. Go to "Configuration" > "Cloud Providers" diff --git a/docs/user-guide/providers/iac/getting-started-iac.mdx b/docs/user-guide/providers/iac/getting-started-iac.mdx index 849571b2c8..2aba3cf551 100644 --- a/docs/user-guide/providers/iac/getting-started-iac.mdx +++ b/docs/user-guide/providers/iac/getting-started-iac.mdx @@ -31,7 +31,7 @@ Prowler IaC provider scans the following Infrastructure as Code configurations f - Mutelist logic ([filtering](https://trivy.dev/latest/docs/configuration/filtering/)) is handled by Trivy, not Prowler. - Results are output in the same formats as other Prowler providers (CSV, JSON, HTML, etc.). -## Prowler App +## Prowler Cloud diff --git a/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx b/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx index c4f4822792..aff63b81a3 100644 --- a/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx +++ b/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx @@ -2,7 +2,7 @@ title: 'Getting Started with Kubernetes' --- -## Prowler App +## Prowler Cloud ### Step 1: Access Prowler Cloud/App diff --git a/docs/user-guide/providers/oci/getting-started-oci.mdx b/docs/user-guide/providers/oci/getting-started-oci.mdx index 8affa2f992..459d9ad685 100644 --- a/docs/user-guide/providers/oci/getting-started-oci.mdx +++ b/docs/user-guide/providers/oci/getting-started-oci.mdx @@ -14,7 +14,7 @@ The following steps apply to Prowler Cloud and the self-hosted Prowler App. 3. Generate or locate the API key fingerprint and private key for that user. Follow the [Config File Authentication steps](/user-guide/providers/oci/authentication#config-file-authentication-manual-api-key-setup) to create or rotate the key pair and copy the fingerprint. 4. Note the **Region** identifier to scan (for example, `us-ashburn-1`). -### Step 2: Access Prowler Cloud or Prowler App +### Step 2: Access Prowler Cloud 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app). 2. Go to **Configuration** → **Cloud Providers** and click **Add Cloud Provider**. ![Add OCI Cloud Provider](./images/oci-add-cloud-provider.png) diff --git a/docs/user-guide/providers/vercel/getting-started-vercel.mdx b/docs/user-guide/providers/vercel/getting-started-vercel.mdx index 67d4853d18..ddec26e6dc 100644 --- a/docs/user-guide/providers/vercel/getting-started-vercel.mdx +++ b/docs/user-guide/providers/vercel/getting-started-vercel.mdx @@ -13,9 +13,63 @@ Set up authentication for Vercel with the [Vercel Authentication](/user-guide/pr - Create a Vercel API Token with access to the target team - Identify the Team ID (optional, required to scope the scan to a single team) + + + Onboard Vercel using Prowler Cloud + + + Onboard Vercel using Prowler CLI + + + +## Prowler Cloud + + + +### Step 1: Add the Provider + +1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app). +2. Navigate 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 "Vercel". + + ![Select Vercel](/images/providers/select-vercel-prowler-cloud.png) + +5. Enter the **Team ID** and an optional alias, then click "Next". + + ![Add Vercel Team ID](/images/providers/vercel-team-id-form.png) + + +The Team ID can be found in the Vercel Dashboard under "Settings" > "General". It follows the format `team_xxxxxxxxxxxxxxxxxxxx`. For detailed instructions, see the [Authentication guide](/user-guide/providers/vercel/authentication). + + +### Step 2: Provide Credentials + +1. Enter the **API Token** created in the Vercel Dashboard. + + ![API Token Form](/images/providers/vercel-token-form.png) + +For the complete token creation workflow, follow the [Authentication guide](/user-guide/providers/vercel/authentication#api-token). + +### Step 3: Launch the Scan + +1. Review the connection summary. +2. Choose the scan schedule: run a single scan or set up daily scans (every 24 hours). +3. Click **Launch Scan** to start auditing Vercel. + + ![Launch Scan](/images/providers/vercel-launch-scan.png) + +--- + ## Prowler CLI - + ### Step 1: Set Up Authentication diff --git a/docs/user-guide/tutorials/prowler-app-attack-paths.mdx b/docs/user-guide/tutorials/prowler-app-attack-paths.mdx index 646ee557de..ad0b1fcce1 100644 --- a/docs/user-guide/tutorials/prowler-app-attack-paths.mdx +++ b/docs/user-guide/tutorials/prowler-app-attack-paths.mdx @@ -105,6 +105,102 @@ If a query requires no parameters, the form displays a message confirming that t width="700" /> +## Writing Custom openCypher Queries + +In addition to the built-in queries, Attack Paths supports custom read-only [openCypher](https://opencypher.org/) queries. Custom queries provide direct access to the underlying graph so security teams can answer ad-hoc questions, prototype detections, or extend coverage beyond the built-in catalogue. + +To write a custom query, select **Custom openCypher query** from the query dropdown. A code editor with syntax highlighting and line numbers appears, ready to receive the query. + +### Constraints and Safety Limits + +Custom queries are sandboxed to keep the graph database safe and responsive: + +- **Read-only:** Only read operations are allowed. Statements that mutate the graph (`CREATE`, `MERGE`, `SET`, `DELETE`, `REMOVE`, `DROP`, `LOAD CSV`, `CALL { ... }` writes, etc.) are rejected before execution. +- **Length limit:** Each query is capped at **10,000 characters**. +- **Scoped to the selected scan:** Results are automatically scoped to the provider and scan selected on the left panel. There is no need to filter by tenant or scan identifier in the query body. + +### Example Queries + +The following examples are read-only and can be pasted directly into the editor. Each one demonstrates a different graph traversal pattern. + +**Internet-exposed EC2 instances with their security group rules:** + +```cypher +MATCH (i:EC2Instance)--(sg:EC2SecurityGroup)--(rule:IpPermissionInbound) +WHERE i.exposed_internet = true +RETURN i.instanceid AS instance, sg.name AS security_group, + rule.fromport AS from_port, rule.toport AS to_port +LIMIT 25 +``` + +**EC2 instances that can assume IAM roles:** + +```cypher +MATCH (i:EC2Instance)-[:STS_ASSUMEROLE_ALLOW]->(r:AWSRole) +WHERE i.exposed_internet = true +RETURN i.instanceid AS instance, r.name AS role_name, r.arn AS role_arn +LIMIT 25 +``` + +**IAM principals with wildcard Allow statements:** + +```cypher +MATCH (principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) +WHERE stmt.effect = 'Allow' + AND ANY(action IN stmt.action WHERE action = '*') +RETURN principal.arn AS principal, policy.arn AS policy, + stmt.action AS actions, stmt.resource AS resources +LIMIT 25 +``` + +**Critical findings on internet-exposed resources:** + +```cypher +MATCH (i:EC2Instance)-[:HAS_FINDING]->(f:ProwlerFinding) +WHERE i.exposed_internet = true AND f.status = 'FAIL' + AND f.severity IN ['critical', 'high'] +RETURN i.instanceid AS instance, f.check_id AS check, + f.severity AS severity, f.status AS status +LIMIT 50 +``` + +**Roles trusting an AWS service (building block for PassRole escalation):** + +```cypher +MATCH (r:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(p:AWSPrincipal) +WHERE p.arn ENDS WITH '.amazonaws.com' +RETURN r.name AS role_name, r.arn AS role_arn, p.arn AS trusted_service +LIMIT 25 +``` + +### Tips for Writing Queries + +- Start small with `LIMIT` to inspect the shape of the data before broadening the pattern. +- Use `RETURN` projections (`RETURN n.name, n.region`) instead of returning whole nodes to keep responses compact. +- Combine resource nodes with `ProwlerFinding` nodes via `HAS_FINDING` to correlate misconfigurations with the affected resources. +- When a query times out or returns no rows, simplify the pattern step by step until the first variant runs successfully, then add constraints back. + +### Cartography Schema Reference + +Attack Paths graphs are populated by [Cartography](https://github.com/cartography-cncf/cartography), an open-source graph ingestion framework. The node labels, relationship types, and properties available in custom queries follow the upstream Cartography schema for the corresponding provider. + +For the complete catalogue of node labels and relationships available in custom queries, refer to the official Cartography schema documentation: + +- **AWS:** [Cartography AWS Schema](https://cartography-cncf.github.io/cartography/modules/aws/schema.html) + +In addition to the upstream schema, Prowler enriches the graph with: + +- **`ProwlerFinding`** nodes representing Prowler check results, linked to affected resources via `HAS_FINDING` relationships. +- **`Internet`** nodes used to model exposure paths from the public internet to internal resources. + + + AI assistants connected through Prowler MCP Server can fetch the exact + Cartography schema for the active scan via the + `prowler_app_get_attack_paths_cartography_schema` tool. This guarantees that + generated queries match the schema version pinned by the running Prowler + release. + + ## Executing a Query To run the selected query against the scan data, click **Execute Query**. The button displays a loading state while the query processes. diff --git a/docs/user-guide/tutorials/prowler-app-multi-tenant.mdx b/docs/user-guide/tutorials/prowler-app-multi-tenant.mdx new file mode 100644 index 0000000000..69577c2f0c --- /dev/null +++ b/docs/user-guide/tutorials/prowler-app-multi-tenant.mdx @@ -0,0 +1,151 @@ +--- +title: 'Managing Organizations (Multi-Tenant)' +--- + +import { VersionBadge } from "/snippets/version-badge.mdx" + + + +Prowler App supports multi-tenancy through **Organizations**, allowing users to belong to multiple isolated environments within a single account. Each organization maintains its own providers, scans, findings, and user memberships, ensuring complete data separation between teams or business units. + +## Key Concepts + +* **Organization (Tenant):** An isolated workspace containing its own providers, scans, findings, roles, and users. Every Prowler account operates within at least one organization. +* **Membership:** The association between a user and an organization, including the membership role (`owner` or `member`). +* **Active Organization:** The organization currently in use for the session. All actions (scans, findings, provider management) apply to the active organization. + + +When a new account is created without an invitation, a default organization is automatically provisioned. Accounts created through an invitation join the inviter's organization instead. + + + +## Viewing Organizations + +To view all organizations associated with an account, navigate to the **Profile** page. The **Organizations** card displays every organization the user belongs to, including the role, name, join date, and whether it is the currently active organization. + +Organizations card in profile page + +## Creating an Organization + +To create a new organization: + +1. Navigate to the **Profile** page. + +2. In the **Organizations** card, click the **Create organization** button. + + Create organization button + +3. Enter a name for the new organization (maximum 100 characters). + + Create organization modal + +4. Click **Create**. The session automatically switches to the newly created organization. + + +Creating an organization requires being authenticated. Any user can create a new organization regardless of their current role. + + + +## Switching Between Organizations + +To switch the active organization: + +1. Navigate to the **Profile** page. + +2. In the **Organizations** card, locate the organization to switch to. + +3. Click the **Switch** button next to the desired organization. + +4. Confirm the switch in the dialog. The page reloads with the new organization's context, and all subsequent actions apply to it. + + Switch organization confirmation modal + + +The currently active organization is indicated by an **Active** badge. Switching updates the session tokens, so the page will reload automatically. + + + +## Editing an Organization Name + +Organization owners with the **Manage Account** permission can rename an organization: + +1. Navigate to the **Profile** page. + +2. In the **Organizations** card, click the **Edit** button next to the organization. + +3. Update the name and save the changes. + + Edit organization name modal + +## Deleting an Organization + +Organization owners with the **Manage Account** permission can delete an organization, provided they belong to at least two organizations (the last remaining organization cannot be deleted). + +### Deleting a Non-Active Organization + +1. Navigate to the **Profile** page. + +2. Click the **Delete** button next to the organization to remove. + +3. Type the organization name to confirm deletion. + + Delete organization confirmation modal + +4. Click **Delete**. The organization and all its associated data (providers, scans, findings) are permanently removed. + +### Deleting the Active Organization + +When deleting the currently active organization, an additional step is required: + +1. Navigate to the **Profile** page. + +2. Click the **Delete** button next to the active organization. + +3. Select which organization to switch to after deletion. + +4. Type the organization name to confirm. + + Delete active organization modal with target selection + +5. Click **Delete**. The session switches to the selected organization, and the deleted organization's data is permanently removed. + + +Deleting an organization is irreversible. All providers, scans, findings, and configuration data within the organization are permanently deleted. Users who belong only to the deleted organization will lose access to Prowler. + + +## Accepting an Invitation to an Organization + +When invited to join an organization, the invited user receives a link to accept the invitation. The flow adapts depending on whether the user already has a Prowler account: + +### Existing Users + +1. Open the invitation link. + +2. If already authenticated, the invitation is accepted automatically and the user is redirected to Prowler App. + +3. If not authenticated, choose **I have an account -- Sign in**, authenticate with existing credentials, and the invitation is accepted upon sign-in. + + Sign in screen after choosing I have an account from invitation + +### New Users + +1. Open the invitation link. + +2. Choose **I'm new -- Create an account**. + +3. Complete the sign-up process. Upon account creation, the invitation is accepted and the user joins the inviter's organization. + + +Invitations expire after 7 days. If an invitation has expired, contact the organization administrator to send a new one. For more details on invitation management, see [Managing Users and Role-Based Access Control (RBAC)](/user-guide/tutorials/prowler-app-rbac#invitations). + + + +## Permissions Reference + +| Action | Required Conditions | +|--------|-------------------| +| View organizations | Any authenticated user | +| Create an organization | Any authenticated user | +| Switch organizations | Any authenticated user | +| Edit organization name | Organization owner with **Manage Account** permission | +| Delete an organization | Organization owner with **Manage Account** permission; must belong to more than one organization | diff --git a/mcp_server/CHANGELOG.md b/mcp_server/CHANGELOG.md index 21aa71dbf2..64ea033f0e 100644 --- a/mcp_server/CHANGELOG.md +++ b/mcp_server/CHANGELOG.md @@ -2,12 +2,16 @@ All notable changes to the **Prowler MCP Server** are documented in this file. -## [0.6.0] (Prowler UNRELEASED) +## [0.6.0] (Prowler v5.23.0) ### 🚀 Added - Resource events tool to get timeline for a resource (who, what, when) [(#10412)](https://github.com/prowler-cloud/prowler/pull/10412) +### 🔄 Changed + +- Pin `httpx` dependency to exact version for reproducible installs [(#10593)](https://github.com/prowler-cloud/prowler/pull/10593) + ### 🔐 Security - `authlib` bumped from 1.6.5 to 1.6.9 to fix CVE-2026-28802 (JWT `alg: none` validation bypass) [(#10579)](https://github.com/prowler-cloud/prowler/pull/10579) diff --git a/mcp_server/pyproject.toml b/mcp_server/pyproject.toml index 4ea4a9859e..2a6885fedb 100644 --- a/mcp_server/pyproject.toml +++ b/mcp_server/pyproject.toml @@ -5,7 +5,7 @@ requires = ["setuptools>=61.0", "wheel"] [project] dependencies = [ "fastmcp==2.14.0", - "httpx>=0.28.0" + "httpx==0.28.1" ] description = "MCP server for Prowler ecosystem" name = "prowler-mcp" diff --git a/mcp_server/uv.lock b/mcp_server/uv.lock index ca1d9482be..969559eb92 100644 --- a/mcp_server/uv.lock +++ b/mcp_server/uv.lock @@ -727,7 +727,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "fastmcp", specifier = "==2.14.0" }, - { name = "httpx", specifier = ">=0.28.0" }, + { name = "httpx", specifier = "==0.28.1" }, ] [[package]] @@ -888,11 +888,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, ] [package.optional-dependencies] diff --git a/poetry.lock b/poetry.lock index a76112ff76..7d5ccdf355 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand. [[package]] name = "about-time" @@ -1671,18 +1671,6 @@ files = [ [package.dependencies] pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - [[package]] name = "cfn-lint" version = "1.38.0" @@ -1888,6 +1876,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "contextlib2" @@ -2180,18 +2169,6 @@ files = [ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] -[[package]] -name = "distlib" -version = "0.4.0" -description = "Distribution utilities" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, - {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, -] - [[package]] name = "distro" version = "1.9.0" @@ -2848,21 +2825,6 @@ files = [ {file = "iamdata-0.1.202507281.tar.gz", hash = "sha256:4050870068ca2fb044d03c46229bc8dbafb4f99db2f50c77297aafd437154ddd"}, ] -[[package]] -name = "identify" -version = "2.6.12" -description = "File identification library for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2"}, - {file = "identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6"}, -] - -[package.extras] -license = ["ukkonen"] - [[package]] name = "idna" version = "3.10" @@ -3083,7 +3045,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -3163,7 +3125,7 @@ files = [ ] [package.dependencies] -certifi = ">=14.05.14" +certifi = ">=14.5.14" durationpy = ">=0.7" google-auth = ">=1.0.1" oauthlib = ">=3.2.2" @@ -4000,18 +3962,6 @@ plot = ["matplotlib"] tgrep = ["pyparsing"] twitter = ["twython"] -[[package]] -name = "nodeenv" -version = "1.9.1" -description = "Node.js virtual environment builder" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, -] - [[package]] name = "numpy" version = "2.0.2" @@ -4443,24 +4393,32 @@ files = [ ] [[package]] -name = "pre-commit" -version = "4.2.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." +name = "prek" +version = "0.3.9" +description = "A Git hook manager written in Rust, designed as a drop-in alternative to pre-commit." optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, - {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, + {file = "prek-0.3.9-py3-none-linux_armv6l.whl", hash = "sha256:3ed793d51bfaa27bddb64d525d7acb77a7c8644f549412d82252e3eb0b88aad8"}, + {file = "prek-0.3.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:399c58400c0bd0b82a93a3c09dc1bfd88d8d0cfb242d414d2ed247187b06ead1"}, + {file = "prek-0.3.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ea1ffb124e92f081b8e2ca5b5a623a733efb3be0c5b1f4b7ffe2ee17d1f20c"}, + {file = "prek-0.3.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:aaf639f95b7301639298311d8d44aad0d0b4864e9736083ad3c71ce9765d37ab"}, + {file = "prek-0.3.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff104863b187fa443ea8451ca55d51e2c6e94f99f00d88784b5c3c4c623f1ebe"}, + {file = "prek-0.3.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:039ecaf87c63a3e67cca645ebd5bc5eb6aafa6c9d929e9a27b2921e7849d7ef9"}, + {file = "prek-0.3.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bde2a3d045705095983c7f78ba04f72a7565fe1c2b4e85f5628502a254754ff"}, + {file = "prek-0.3.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0960a21543563e2c8e19aaad176cc8423a87aac3c914d0f313030d7a9244a"}, + {file = "prek-0.3.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb5d5171d7523271909246ee306b4dc3d5b63752e7dd7c7e8a8908fc9490d1"}, + {file = "prek-0.3.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82b791bd36c1430c84d3ae7220a85152babc7eaf00f70adcb961bd594e756ba3"}, + {file = "prek-0.3.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:6eac6d2f736b041118f053a1487abed468a70dd85a8688eaf87bb42d3dcecf20"}, + {file = "prek-0.3.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5517e46e761367a3759b3168eabc120840ffbca9dfbc53187167298a98f87dc4"}, + {file = "prek-0.3.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:92024778cf78683ca32687bb249ab6a7d5c33887b5ee1d1a9f6d0c14228f4cf3"}, + {file = "prek-0.3.9-py3-none-win32.whl", hash = "sha256:7f89c55e5f480f5d073769e319924ad69d4bf9f98c5cb46a83082e26e634c958"}, + {file = "prek-0.3.9-py3-none-win_amd64.whl", hash = "sha256:7722f3372eaa83b147e70a43cb7b9fe2128c13d0c78d8a1cdbf2a8ec2ee071eb"}, + {file = "prek-0.3.9-py3-none-win_arm64.whl", hash = "sha256:0bced6278d6cc8a4b46048979e36bc9da034611dc8facd77ab123177b833a929"}, + {file = "prek-0.3.9.tar.gz", hash = "sha256:f82b92d81f42f1f90a47f5fbbf492373e25ef1f790080215b2722dd6da66510e"}, ] -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - [[package]] name = "propcache" version = "0.3.2" @@ -4944,24 +4902,25 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.12.1" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, - {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, + {file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"}, + {file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"}, ] [package.dependencies] cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} +typing_extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"] [[package]] name = "pylint" @@ -4976,7 +4935,7 @@ files = [ ] [package.dependencies] -astroid = ">=3.3.8,<=3.4.0-dev0" +astroid = ">=3.3.8,<=3.4.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -5822,10 +5781,10 @@ files = [ ] [package.dependencies] -botocore = ">=1.37.4,<2.0a.0" +botocore = ">=1.37.4,<2.0a0" [package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] [[package]] name = "safety" @@ -6281,27 +6240,6 @@ files = [ {file = "uuid6-2024.7.10.tar.gz", hash = "sha256:2d29d7f63f593caaeea0e0d0dd0ad8129c9c663b29e19bdf882e864bedf18fb0"}, ] -[[package]] -name = "virtualenv" -version = "20.32.0" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56"}, - {file = "virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] - [[package]] name = "vulture" version = "2.14" @@ -6743,4 +6681,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.13" -content-hash = "91739ee5e383337160f9f08b76944ab4e8629c94084c8a9d115246862557f7c5" +content-hash = "786921163bb46716defae1d9de1df001af2abf17edd3061165638707bcd28ce4" diff --git a/prowler/AGENTS.md b/prowler/AGENTS.md index b1d83fa200..5f7099ac0a 100644 --- a/prowler/AGENTS.md +++ b/prowler/AGENTS.md @@ -81,7 +81,7 @@ class {check_name}(Check): ## TECH STACK -Python 3.10+ | Poetry 2+ | pytest | moto (AWS mocking) | Pre-commit hooks (black, flake8, pylint, bandit) +Python 3.10+ | Poetry 2.3+ | pytest | moto (AWS mocking) | Pre-commit hooks (black, flake8, pylint, bandit) --- diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index e3728ce68e..5d5778874d 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,7 +2,30 @@ All notable changes to the **Prowler SDK** are documented in this file. -## [5.23.0] (Prowler UNRELEASED) +## [5.24.0] (Prowler UNRELEASED) + +### 🚀 Added + +- `entra_conditional_access_policy_directory_sync_account_excluded` check for M365 provider [(#10620)](https://github.com/prowler-cloud/prowler/pull/10620) +- `intune_device_compliance_policy_unassigned_devices_not_compliant_by_default` check for M365 provider [(#10599)](https://github.com/prowler-cloud/prowler/pull/10599) +- `entra_conditional_access_policy_all_apps_all_users` check for M365 provider [(#10619)](https://github.com/prowler-cloud/prowler/pull/10619) +- `bedrock_full_access_policy_attached` check for AWS provider [(#10577)](https://github.com/prowler-cloud/prowler/pull/10577) +- `iam_role_access_not_stale_to_bedrock` and `iam_user_access_not_stale_to_bedrock` checks for AWS provider [(#10536)](https://github.com/prowler-cloud/prowler/pull/10536) +- `iam_policy_no_wildcard_marketplace_subscribe` and `iam_inline_policy_no_wildcard_marketplace_subscribe` checks for AWS provider [(#10525)](https://github.com/prowler-cloud/prowler/pull/10525) +- `bedrock_vpc_endpoints_configured` check for AWS provider [(#10591)](https://github.com/prowler-cloud/prowler/pull/10591) +- `exchange_organization_delicensing_resiliency_enabled` check for m365 provider [(#10608)](https://github.com/prowler-cloud/prowler/pull/10608) +- `entra_conditional_access_policy_mfa_enforced_for_guest_users` check for M365 provider [(#10616)](https://github.com/prowler-cloud/prowler/pull/10616) +- `entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced` check for m365 provider [(#10618)](https://github.com/prowler-cloud/prowler/pull/10618) +- `entra_conditional_access_policy_block_unknown_device_platforms` check for m365 provider [(#10615)](https://github.com/prowler-cloud/prowler/pull/10615) + +### 🔄 Changed + +- Bump Poetry to `2.3.4` and consolidate SDK workflows onto the `setup-python-poetry` composite action with opt-in lockfile regeneration [(#10681)](https://github.com/prowler-cloud/prowler/pull/10681) +- Normalize Conditional Access platform values in Entra models and simplify platform-based checks [(#10635)](https://github.com/prowler-cloud/prowler/pull/10635) + +--- + +## [5.23.0] (Prowler v5.23.0) ### 🚀 Added @@ -18,20 +41,24 @@ All notable changes to the **Prowler SDK** are documented in this file. - CISA SCuBA Google Workspace Baselines compliance [(#10466)](https://github.com/prowler-cloud/prowler/pull/10466) - CIS Google Workspace Foundations Benchmark v1.3.0 compliance [(#10462)](https://github.com/prowler-cloud/prowler/pull/10462) - `calendar_external_sharing_primary_calendar`, `calendar_external_sharing_secondary_calendar`, and `calendar_external_invitations_warning` checks for Google Workspace provider using the Cloud Identity Policy API [(#10597)](https://github.com/prowler-cloud/prowler/pull/10597) +- 11 Drive and Docs checks for Google Workspace provider (`drive_external_sharing_warn_users`, `drive_publishing_files_disabled`, `drive_sharing_allowlisted_domains`, `drive_warn_sharing_with_allowlisted_domains`, `drive_access_checker_recipients_only`, `drive_internal_users_distribute_content`, `drive_shared_drive_creation_allowed`, `drive_shared_drive_managers_cannot_override`, `drive_shared_drive_members_only_access`, `drive_shared_drive_disable_download_print_copy`, `drive_desktop_access_disabled`) using the Cloud Identity Policy API [(#10648)](https://github.com/prowler-cloud/prowler/pull/10648) - `entra_conditional_access_policy_device_registration_mfa_required` check and `entra_intune_enrollment_sign_in_frequency_every_time` enhancement for M365 provider [(#10222)](https://github.com/prowler-cloud/prowler/pull/10222) - `entra_conditional_access_policy_block_elevated_insider_risk` check for M365 provider [(#10234)](https://github.com/prowler-cloud/prowler/pull/10234) - `Vercel` provider support with 30 checks [(#10189)](https://github.com/prowler-cloud/prowler/pull/10189) - `api_token_ip_restriction_enabled` check for cloudflare provider [(#10624)](https://github.com/prowler-cloud/prowler/pull/10624) +- `internet-exposed` category for 13 AWS checks (CloudFront, CodeArtifact, EC2, EFS, RDS, SageMaker, Shield, VPC) [(#10502)](https://github.com/prowler-cloud/prowler/pull/10502) +- `stepfunctions_statemachine_no_secrets_in_definition` check for hardcoded secrets in AWS Step Functions state machine definitions [(#10570)](https://github.com/prowler-cloud/prowler/pull/10570) +- CCC improvements with the latest checks and new mappings [(#10625)](https://github.com/prowler-cloud/prowler/pull/10625) ### 🔄 Changed -- Added `internet-exposed` category to 13 AWS checks (CloudFront, CodeArtifact, EC2, EFS, RDS, SageMaker, Shield, VPC) [(#10502)](https://github.com/prowler-cloud/prowler/pull/10502) - Minimum Python version from 3.9 to 3.10 and updated classifiers to reflect supported versions (3.10, 3.11, 3.12) [(#10464)](https://github.com/prowler-cloud/prowler/pull/10464) +- Pin direct SDK dependencies to exact versions and rely on `poetry.lock` artifact hashes for reproducible installs [(#10593)](https://github.com/prowler-cloud/prowler/pull/10593) - Sensitive CLI flags now warn when values are passed directly, recommending environment variables instead [(#10532)](https://github.com/prowler-cloud/prowler/pull/10532) ### 🐞 Fixed -- OCI mutelist support: pass `tenancy_id` to `is_finding_muted` and update `oraclecloud_mutelist_example.yaml` to use `Accounts` key [(#10565)](https://github.com/prowler-cloud/prowler/issues/10565) +- OCI mutelist support: pass `tenancy_id` to `is_finding_muted` and update `oraclecloud_mutelist_example.yaml` to use `Accounts` key [(#10566)](https://github.com/prowler-cloud/prowler/pull/10566) - `return` statements in `finally` blocks replaced across IAM, Organizations, GCP provider, and custom checks metadata to stop silently swallowing exceptions [(#10102)](https://github.com/prowler-cloud/prowler/pull/10102) - `JiraConnection` now includes issue types per project fetched during `test_connection`, fixing `JiraInvalidIssueTypeError` on non-English Jira instances [(#10534)](https://github.com/prowler-cloud/prowler/pull/10534) - `--list-checks` and `--list-checks-json` now include `threat-detection` category checks in their output [(#10578)](https://github.com/prowler-cloud/prowler/pull/10578) @@ -40,6 +67,15 @@ All notable changes to the **Prowler SDK** are documented in this file. - `is_policy_public` now recognizes `kms:CallerAccount`, `kms:ViaService`, `aws:CalledVia`, `aws:CalledViaFirst`, and `aws:CalledViaLast` as restrictive condition keys, fixing false positives in `kms_key_policy_is_not_public` and other checks that use `is_condition_block_restrictive` [(#10600)](https://github.com/prowler-cloud/prowler/pull/10600) - `_enabled_regions` empty-set bug in `AwsProvider.generate_regional_clients` creating boto3 clients for all 36 AWS regions instead of the audited ones, causing random CI timeouts and slow test runs [(#10598)](https://github.com/prowler-cloud/prowler/pull/10598) - Retrieve only the latest version from a package in AWS CodeArtifact [(#10243)](https://github.com/prowler-cloud/prowler/pull/10243) +- AWS global services (CloudFront, Route53, Shield, FMS) now use the partition's global region instead of the profile's default region [(#10458)](https://github.com/prowler-cloud/prowler/pull/10458) +- Oracle Cloud `events_rule_idp_group_mapping_changes` now recognizes the CIS 3.1 `add/remove` event names to avoid false positives [(#10416)](https://github.com/prowler-cloud/prowler/pull/10416) +- Oracle Cloud password policy checks now exclude immutable system-managed policies (`SimplePasswordPolicy`, `StandardPasswordPolicy`) to avoid false positives [(#10453)](https://github.com/prowler-cloud/prowler/pull/10453) +- Oracle Cloud `kms_key_rotation_enabled` now checks current key version age to avoid false positives on vaults without auto-rotation support [(#10450)](https://github.com/prowler-cloud/prowler/pull/10450) +- OCI filestorage, blockstorage, KMS, and compute services now honor `--region` for scanning outside the tenancy home region [(#10472)](https://github.com/prowler-cloud/prowler/pull/10472) +- OCI provider now supports multi-region filtering via `--region` [(#10473)](https://github.com/prowler-cloud/prowler/pull/10473) +- `prowler image --registry` failing with `ImageNoImagesProvidedError` due to registry arguments not being forwarded to `ImageProvider` in `init_global_provider` [(#10470)](https://github.com/prowler-cloud/prowler/pull/10470) +- OCI multi-region support for identity client configuration in blockstorage, identity, and filestorage services [(#10520)](https://github.com/prowler-cloud/prowler/pull/10520) +- Google Workspace Calendar checks now filter for customer-level policies only, skipping OU and group overrides that could produce incorrect audit results [(#10658)](https://github.com/prowler-cloud/prowler/pull/10658) ### 🔐 Security @@ -50,21 +86,6 @@ All notable changes to the **Prowler SDK** are documented in this file. --- -## [5.22.1] (Prowler UNRELEASED) - -### 🐞 Fixed - -- AWS global services (CloudFront, Route53, Shield, FMS) now use the partition's global region instead of the profile's default region [(#10458)](https://github.com/prowler-cloud/prowler/issues/10458) -- Oracle Cloud `events_rule_idp_group_mapping_changes` now recognizes the CIS 3.1 `add/remove` event names to avoid false positives [(#10416)](https://github.com/prowler-cloud/prowler/pull/10416) -- Oracle Cloud password policy checks now exclude immutable system-managed policies (`SimplePasswordPolicy`, `StandardPasswordPolicy`) to avoid false positives [(#10453)](https://github.com/prowler-cloud/prowler/pull/10453) -- Oracle Cloud `kms_key_rotation_enabled` now checks current key version age to avoid false positives on vaults without auto-rotation support [(#10450)](https://github.com/prowler-cloud/prowler/pull/10450) -- Oracle Cloud patch for filestorage, blockstorage, kms, and compute services in OCI to allow for region scanning outside home [(#10455)](https://github.com/prowler-cloud/prowler/pull/10472) -- Oracle cloud provider now supports multi-region filtering [(#10435)](https://github.com/prowler-cloud/prowler/pull/10473) -- `prowler image --registry` failing with `ImageNoImagesProvidedError` due to registry arguments not being forwarded to `ImageProvider` in `init_global_provider` [(#10457)](https://github.com/prowler-cloud/prowler/issues/10457) -- Oracle Cloud multi-region support for identity client configuration in blockstorage, identity, and filestorage services [(#10519)](https://github.com/prowler-cloud/prowler/pull/10520) - ---- - ## [5.22.0] (Prowler v5.22.0) ### 🐞 Fixed diff --git a/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json b/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json index 496ef9cabd..7a04e4ddf7 100644 --- a/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json +++ b/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json @@ -344,6 +344,9 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", + "bedrock_full_access_policy_attached", "ec2_instance_profile_attached", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", @@ -671,6 +674,7 @@ "sagemaker_notebook_instance_vpc_settings_configured", "sagemaker_training_jobs_network_isolation_enabled", "sagemaker_training_jobs_vpc_settings_configured", + "bedrock_vpc_endpoints_configured", "vpc_endpoint_connections_trust_boundaries", "vpc_endpoint_services_allowed_principals_trust_boundaries" ] diff --git a/prowler/compliance/aws/c5_aws.json b/prowler/compliance/aws/c5_aws.json index cf553bef70..ddb4c8fce7 100644 --- a/prowler/compliance/aws/c5_aws.json +++ b/prowler/compliance/aws/c5_aws.json @@ -5130,6 +5130,8 @@ "iam_support_role_created", "iam_user_with_temporary_credentials", "bedrock_api_key_no_administrative_privileges", + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "fms_policy_compliant", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", @@ -5200,6 +5202,8 @@ "iam_support_role_created", "iam_user_with_temporary_credentials", "bedrock_api_key_no_administrative_privileges", + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "fms_policy_compliant", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", @@ -5282,6 +5286,8 @@ "iam_password_policy_reuse_24", "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "secretsmanager_secret_unused" @@ -5568,6 +5574,7 @@ } ], "Checks": [ + "bedrock_full_access_policy_attached", "iam_policy_allows_privilege_escalation", "iam_role_administratoraccess_policy", "iam_policy_cloudshell_admin_not_attached", @@ -6350,6 +6357,8 @@ "iam_role_administratoraccess_policy", "iam_role_cross_account_readonlyaccess_policy", "iam_rotate_access_key_90_days", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_administrator_access_policy", "iam_user_console_access_unused", @@ -7209,6 +7218,7 @@ ], "Checks": [ "workspaces_vpc_2private_1public_subnets_nat", + "bedrock_vpc_endpoints_configured", "vpc_endpoint_connections_trust_boundaries", "networkfirewall_multi_az" ] diff --git a/prowler/compliance/aws/ccc_aws.json b/prowler/compliance/aws/ccc_aws.json index 1f6da6d5f6..a625de389c 100644 --- a/prowler/compliance/aws/ccc_aws.json +++ b/prowler/compliance/aws/ccc_aws.json @@ -1,10 +1,1835 @@ { "Framework": "CCC", - "Version": "", + "Version": "v2025.10", "Provider": "AWS", "Name": "Common Cloud Controls Catalog (CCC)", "Description": "Common Cloud Controls Catalog (CCC) for AWS", "Requirements": [ + { + "Id": "CCC.Core.CN01.AR01", + "Description": "When a port is exposed for non-SSH network traffic, all traffic MUST include a TLS handshake AND be encrypted using TLS 1.3 or higher.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Most cloud services enable TLS 1.3 by default. Where it is not already set, ensure that your services are configured or updated accordingly.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [ + "cloudfront_distributions_https_enabled", + "cloudfront_distributions_origin_traffic_encrypted", + "cloudfront_distributions_using_deprecated_ssl_protocols", + "elb_insecure_ssl_ciphers", + "elb_ssl_listeners", + "elbv2_insecure_ssl_ciphers", + "elbv2_ssl_listeners", + "elbv2_nlb_tls_termination_enabled", + "s3_bucket_secure_transport_policy", + "opensearch_service_domains_https_communications_enforced", + "opensearch_service_domains_node_to_node_encryption_enabled", + "elasticache_redis_cluster_in_transit_encryption_enabled", + "dynamodb_accelerator_cluster_in_transit_encryption_enabled", + "dms_endpoint_ssl_enabled", + "dms_endpoint_redis_in_transit_encryption_enabled", + "kafka_cluster_in_transit_encryption_enabled", + "kafka_connector_in_transit_encryption_enabled", + "redshift_cluster_in_transit_encryption_enabled", + "rds_instance_transport_encrypted", + "transfer_server_in_transit_encryption_enabled", + "glue_database_connections_ssl_enabled", + "sns_subscription_not_using_http_endpoints" + ] + }, + { + "Id": "CCC.Core.CN01.AR02", + "Description": "When a port is exposed for SSH network traffic, all traffic MUST include a SSH handshake AND be encrypted using SSHv2 or higher.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Any time port 22 is exposed, ensure that it has a properly implemented SSH server with SSHv2 enabled and configured with strong ciphers.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [ + "ec2_instance_port_ssh_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_networkacl_allow_ingress_tcp_port_22" + ] + }, + { + "Id": "CCC.Core.CN01.AR03", + "Description": "When the service receives unencrypted traffic, then it MUST either block the request or automatically redirect it to the secure equivalent.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Review firewall, load balancer, and application configurations to ensure insecure protocols such as HTTP, FTP, and Telnet are not exposed. Where possible, implement automatic redirection to secure protocols such as HTTPS, SFTP, SSH, and regularly scan for protocol drift.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [ + "cloudfront_distributions_https_enabled", + "cloudfront_distributions_origin_traffic_encrypted", + "opensearch_service_domains_https_communications_enforced", + "transfer_server_in_transit_encryption_enabled", + "s3_bucket_secure_transport_policy", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23" + ] + }, + { + "Id": "CCC.Core.CN01.AR07", + "Description": "When a port is exposed, the service MUST ensure that the protocol and service officially assigned to that port number by the IANA Service Name and Transport Protocol Port Number Registry, and no other, is run on that port.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Reference the IANA Service Name and Transport Protocol Port Number Registry for more information about correct protocol-to-port assignments. Avoid running non-standard services on well-known ports.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN01.AR08", + "Description": "When a service transmits data using TLS, mutual TLS (mTLS) MUST be implemented to require both client and server certificate authentication for all connections.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Configure mTLS for all endpoints that process or transmit sensitive data. Ensure both client and server certificates are validated and managed securely. Regularly review certificate authorities and automate certificate rotation where possible.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [ + "apigateway_restapi_client_certificate_enabled", + "kafka_cluster_mutual_tls_authentication_enabled" + ] + }, + { + "Id": "CCC.Core.CN13.AR01", + "Description": "When a port is exposed that uses certificate-based encryption, the service MUST only use valid, unexpired certificates issued by a trusted certificate authority.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH18" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "acm_certificates_expiration_check", + "acm_certificates_with_secure_key_algorithms", + "acm_certificates_transparency_logs_enabled" + ] + }, + { + "Id": "CCC.Core.CN13.AR02", + "Description": "When a port is exposed that uses certificate-based encryption, the service MUST rotate active certificates within 180 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", + "Applicability": [ + "tlp-amber" + ], + "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH18" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "acm_certificates_expiration_check" + ] + }, + { + "Id": "CCC.Core.CN13.AR03", + "Description": "When a port is exposed that uses certificate-based encryption, the service MUST rotate active certificates within 90 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH18" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "acm_certificates_expiration_check" + ] + }, + { + "Id": "CCC.Core.CN06.AR01", + "Description": "When the service is running, its region and availability zone MUST be included in a list of explicitly trusted or approved locations within the trust perimeter.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN06 Restrict Deployments to Trust Perimeter", + "SubSection": "", + "SubSectionObjective": "Ensure that the service and its child resources are only deployed on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Maintain an up-to-date list of trusted and approved regions based on organizational policies. Validate the service's deployment location is included in this list.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-19" + ] + } + ] + } + ], + "Checks": [ + "organizations_scp_check_deny_regions" + ] + }, + { + "Id": "CCC.Core.CN06.AR02", + "Description": "When a child resource is deployed, its region and availability zone MUST be included in a list of explicitly trusted or approved locations within the trust perimeter.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN06 Restrict Deployments to Trust Perimeter", + "SubSection": "", + "SubSectionObjective": "Ensure that the service and its child resources are only deployed on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Maintain an up-to-date list of trusted and approved regions based on organizational policies. Validate that child resources can only be deployed to locations included in this list.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-19" + ] + } + ] + } + ], + "Checks": [ + "organizations_scp_check_deny_regions" + ] + }, + { + "Id": "CCC.Core.CN08.AR01", + "Description": "When data is created or modified, the data MUST have a complete and recoverable duplicate that is stored in a physically separate data center.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN08 Replicate Data to Multiple Locations", + "SubSection": "", + "SubSectionObjective": "Ensure that data is replicated across multiple physical locations to protect against data loss due to hardware failures, natural disasters, or other catastrophic events.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Implement automated data replication processes to ensure that data is consistently duplicated in another region or availability zone. Regularly test data recovery from the replicated location to ensure integrity and availability.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "BCR-08", + "BCR-10", + "BCR-11" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_cross_region_replication", + "backup_plans_exist", + "backup_vaults_exist", + "dynamodb_table_protected_by_backup_plan", + "rds_cluster_protected_by_backup_plan", + "rds_instance_protected_by_backup_plan", + "rds_cluster_multi_az", + "rds_instance_multi_az", + "efs_multi_az_enabled", + "neptune_cluster_multi_az", + "documentdb_cluster_multi_az_enabled", + "elasticache_redis_cluster_multi_az_enabled" + ] + }, + { + "Id": "CCC.Core.CN08.AR02", + "Description": "When data is replicated into a second location, the service MUST be able to accurately represent the replication locations, replication status, and data synchronization status.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN08 Replicate Data to Multiple Locations", + "SubSection": "", + "SubSectionObjective": "Ensure that data is replicated across multiple physical locations to protect against data loss due to hardware failures, natural disasters, or other catastrophic events.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "BCR-08", + "BCR-10", + "BCR-11" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_cross_region_replication" + ] + }, + { + "Id": "CCC.Core.CN09.AR01", + "Description": "When the service is operational, its logs and any child resource logs MUST NOT be accessible from the resource they record access to.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH07", + "CCC.Core.TH09", + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-02", + "LOG-04", + "LOG-09" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", + "cloudwatch_log_group_not_publicly_accessible", + "cloudtrail_logs_s3_bucket_access_logging_enabled", + "cloudtrail_log_file_validation_enabled", + "cloudtrail_kms_encryption_enabled", + "cloudtrail_bucket_requires_mfa_delete" + ] + }, + { + "Id": "CCC.Core.CN09.AR02", + "Description": "When the service is operational, disabling the logs for the service or its child resources MUST NOT be possible without also disabling the corresponding resource.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "No normal business operations should disable logs, as this could indicate an attempt to cover up unauthorized access. Ensure that logging mechanisms are tightly integrated with service operations, so that logging cannot be disabled without stopping the service itself.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH07", + "CCC.Core.TH09", + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-02", + "LOG-04", + "LOG-09" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_multi_region_enabled", + "cloudtrail_log_file_validation_enabled", + "cloudtrail_kms_encryption_enabled", + "cloudtrail_cloudwatch_logging_enabled", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled" + ] + }, + { + "Id": "CCC.Core.CN09.AR03", + "Description": "When the service is operational, any attempt to redirect logs for the service or its child resources MUST NOT be possible without halting operation of the corresponding resource and publishing corresponding events to monitored channels.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "No normal business operations should result in the redirection of logs, as this could indicate an attempt to cover up unauthorized access. Ensure that logging configurations are immutable during service operation so that any changes require stopping the service and publishing corresponding events to monitored channels.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH07", + "CCC.Core.TH09", + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-02", + "LOG-04", + "LOG-09" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_log_file_validation_enabled", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", + "cloudwatch_changes_to_network_acls_alarm_configured", + "cloudwatch_changes_to_network_gateways_alarm_configured", + "cloudwatch_changes_to_network_route_tables_alarm_configured", + "cloudwatch_changes_to_vpcs_alarm_configured" + ] + }, + { + "Id": "CCC.Core.CN10.AR01", + "Description": "When data is replicated, the service MUST ensure that replication only occurs to destinations that are explicitly included within the defined trust perimeter.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN10 Restrict Data Replication to Trust Perimeter", + "SubSection": "", + "SubSectionObjective": "Ensure that data is only replicated on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-10", + "DSP-19" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_cross_region_replication" + ] + }, + { + "Id": "CCC.Core.CN02.AR01", + "Description": "When data is stored, it MUST be encrypted using the latest industry-standard encryption methods.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN02 Encrypt Data for Storage", + "SubSection": "", + "SubSectionObjective": "Ensure that all data stored is encrypted at rest using strong encryption algorithms.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "UEM-08", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_default_encryption", + "s3_bucket_kms_encryption", + "ec2_ebs_default_encryption", + "ec2_ebs_volume_encryption", + "ec2_ebs_snapshots_encrypted", + "efs_encryption_at_rest_enabled", + "storagegateway_fileshare_encryption_enabled", + "rds_instance_storage_encrypted", + "rds_cluster_storage_encrypted", + "rds_snapshots_encrypted", + "redshift_cluster_encrypted_at_rest", + "documentdb_cluster_storage_encrypted", + "neptune_cluster_storage_encrypted", + "neptune_cluster_snapshot_encrypted", + "dynamodb_tables_kms_cmk_encryption_enabled", + "dynamodb_accelerator_cluster_encryption_enabled", + "kafka_cluster_encryption_at_rest_uses_cmk", + "kinesis_stream_encrypted_at_rest", + "firehose_stream_encrypted_at_rest", + "sns_topics_kms_encryption_at_rest_enabled", + "sqs_queues_server_side_encryption_enabled", + "opensearch_service_domains_encryption_at_rest_enabled", + "athena_workgroup_encryption", + "glue_data_catalogs_metadata_encryption_enabled", + "glue_data_catalogs_connection_passwords_encryption_enabled", + "glue_etl_jobs_amazon_s3_encryption_enabled", + "backup_vaults_encrypted", + "backup_recovery_point_encrypted", + "cloudtrail_kms_encryption_enabled", + "cloudwatch_log_group_kms_encryption_enabled", + "eks_cluster_kms_cmk_encryption_in_secrets_enabled", + "sagemaker_notebook_instance_encryption_enabled", + "apigateway_restapi_cache_encrypted" + ] + }, + { + "Id": "CCC.Core.CN11.AR01", + "Description": "When encryption keys are used, the service MUST verify that all encryption keys use the latest industry-standard cryptographic algorithms.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "acm_certificates_with_secure_key_algorithms" + ] + }, + { + "Id": "CCC.Core.CN11.AR02", + "Description": "When encryption keys are used, the service MUST rotate active keys within 180 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "kms_cmk_rotation_enabled" + ] + }, + { + "Id": "CCC.Core.CN11.AR03", + "Description": "When encrypting data, the service MUST verify that customer-managed encryption keys (CMEKs) are used.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "kms_cmk_are_used" + ] + }, + { + "Id": "CCC.Core.CN11.AR04", + "Description": "When encryption keys are accessed, the service MUST verify that access to encryption keys is restricted to authorized personnel and services, following the principle of least privilege.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "iam_inline_policy_no_full_access_to_kms", + "iam_policy_no_full_access_to_kms", + "kms_cmk_not_deleted_unintentionally" + ] + }, + { + "Id": "CCC.Core.CN11.AR05", + "Description": "When encryption keys are used, the service MUST rotate active keys within 365 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-clear", + "tlp-green" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "kms_cmk_rotation_enabled" + ] + }, + { + "Id": "CCC.Core.CN11.AR06", + "Description": "When encryption keys are used, the service MUST rotate active keys within 90 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "kms_cmk_rotation_enabled" + ] + }, + { + "Id": "CCC.Core.CN14.AR01", + "Description": "When backups are created for disaster recovery purposes, the storage mechanism MUST NOT allow modification or deletion within 30 days of creation.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN14 Maintain Recent Backups", + "SubSection": "", + "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Use immutable storage solutions where possible. Implement backup retention policies that enforce a minimum retention period of 30 days.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "backup_vaults_exist", + "backup_plans_exist", + "rds_instance_backup_enabled", + "neptune_cluster_backup_enabled", + "documentdb_cluster_backup_enabled" + ] + }, + { + "Id": "CCC.Core.CN14.AR02", + "Description": "When backups are created for disaster recovery purposes, the most recent backup MUST have a creation date within the past 30 days.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN14 Maintain Recent Backups", + "SubSection": "", + "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber" + ], + "Recommendation": "Implement automated backup processes to ensure that backups are created regularly. Monitor backup schedules and verify that the most recent backup creation date is within the last 30 days.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "backup_vaults_exist", + "backup_plans_exist", + "backup_reportplans_exist", + "backup_recovery_point_encrypted", + "rds_instance_backup_enabled", + "rds_instance_protected_by_backup_plan", + "rds_cluster_protected_by_backup_plan", + "neptune_cluster_backup_enabled", + "documentdb_cluster_backup_enabled", + "dynamodb_table_protected_by_backup_plan", + "efs_have_backup_enabled" + ] + }, + { + "Id": "CCC.Core.CN14.AR03", + "Description": "When backups are created for disaster recovery purposes, the most recent backup MUST have a creation date within the past 14 days.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN14 Maintain Recent Backups", + "SubSection": "", + "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "Implement automated backup processes to ensure that backups are created regularly. Monitor backup schedules and verify that the most recent backup creation date is within the last 14 days.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "backup_vaults_exist", + "backup_plans_exist", + "rds_instance_backup_enabled", + "neptune_cluster_backup_enabled", + "documentdb_cluster_backup_enabled" + ] + }, + { + "Id": "CCC.Core.CN03.AR01", + "Description": "When an entity attempts to modify the service through a user interface, the authentication process MUST require multiple identifying factors for authentication.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "SubSection": "", + "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-14" + ] + } + ] + } + ], + "Checks": [ + "iam_root_mfa_enabled", + "iam_root_hardware_mfa_enabled", + "iam_user_mfa_enabled_console_access", + "iam_user_hardware_mfa_enabled", + "iam_administrator_access_with_mfa", + "cognito_user_pool_mfa_enabled" + ] + }, + { + "Id": "CCC.Core.CN03.AR02", + "Description": "When an entity attempts to modify the service through an API endpoint, the authentication process MUST require a credential such as an API key or token AND originate from within the trust perimeter.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "SubSection": "", + "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-14" + ] + } + ] + } + ], + "Checks": [ + "iam_no_root_access_key", + "iam_root_credentials_management_enabled", + "iam_user_no_setup_initial_access_key", + "iam_administrator_access_with_mfa", + "apigateway_restapi_authorizers_enabled", + "apigatewayv2_api_authorizers_enabled", + "apigateway_restapi_public_with_authorizer" + ] + }, + { + "Id": "CCC.Core.CN03.AR03", + "Description": "When an entity attempts to view information on the service through a user interface, the authentication process MUST require multiple identifying factors from the user.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "SubSection": "", + "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-14" + ] + } + ] + } + ], + "Checks": [ + "iam_user_mfa_enabled_console_access", + "iam_user_hardware_mfa_enabled", + "iam_root_mfa_enabled", + "iam_root_hardware_mfa_enabled", + "iam_administrator_access_with_mfa", + "cognito_user_pool_mfa_enabled" + ] + }, + { + "Id": "CCC.Core.CN03.AR04", + "Description": "When an entity attempts to view information on the service through an API endpoint, the authentication process MUST require a credential such as an API key or token AND originate from within the trust perimeter.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "SubSection": "", + "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-14" + ] + } + ] + } + ], + "Checks": [ + "iam_no_root_access_key", + "iam_user_no_setup_initial_access_key", + "apigateway_restapi_authorizers_enabled", + "apigatewayv2_api_authorizers_enabled", + "apigateway_restapi_public_with_authorizer" + ] + }, + { + "Id": "CCC.Core.CN05.AR01", + "Description": "When an attempt is made to modify data on the service or a child resource, the service MUST block requests from unauthorized entities.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "apigateway_restapi_authorizers_enabled", + "apigateway_restapi_public", + "apigateway_restapi_public_with_authorizer", + "apigatewayv2_api_authorizers_enabled", + "awslambda_function_url_public", + "awslambda_function_not_publicly_accessible", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "s3_bucket_public_access", + "s3_bucket_public_list_acl", + "s3_bucket_public_write_acl", + "s3_bucket_cross_account_access", + "s3_account_level_public_access_blocks", + "iam_policy_no_full_access_to_cloudtrail", + "iam_policy_no_full_access_to_kms", + "iam_inline_policy_no_full_access_to_cloudtrail", + "iam_inline_policy_no_full_access_to_kms", + "iam_role_administratoraccess_policy", + "iam_group_administrator_access_policy", + "iam_user_administrator_access_policy", + "iam_policy_attached_only_to_group_or_roles", + "iam_role_cross_account_readonlyaccess_policy", + "iam_role_cross_service_confused_deputy_prevention" + ] + }, + { + "Id": "CCC.Core.CN05.AR02", + "Description": "When administrative access or configuration change is attempted on the service or a child resource, the service MUST refuse requests from unauthorized entities.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "iam_root_mfa_enabled", + "iam_root_hardware_mfa_enabled", + "iam_avoid_root_usage", + "iam_user_mfa_enabled_console_access", + "iam_administrator_access_with_mfa", + "iam_group_administrator_access_policy", + "iam_role_administratoraccess_policy", + "iam_user_administrator_access_policy", + "iam_inline_policy_no_full_access_to_cloudtrail", + "iam_inline_policy_no_full_access_to_kms", + "iam_policy_no_full_access_to_cloudtrail", + "iam_policy_no_full_access_to_kms", + "iam_policy_allows_privilege_escalation", + "iam_inline_policy_allows_privilege_escalation", + "iam_customer_attached_policy_no_administrative_privileges", + "iam_customer_unattached_policy_no_administrative_privileges", + "iam_aws_attached_policy_no_administrative_privileges", + "iam_password_policy_minimum_length_14", + "iam_password_policy_uppercase", + "iam_password_policy_lowercase", + "iam_password_policy_symbol", + "iam_password_policy_number", + "iam_password_policy_expires_passwords_within_90_days_or_less", + "iam_password_policy_reuse_24" + ] + }, + { + "Id": "CCC.Core.CN05.AR03", + "Description": "When administrative access or configuration change is attempted on the service or a child resource in a multi-tenant environment, the service MUST refuse requests across tenant boundaries unless the origin is explicitly included in a pre-approved allowlist.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "vpc_endpoint_services_allowed_principals_trust_boundaries", + "vpc_endpoint_connections_trust_boundaries", + "iam_role_cross_service_confused_deputy_prevention", + "iam_role_cross_account_readonlyaccess_policy", + "s3_bucket_cross_account_access", + "eventbridge_bus_cross_account_access", + "eventbridge_schema_registry_cross_account_access" + ] + }, + { + "Id": "CCC.Core.CN05.AR04", + "Description": "When data is requested from outside the trust perimeter, the service MUST refuse requests from unauthorized entities.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "accessanalyzer_enabled", + "accessanalyzer_enabled_without_findings", + "vpc_endpoint_connections_trust_boundaries", + "vpc_endpoint_services_allowed_principals_trust_boundaries", + "s3_bucket_cross_account_access", + "s3_bucket_public_access", + "iam_administrator_access_with_mfa", + "iam_inline_policy_no_full_access_to_kms", + "iam_inline_policy_no_full_access_to_cloudtrail", + "iam_policy_no_full_access_to_kms", + "iam_policy_no_full_access_to_cloudtrail", + "iam_policy_attached_only_to_group_or_roles" + ] + }, + { + "Id": "CCC.Core.CN05.AR05", + "Description": "When any request is made from outside the trust perimeter, the service MUST NOT provide any response that may indicate the service exists.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "awslambda_function_url_public", + "apigateway_restapi_public", + "s3_bucket_public_access", + "s3_bucket_policy_public_write_access", + "sns_topics_not_publicly_accessible", + "sqs_queues_not_publicly_accessible", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_networkacl_allow_ingress_any_port", + "ec2_securitygroup_default_restrict_traffic", + "vpc_endpoint_for_ec2_enabled", + "vpc_endpoint_connections_trust_boundaries", + "vpc_endpoint_services_allowed_principals_trust_boundaries" + ] + }, + { + "Id": "CCC.Core.CN05.AR06", + "Description": "When any request is made to the service or a child resource, the service MUST refuse requests from unauthorized entities.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "iam_root_mfa_enabled", + "iam_root_hardware_mfa_enabled", + "iam_no_root_access_key", + "iam_administrator_access_with_mfa", + "iam_user_mfa_enabled_console_access", + "iam_user_hardware_mfa_enabled", + "iam_root_credentials_management_enabled", + "iam_check_saml_providers_sts", + "iam_policy_attached_only_to_group_or_roles", + "iam_role_cross_service_confused_deputy_prevention", + "iam_role_cross_account_readonlyaccess_policy", + "vpc_endpoint_connections_trust_boundaries", + "vpc_endpoint_services_allowed_principals_trust_boundaries" + ] + }, + { + "Id": "CCC.Core.CN04.AR01", + "Description": "When administrative access or configuration change is attempted on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN04 Log All Access and Changes", + "SubSection": "", + "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-08" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_multi_region_enabled", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_cloudwatch_logging_enabled", + "cloudtrail_log_file_validation_enabled", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_log_metric_filter_root_usage", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", + "cloudwatch_log_metric_filter_policy_changes", + "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", + "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", + "cloudwatch_changes_to_network_acls_alarm_configured", + "cloudwatch_changes_to_network_gateways_alarm_configured", + "cloudwatch_changes_to_network_route_tables_alarm_configured", + "cloudwatch_changes_to_vpcs_alarm_configured", + "config_recorder_all_regions_enabled" + ] + }, + { + "Id": "CCC.Core.CN04.AR02", + "Description": "When any attempt is made to modify data on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN04 Log All Access and Changes", + "SubSection": "", + "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-08" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_s3_dataevents_write_enabled", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_cloudwatch_logging_enabled", + "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", + "vpc_flow_logs_enabled", + "apigateway_restapi_logging_enabled", + "apigatewayv2_api_access_logging_enabled" + ] + }, + { + "Id": "CCC.Core.CN04.AR03", + "Description": "When any attempt is made to read data on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN04 Log All Access and Changes", + "SubSection": "", + "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-08" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_s3_dataevents_read_enabled", + "cloudtrail_insights_exist", + "cloudtrail_cloudwatch_logging_enabled", + "cloudtrail_multi_region_enabled", + "apigateway_restapi_logging_enabled", + "apigatewayv2_api_access_logging_enabled", + "s3_bucket_server_access_logging_enabled" + ] + }, + { + "Id": "CCC.Core.CN07.AR01", + "Description": "When enumeration activities are detected, the service MUST publish an event to a monitored channel which includes the client identity, time, and nature of the activity.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN07 Alert on Unusual Enumeration Activity", + "SubSection": "", + "SubSectionObjective": "Ensure that logs and associated alerts are generated when unusual enumeration activity is detected that may indicate reconnaissance activities.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Implement event publication mechanisms and alerts for patterns indicative of enumeration activities, such as repeated access attempts, requests, or liveness probes. Configure alerts to notify security teams of any activities that merit further investigation.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH15" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-05", + "SEF-05" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_threat_detection_enumeration", + "guardduty_is_enabled", + "guardduty_no_high_severity_findings" + ] + }, + { + "Id": "CCC.Core.CN07.AR02", + "Description": "When enumeration activities are detected, the service MUST log the client identity, time, and nature of the activity.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN07 Alert on Unusual Enumeration Activity", + "SubSection": "", + "SubSectionObjective": "Ensure that logs and associated alerts are generated when unusual enumeration activity is detected that may indicate reconnaissance activities.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Implement logging mechanisms to capture details of enumeration activities, including client identity, timestamps, and activity nature. Retain logs according to organizational policies, and occasionally review them for patterns that may indicate reconnaissance activities.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH15" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-05", + "SEF-05" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_threat_detection_enumeration" + ] + }, { "Id": "CCC.AuditLog.CN01.AR01", "Description": "When the signature validation process is performed, then it MUST detect any modification of data.", @@ -18,13 +1843,13 @@ "Applicability": [ "tlp-red" ], - "Recommendation": "Ensure hash of data is included in digital signature. ", + "Recommendation": "Ensure hash of data is included in digital signature.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06", - "CCC.TH07" + "CCC.Core.TH06", + "CCC.Core.TH07" ] } ], @@ -61,13 +1886,13 @@ "Applicability": [ "tlp-red" ], - "Recommendation": "Ensure verification process includes a chained hash function. ", + "Recommendation": "Ensure verification process includes a chained hash function.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06", - "CCC.TH07" + "CCC.Core.TH06", + "CCC.Core.TH07" ] } ], @@ -110,7 +1935,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06" + "CCC.Core.TH06" ] } ], @@ -135,17 +1960,10 @@ "Checks": [ "cloudtrail_multi_region_enabled", "cloudtrail_multi_region_enabled_logging_management_events", - "cloudtrail_insights_exist", - "cloudtrail_log_file_validation_enabled", - "cloudtrail_kms_encryption_enabled", "cloudtrail_cloudwatch_logging_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_logs_s3_bucket_access_logging_enabled", "cloudtrail_s3_dataevents_read_enabled", "cloudtrail_s3_dataevents_write_enabled", - "cloudtrail_threat_detection_enumeration", - "cloudtrail_threat_detection_llm_jacking", - "cloudtrail_threat_detection_privilege_escalation" + "cloudtrail_insights_exist" ] }, { @@ -162,12 +1980,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Ensure alerting is correctly configured ", + "Recommendation": "Ensure alerting is correctly configured", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -189,14 +2007,7 @@ } ], "Checks": [ - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "cloudtrail_log_file_validation_enabled", - "cloudtrail_kms_encryption_enabled", - "cloudtrail_cloudwatch_logging_enabled", - "cloudtrail_multi_region_enabled", - "cloudtrail_insights_exist", - "s3_bucket_object_lock", - "cloudtrail_multi_region_enabled_logging_management_events" + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled" ] }, { @@ -213,12 +2024,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Ensure alerting is correctly configured ", + "Recommendation": "Ensure alerting is correctly configured", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -240,17 +2051,7 @@ } ], "Checks": [ - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "cloudwatch_changes_to_network_acls_alarm_configured", - "cloudwatch_changes_to_network_gateways_alarm_configured", - "cloudwatch_changes_to_network_route_tables_alarm_configured", - "cloudwatch_changes_to_vpcs_alarm_configured", - "cloudwatch_log_metric_filter_policy_changes", - "cloudwatch_log_metric_filter_aws_organizations_changes", - "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", - "cloudwatch_log_metric_filter_security_group_changes", - "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", - "cloudwatch_log_metric_filter_unauthorized_api_calls" + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes" ] }, { @@ -267,13 +2068,13 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Configure the audit log bucket to enable server access logging. Ensure the target logging bucket is configured for appropriate security, including restricted access and immutability. ", + "Recommendation": "Configure the audit log bucket to enable server access logging. Ensure the target logging bucket is configured for appropriate security, including restricted access and immutability.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01", - "CCC.TH09" + "CCC.Core.TH01", + "CCC.Core.TH09" ] } ], @@ -296,9 +2097,6 @@ ], "Checks": [ "cloudtrail_logs_s3_bucket_access_logging_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_bucket_requires_mfa_delete", - "cloudtrail_kms_encryption_enabled", "s3_bucket_server_access_logging_enabled" ] }, @@ -316,12 +2114,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Configure audit log exporting. ", + "Recommendation": "Configure audit log exporting.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -344,16 +2142,10 @@ } ], "Checks": [ - "cloudtrail_kms_encryption_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_logs_s3_bucket_access_logging_enabled", - "cloudtrail_bucket_requires_mfa_delete", + "cloudtrail_multi_region_enabled", "cloudtrail_s3_dataevents_read_enabled", "cloudtrail_s3_dataevents_write_enabled", - "cloudtrail_multi_region_enabled", - "cloudtrail_multi_region_enabled_logging_management_events", - "s3_bucket_cross_region_replication", - "s3_bucket_cross_account_access" + "s3_bucket_cross_region_replication" ] }, { @@ -371,13 +2163,13 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Configure the audit log bucket's lifecycle rules or object retention settings to enforce the required data retention period. ", + "Recommendation": "Configure the audit log bucket's lifecycle rules or object retention settings to enforce the required data retention period.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06", - "CCC.TH07" + "CCC.Core.TH06", + "CCC.Core.TH07" ] } ], @@ -417,13 +2209,13 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Enable MFA Delete (or equivalent multi-factor authentication for delete operations) on the audit log bucket. ", + "Recommendation": "Enable MFA Delete (or equivalent multi-factor authentication for delete operations) on the audit log bucket.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06", - "CCC.TH07" + "CCC.Core.TH06", + "CCC.Core.TH07" ] } ], @@ -462,12 +2254,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Configure object lock policy. ", + "Recommendation": "Configure object lock policy.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -506,12 +2298,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Review field level access controls on audit data. ", + "Recommendation": "Review field level access controls on audit data.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -537,11 +2329,8 @@ } ], "Checks": [ - "cloudtrail_kms_encryption_enabled", "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_bucket_requires_mfa_delete", - "cloudwatch_log_group_not_publicly_accessible", - "s3_bucket_public_access" + "cloudwatch_log_group_not_publicly_accessible" ] }, { @@ -559,12 +2348,12 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted. ", + "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -587,7 +2376,7 @@ ], "Checks": [ "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_kms_encryption_enabled", + "s3_bucket_public_access", "s3_bucket_public_list_acl", "s3_bucket_public_write_acl" ] @@ -607,12 +2396,12 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted. ", + "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -635,220 +2424,34 @@ ], "Checks": [ "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "s3_bucket_public_write_acl", - "s3_bucket_public_list_acl", "s3_bucket_public_access", + "s3_bucket_public_list_acl", + "s3_bucket_public_write_acl", "s3_bucket_policy_public_write_access" ] }, { - "Id": "CCC.Build.CN01.AR01", - "Description": "Attempt to initiate a build using an unauthorized build agent and verify that the build is rejected.", - "Attributes": [ - { - "FamilyName": "Access Control", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.Build.CN01 Restrict Allowed Build Agents", - "SubSection": "", - "SubSectionObjective": "Ensure that builds are executed only on authorized build agents to maintain control over the build environment and prevent unauthorized code execution.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "AC-6" - ] - } - ] - } - ], - "Checks": [ - "codebuild_project_user_controlled_buildspec", - "codebuild_project_source_repo_url_no_sensitive_credentials", - "codebuild_project_uses_allowed_github_organizations", - "codebuild_project_not_publicly_accessible", - "codebuild_project_logging_enabled", - "codebuild_project_s3_logs_encrypted", - "codebuild_project_no_secrets_in_variables", - "codebuild_project_older_90_days" - ] - }, - { - "Id": "CCC.Build.CN02.AR01", - "Description": "Attempt to trigger a build from an unauthorized external service or repository and verify that the build does not start.", - "Attributes": [ - { - "FamilyName": "Access Control", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.Build.CN02 Restrict Allowed External Services for Build Triggers", - "SubSection": "", - "SubSectionObjective": "Ensure that builds can only be triggered by authorized external services or repositories to prevent unauthorized code execution or tampering.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "AC-6" - ] - } - ] - } - ], - "Checks": [ - "codebuild_project_uses_allowed_github_organizations", - "codebuild_project_user_controlled_buildspec", - "codebuild_project_source_repo_url_no_sensitive_credentials", - "codebuild_project_not_publicly_accessible" - ] - }, - { - "Id": "CCC.Build.CN03.AR01", - "Description": "Attempt to access the build environment from an external network and verify that access is denied.", - "Attributes": [ - { - "FamilyName": "Network Security", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.Build.CN03 Deny External Network Access for Build Environments", - "SubSection": "", - "SubSectionObjective": "Ensure that build environments do not have external network access to prevent unauthorized external access and data exfiltration.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02", - "CCC.TH05" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-5" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-7", - "SC-5" - ] - } - ] - } - ], - "Checks": [ - "codebuild_project_not_publicly_accessible" - ] - }, - { - "Id": "CCC.CntrReg.CN01.AR01", - "Description": "Attempt to push an artifact with known vulnerabilities to the registry and observe if it is flagged or rejected by the vulnerability scanning process.", - "Attributes": [ - { - "FamilyName": "Risk Management", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CntrReg.CN01 Implement Vulnerability Scanning for Artifacts", - "SubSection": "", - "SubSectionObjective": "Ensure that container images and artifacts stored in the container registry are scanned for vulnerabilities to identify and remediate security issues before deployment.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.CntrReg.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "ID.RA-1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "RA-5", - "SI-5" - ] - } - ] - } - ], - "Checks": [ - "ecr_registry_scan_images_on_push_enabled", - "ecr_repositories_scan_vulnerabilities_in_latest_image" - ] - }, - { - "Id": "CCC.DataWar.CN01.AR01", - "Description": "Attempt to access underlying database tables directly without using managed views and verify that access is denied.", + "Id": "CCC.Logging.CN01.AR01", + "Description": "When a new cloud account is created, provider-level audit and network flow logging MUST be enabled by default and directed to the central sink.", "Attributes": [ { "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.DataWar.CN01 Enforce Use of Managed Views for Data Access", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", + "Section": "CCC.Logging.CN01 Centralized and Comprehensive Log Aggregation", "SubSection": "", - "SubSectionObjective": "Ensure that data access is provided through managed views, restricting users from accessing underlying tables directly and enforcing consistent security policies.", + "SubSectionObjective": "Ensure all operational and security logs from across the cloud environment, including applications, operating systems, network traffic, and cloud service activity, are captured automatically and streamed to a central, secure log management service.", "Applicability": [ - "tlp-red", - "tlp-amber" + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Logging.TH07" ] } ], @@ -856,14 +2459,490 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.AC-4" + "PR.PS-04" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-2", + "AU-3" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_multi_region_enabled", + "cloudtrail_multi_region_enabled_logging_management_events", + "vpc_flow_logs_enabled" + ] + }, + { + "Id": "CCC.Logging.CN01.AR02", + "Description": "When a new cloud compute resource is deployed, it MUST be configured to forward all relevant logs (e.g., OS, application, service logs) to the central log sink.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", + "Section": "CCC.Logging.CN01 Centralized and Comprehensive Log Aggregation", + "SubSection": "", + "SubSectionObjective": "Ensure all operational and security logs from across the cloud environment, including applications, operating systems, network traffic, and cloud service activity, are captured automatically and streamed to a central, secure log management service.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Logging.TH07" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.PS-04" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-2", + "AU-3" + ] + } + ] + } + ], + "Checks": [ + "vpc_flow_logs_enabled", + "cloudtrail_cloudwatch_logging_enabled", + "apigateway_restapi_logging_enabled", + "apigatewayv2_api_access_logging_enabled", + "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled" + ] + }, + { + "Id": "CCC.Logging.CN02.AR01", + "Description": "When a new log bucket or stream is created, its retention policy MUST be configured in accordance with organisation's data retention policy.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", + "Section": "CCC.Logging.CN02 Enforce Data Retention Policy for Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that the retention period configured for logs aligns with the organization's data retention policy.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Logging.TH05" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "GV.PO-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-11" + ] + } + ] + } + ], + "Checks": [ + "cloudwatch_log_group_retention_policy_specific_days_enabled" + ] + }, + { + "Id": "CCC.Logging.CN02.AR02", + "Description": "When a query is performed to retrieve log events older than the number of days defined in the organisation's data retention policy, it MUST return an empty result.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", + "Section": "CCC.Logging.CN02 Enforce Data Retention Policy for Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that the retention period configured for logs aligns with the organization's data retention policy.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Logging.TH05" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "GV.PO-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-11" + ] + } + ] + } + ], + "Checks": [ + "cloudwatch_log_group_retention_policy_specific_days_enabled" + ] + }, + { + "Id": "CCC.Logging.CN03.AR01", + "Description": "When an attempt is made to modify or delete data before the object lock period expires, then the action MUST be denied.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", + "Section": "CCC.Logging.CN03 Enable Object Lock On Log Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure log immutability by enabling Write Once, Read Many (WORM) protection using object lock on log storage buckets. This prevents logs from being modified or deleted during the defined retention period, supporting compliance and forensic integrity.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "Configure object lock policy.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH07" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.PS-04" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-9", + "AU-11" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_object_lock" + ] + }, + { + "Id": "CCC.Logging.CN04.AR01", + "Description": "When restricted fields are accessed by unauthorized users, then those fields MUST remain masked.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify logs.", + "Section": "CCC.Logging.CN04 Restrict Field And Log Type Access", + "SubSection": "", + "SubSectionObjective": "Configure access to logs to follow the principle of least privilege in particular where technically possible limit the log fields users have access to to prevent accidental exposure to sensitive information such as PII.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "Review field level access controls on log data.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Logging.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.PS-04" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6", + "AU-9", + "AC-3", + "PT-2", + "PT-3", + "PT-3" + ] + } + ] + } + ], + "Checks": [ + "cloudwatch_log_group_not_publicly_accessible", + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible" + ] + }, + { + "Id": "CCC.Logging.CN05.AR01", + "Description": "When a log storage bucket is created, the bucket's access control settings MUST explicitly deny public read and write access.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify logs.", + "Section": "CCC.Logging.CN05 Ensure Log Bucket is Not Publicly Accessible", + "SubSection": "", + "SubSectionObjective": "Ensure that log storage buckets are not publicly accessible to prevent unauthorized access to sensitive log data. In addition, logs should be replicated to another cloud region to enhance availability, durability, and support disaster recovery requirements.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green" + ], + "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-05" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ "AC-3", - "AC-6" + "SC-7" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", + "s3_bucket_public_access", + "s3_bucket_public_list_acl", + "s3_bucket_public_write_acl", + "s3_account_level_public_access_blocks", + "s3_bucket_level_public_access_block" + ] + }, + { + "Id": "CCC.Logging.CN05.AR02", + "Description": "When the URL of a log storage bucket's object is accessed publicly, the action MUST be denied by bucket policy.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify logs.", + "Section": "CCC.Logging.CN05 Ensure Log Bucket is Not Publicly Accessible", + "SubSection": "", + "SubSectionObjective": "Ensure that log storage buckets are not publicly accessible to prevent unauthorized access to sensitive log data. In addition, logs should be replicated to another cloud region to enhance availability, durability, and support disaster recovery requirements.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green" + ], + "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", + "SC-7" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_public_access", + "s3_bucket_public_list_acl", + "s3_bucket_public_write_acl", + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", + "s3_account_level_public_access_blocks" + ] + }, + { + "Id": "CCC.Logging.CN06.AR01", + "Description": "When a single principal executes an anomalously high number of log queries, an alert MUST be generated.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain logging-related events.", + "Section": "CCC.Logging.CN06 Detect and Alert on Potential Log Exfiltration", + "SubSection": "", + "SubSectionObjective": "Identify and alert on anomalous data access patterns that may indicate an attempt to exfiltrate log data.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Logging.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-03", + "DE.CM-09" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4", + "CA-7", + "AU-6" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_threat_detection_enumeration" + ] + }, + { + "Id": "CCC.Logging.CN07.AR01", + "Description": "When an audit log event is recorded that corresponds to a modification of the logging service configuration such as disabling a log trail, deleting a log sink, or altering a log forwarding rule, an alert MUST be generated.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain logging-related events.", + "Section": "CCC.Logging.CN07 Detect and Alert on Log Service Tampering", + "SubSection": "", + "SubSectionObjective": "Alert when any component of the critical logging infrastructure is disabled, modified, or deleted, indicating a defense evasion attempt.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-03", + "DE.CM-09" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4", + "CA-7", + "AU-6" + ] + } + ] + } + ], + "Checks": [ + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled" + ] + }, + { + "Id": "CCC.Monitor.CN01.AR01", + "Description": "When an External Monitoring system exceeds the anticipated rate of monitoring checks then Rate Limiting MUST be applied and an Audit Alert MUST be generated.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain events from other monitoring services.", + "Section": "CCC.Monitor.CN01 Rate Limiting on External Monitoring", + "SubSection": "", + "SubSectionObjective": "Prevent DoS attacks using External Monitoring tools.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Monitor.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IR-01", + "DE.CM-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-5", + "SC-7" ] } ] @@ -872,25 +2951,27 @@ "Checks": [] }, { - "Id": "CCC.DataWar.CN02.AR01", - "Description": "Attempt to query sensitive columns without the necessary permissions and verify that access is denied or data is masked.", + "Id": "CCC.Monitor.CN02.AR01", + "Description": "When an Custom or User-Defined Metric starts to flood a collector, then a rate limit MUST be applied to reduce the network impact of traffic and an alert must triggered.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.DataWar.CN02 Enforce Column-Level Security Policies", + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain events from other monitoring services.", + "Section": "CCC.Monitor.CN02 Rate Limiting on Metric Generation", "SubSection": "", - "SubSectionObjective": "Ensure that access to sensitive data columns is restricted based on user roles, preventing unauthorized access to sensitive information.", + "SubSectionObjective": "Prevent Malicious Actor or misconfiguration from flooding services with metric data.", "Applicability": [ - "tlp-red", - "tlp-amber" + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Monitor.TH06" ] } ], @@ -898,14 +2979,15 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.AC-4" + "DE.CM-01" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AC-3", - "AC-6" + "SC-5(2)", + "CA-7", + "SI-4" ] } ] @@ -914,25 +2996,27 @@ "Checks": [] }, { - "Id": "CCC.DataWar.CN03.AR01", - "Description": "Attempt to query data rows that the user should not have access to and verify that access is denied or data is not returned.", + "Id": "CCC.Monitor.CN03.AR01", + "Description": "When external systems have approved access to internal systems not normally available for public access then they MUST be secured to prevent unauthorised access jumping through to the internal systems and only allow access to specific internal services.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.DataWar.CN03 Enforce Row-Level Security Policies", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", + "Section": "CCC.Monitor.CN03 Access External Monitoring", "SubSection": "", - "SubSectionObjective": "Ensure that access to data rows is restricted based on user roles or attributes, preventing unauthorized access to specific subsets of data.", + "SubSectionObjective": "Control access to Synthetic monitoring solutions using API keys or Certificate based authentication to ensure they don't become an attack path, preventing monitoring systems from forging network requests to gain access to internal systems.", "Applicability": [ - "tlp-red", - "tlp-amber" + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Monitor.TH04" ] } ], @@ -940,14 +3024,111 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.AC-4" + "DE.CM-06", + "PR.IR-01", + "PR.AA-05" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AC-3", - "AC-6" + "AC-3" + ] + } + ] + } + ], + "Checks": [ + "apigateway_restapi_authorizers_enabled", + "apigateway_restapi_client_certificate_enabled", + "apigatewayv2_api_authorizers_enabled", + "apigateway_restapi_public_with_authorizer" + ] + }, + { + "Id": "CCC.Monitor.CN04.AR01", + "Description": "When monitoring dashboards display degraded services which may become potential targets then the dashboard MUST be protected from unauthorised access.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", + "Section": "CCC.Monitor.CN04 Restrict access to Monitoring Dashboards", + "SubSection": "", + "SubSectionObjective": "Control access to Monitoring Dashboards and reports to ensure they don't highlight an attack path.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Monitor.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-09", + "DE.AE-03" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4", + "AC-3" + ] + } + ] + } + ], + "Checks": [ + "cloudwatch_log_group_not_publicly_accessible" + ] + }, + { + "Id": "CCC.Monitor.CN05.AR01", + "Description": "When monitoring services have generated an alert, the service MUST ensure only authorised responders silence or acknowledge the alert.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", + "Section": "CCC.Monitor.CN05 Restrict access to silence or acknowledge an alert", + "SubSection": "", + "SubSectionObjective": "Ensure only a subset of users can silence or acknowledge alerts to prevent attackers hiding their activity.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH10" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IR-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3" ] } ] @@ -956,179 +3137,762 @@ "Checks": [] }, { - "Id": "CCC.KeyMgmt.CN01.AR01", - "Description": "When a key version is scheduled for deletion or disabled, an alert MUST be generated within five minutes.", + "Id": "CCC.Monitor.CN06.AR01", + "Description": "When systems push metrics or traces they MUST be authenticated for that particular type of metric or trace", "Attributes": [ { - "FamilyName": "Logging and Metrics Publication", - "FamilyDescription": "Controls that collect, alert, and retain key-management events.", - "Section": "CCC.KeyMgmt.CN01 Alert on Key-version Changes", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", + "Section": "CCC.Monitor.CN06 Metrics pushed for authorised services only", "SubSection": "", - "SubSectionObjective": "Generate near-real-time alerts when a KMS key version is disabled or scheduled for deletion, enabling rapid investigation and recovery.", + "SubSectionObjective": "Use IAM to control which types of metrics or traces can be pushed by different system to avoid a compromised system pushing fabricated metrics about a different service", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Monitor.TH05" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-5" + ] + } + ] + } + ], + "Checks": [ + "apigateway_restapi_authorizers_enabled", + "apigatewayv2_api_authorizers_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN01.AR01", + "Description": "When a request is made to read a bucket, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", + "SubSection": "", + "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption, or sensitive data decryption.", "Applicability": [ "tlp-amber", "tlp-red" ], - "Recommendation": "Use native event services (e.g., CloudWatch Events, Azure Monitor, Cloud Audit Logs) to route notifications to an incident-response channel.", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.KeyMgmt.TH01" + "CCC.Core.TH01", + "CCC.Core.TH06" ] } ], "SectionGuidelineMappings": [ { - "ReferenceId": "NIST-CSF", + "ReferenceId": "CCM", "Identifiers": [ - "RS.AN-1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "IR-5" + "IAM-01", + "IAM-03", + "DSP-17" ] } ] } ], "Checks": [ - "kms_cmk_not_deleted_unintentionally" + "s3_bucket_kms_encryption", + "iam_policy_no_full_access_to_kms", + "iam_inline_policy_no_full_access_to_kms", + "kms_key_not_publicly_accessible" ] }, { - "Id": "CCC.KeyMgmt.CN02.AR01", - "Description": "When IAM roles and key policies are reviewed, Decrypt permission MUST be granted exclusively to documented authorised principals.", + "Id": "CCC.ObjStor.CN01.AR02", + "Description": "When a request is made to read an object, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", + "SubSection": "", + "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption, or sensitive data decryption.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-03", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_kms_encryption", + "iam_policy_no_full_access_to_kms", + "iam_inline_policy_no_full_access_to_kms", + "kms_key_not_publicly_accessible" + ] + }, + { + "Id": "CCC.ObjStor.CN01.AR03", + "Description": "When a request is made to write to a bucket, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", + "SubSection": "", + "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption, or sensitive data decryption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-03", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_kms_encryption", + "iam_policy_no_full_access_to_kms", + "iam_inline_policy_no_full_access_to_kms", + "kms_key_not_publicly_accessible" + ] + }, + { + "Id": "CCC.ObjStor.CN01.AR04", + "Description": "When a request is made to write to an object, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", + "SubSection": "", + "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption, or sensitive data decryption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-03", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_kms_encryption", + "iam_policy_no_full_access_to_kms", + "iam_inline_policy_no_full_access_to_kms", + "kms_key_not_publicly_accessible" + ] + }, + { + "Id": "CCC.ObjStor.CN03.AR01", + "Description": "When an object storage bucket deletion is attempted, the bucket MUST be fully recoverable for a set time-frame after deletion is requested.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN03 Prevent Bucket Deletion Through Irrevocable Bucket Retention Policy", + "SubSection": "", + "SubSectionObjective": "Ensure that object storage bucket is not deleted after creation, and that the preventative measure cannot be unset.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_object_lock", + "s3_bucket_object_versioning" + ] + }, + { + "Id": "CCC.ObjStor.CN03.AR02", + "Description": "When an attempt is made to modify the retention policy for an object storage bucket, the service MUST prevent the policy from being modified.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN03 Prevent Bucket Deletion Through Irrevocable Bucket Retention Policy", + "SubSection": "", + "SubSectionObjective": "Ensure that object storage bucket is not deleted after creation, and that the preventative measure cannot be unset.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_object_lock" + ] + }, + { + "Id": "CCC.ObjStor.CN04.AR01", + "Description": "When an object is uploaded to the object storage system, the object MUST automatically receive a default retention policy that prevents premature deletion or modification.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN04 Objects have an Effective Retention Policy by Default", + "SubSection": "", + "SubSectionObjective": "Ensure that all objects stored in the object storage system have a retention policy applied by default, preventing premature deletion or modification of objects.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + }, + { + "ReferenceId": "CCC.ObjStor", + "Identifiers": [ + "CCC.ObjStor.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_object_lock" + ] + }, + { + "Id": "CCC.ObjStor.CN04.AR02", + "Description": "When an attempt is made to delete or modify an object that is subject to an active retention policy, the service MUST prevent the action from being completed.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN04 Objects have an Effective Retention Policy by Default", + "SubSection": "", + "SubSectionObjective": "Ensure that all objects stored in the object storage system have a retention policy applied by default, preventing premature deletion or modification of objects.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + }, + { + "ReferenceId": "CCC.ObjStor", + "Identifiers": [ + "CCC.ObjStor.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_object_lock" + ] + }, + { + "Id": "CCC.ObjStor.CN05.AR01", + "Description": "When an object is uploaded to the object storage bucket, the object MUST be stored with a unique identifier.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN05 Versioning is Enabled for All Objects in the Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_object_versioning" + ] + }, + { + "Id": "CCC.ObjStor.CN05.AR02", + "Description": "When an object is modified, the service MUST assign a new unique identifier to the modified object to differentiate it from the previous version.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN05 Versioning is Enabled for All Objects in the Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_object_versioning" + ] + }, + { + "Id": "CCC.ObjStor.CN05.AR03", + "Description": "When an object is modified, the service MUST allow for recovery of previous versions of the object.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN05 Versioning is Enabled for All Objects in the Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_object_versioning" + ] + }, + { + "Id": "CCC.ObjStor.CN05.AR04", + "Description": "When an object is deleted, the service MUST retain other versions of the object to allow for recovery of previous versions.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN05 Versioning is Enabled for All Objects in the Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_object_versioning" + ] + }, + { + "Id": "CCC.ObjStor.CN07.AR01", + "Description": "The object storage service MUST support a configuration option that requires MFA to be successfully completed before any object deletion can be attempted, regardless of the request interface.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN07 Multi-Factor Authentication Is Required for Object Deletion", + "SubSection": "", + "SubSectionObjective": "Ensure that deletion of objects stored in the object storage system is protected by multi-factor authentication (MFA), reducing the risk of accidental, unauthorized, or compromised-credential–based data destruction.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06", + "CCC.Core.TH17" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "IAM-12" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_no_mfa_delete" + ] + }, + { + "Id": "CCC.ObjStor.CN07.AR02", + "Description": "When MFA deletion protection is enabled on a bucket or object namespace, the service MUST deny any deletion request from an identity that has not satisfied the MFA requirement at the time of the request.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN07 Multi-Factor Authentication Is Required for Object Deletion", + "SubSection": "", + "SubSectionObjective": "Ensure that deletion of objects stored in the object storage system is protected by multi-factor authentication (MFA), reducing the risk of accidental, unauthorized, or compromised-credential–based data destruction.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06", + "CCC.Core.TH17" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "IAM-12" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_no_mfa_delete" + ] + }, + { + "Id": "CCC.ObjStor.CN07.AR03", + "Description": "When an attempt is made to delete an object, the service's audit logs MUST clearly record each deletion attempt, including whether MFA was required and whether validation was met.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN07 Multi-Factor Authentication Is Required for Object Deletion", + "SubSection": "", + "SubSectionObjective": "Ensure that deletion of objects stored in the object storage system is protected by multi-factor authentication (MFA), reducing the risk of accidental, unauthorized, or compromised-credential–based data destruction.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06", + "CCC.Core.TH17" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "IAM-12" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_no_mfa_delete" + ] + }, + { + "Id": "CCC.ObjStor.CN02.AR01", + "Description": "When a permission set is allowed for an object in a bucket, the service MUST allow the same permission set to access all objects in the same bucket.", "Attributes": [ { "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that enforce least-privilege use of KMS operations.", - "Section": "CCC.KeyMgmt.CN02 Limit Decrypt Permissions", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources.", + "Section": "CCC.ObjStor.CN02 Enforce Uniform Bucket-level Access to Prevent Inconsistent Permissions", "SubSection": "", - "SubSectionObjective": "Restrict the Decrypt operation to authorised principals only, applying the principle of least privilege to protect sensitive data.", + "SubSectionObjective": "Ensure that uniform bucket-level access is enforced across all object storage buckets. This prevents the use of ad-hoc or inconsistent object-level permissions, ensuring centralized, consistent, and secure access management in accordance with the principle of least privilege.", "Applicability": [ - "tlp-green" + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" ], - "Recommendation": "Periodically audit policy documents via automated tooling and report any deviations.", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.KeyMgmt.TH02" + "CCC.Core.TH01" ] } ], "SectionGuidelineMappings": [ { - "ReferenceId": "NIST-CSF", + "ReferenceId": "CCM", "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-6" + "IAM-08" ] } ] } ], "Checks": [ - "kms_cmk_not_deleted_unintentionally", - "kms_cmk_not_multi_region", - "kms_cmk_rotation_enabled", - "kms_cmk_are_used", - "iam_inline_policy_no_full_access_to_kms" + "s3_bucket_acl_prohibited" ] }, { - "Id": "CCC.KeyMgmt.CN03.AR01", - "Description": "When rotation settings are examined, rotation MUST be enabled with an interval not exceeding 365 days.", + "Id": "CCC.ObjStor.CN02.AR02", + "Description": "When a permission set is denied for an object in a bucket, the service MUST deny the same permission set to access all objects in the same bucket.", "Attributes": [ { - "FamilyName": "Key Lifecycle Management", - "FamilyDescription": "Controls that govern creation, rotation, import, and retirement of cryptographic keys.", - "Section": "CCC.KeyMgmt.CN03 Enforce Automatic Rotation", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources.", + "Section": "CCC.ObjStor.CN02 Enforce Uniform Bucket-level Access to Prevent Inconsistent Permissions", "SubSection": "", - "SubSectionObjective": "Ensure symmetric keys rotate automatically within policy intervals to reduce exposure of key material.", + "SubSectionObjective": "Ensure that uniform bucket-level access is enforced across all object storage buckets. This prevents the use of ad-hoc or inconsistent object-level permissions, ensuring centralized, consistent, and secure access management in accordance with the principle of least privilege.", "Applicability": [ - "tlp-green" + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" ], - "Recommendation": "Use cloud-provider rotation features and verify via configuration scanning.", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.KeyMgmt.TH03" + "CCC.Core.TH01" ] } ], "SectionGuidelineMappings": [ { - "ReferenceId": "NIST-CSF", + "ReferenceId": "CCM", "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-12" + "IAM-08" ] } ] } ], "Checks": [ - "kms_cmk_rotation_enabled" - ] - }, - { - "Id": "CCC.KeyMgmt.CN04.AR01", - "Description": "When a key import request is processed, the key MUST use an approved algorithm (RSA-2048+, EC-P256+) and originate from a certified HSM.", - "Attributes": [ - { - "FamilyName": "Key Lifecycle Management", - "FamilyDescription": "Controls that govern creation, rotation, import, and retirement of cryptographic keys.", - "Section": "CCC.KeyMgmt.CN04 Validate Imported Keys", - "SubSection": "", - "SubSectionObjective": "Accept only externally generated keys that meet approved cryptographic strength and provenance requirements.", - "Applicability": [ - "tlp-green" - ], - "Recommendation": "Implement an approval workflow that validates attestation data before import.", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.KeyMgmt.TH04" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "kms_cmk_not_deleted_unintentionally", - "kms_cmk_rotation_enabled", - "kms_cmk_not_multi_region", - "kms_cmk_are_used" + "s3_bucket_acl_prohibited" ] }, { @@ -1136,8 +3900,8 @@ "Description": "When a single client sends more than 2000 requests within any 5-minute sliding window, the load balancer MUST throttle all subsequent requests from that client for at least 60 seconds.", "Attributes": [ { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity. ", + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity.", "Section": "CCC.LB.CN01 Enforce and Detect Rate Limiting", "SubSection": "", "SubSectionObjective": "Detect and throttle malicious or excessive requests to prevent downstream resource exhaustion and brute-force activity.", @@ -1146,7 +3910,7 @@ "tlp-amber", "tlp-red" ], - "Recommendation": "Implement per-IP token-bucket limits with and verify via synthetic traffic tests. ", + "Recommendation": "Implement per-IP token-bucket limits with and verify via synthetic traffic tests.", "SectionThreatMappings": [ { "ReferenceId": "LB", @@ -1177,9 +3941,9 @@ } ], "Checks": [ - "elbv2_logging_enabled", - "elb_logging_enabled", - "vpc_flow_logs_enabled" + "wafv2_webacl_with_rules", + "waf_regional_webacl_with_rules", + "waf_global_webacl_with_rules" ] }, { @@ -1187,8 +3951,8 @@ "Description": "When throttling is invoked, the load balancer MUST record the event in the access log within 5 minutes for alerting and trend analysis.", "Attributes": [ { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity. ", + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity.", "Section": "CCC.LB.CN01 Enforce and Detect Rate Limiting", "SubSection": "", "SubSectionObjective": "Detect and throttle malicious or excessive requests to prevent downstream resource exhaustion and brute-force activity.", @@ -1197,7 +3961,7 @@ "tlp-amber", "tlp-red" ], - "Recommendation": "Enable access logging and configure metric filters on HTTP 429 counts to trigger alerts. ", + "Recommendation": "Enable access logging and configure metric filters on HTTP 429 counts to trigger alerts.", "SectionThreatMappings": [ { "ReferenceId": "LB", @@ -1228,9 +3992,10 @@ } ], "Checks": [ + "wafv2_webacl_logging_enabled", + "waf_global_webacl_logging_enabled", "elbv2_logging_enabled", - "elb_logging_enabled", - "vpc_flow_logs_enabled" + "elb_logging_enabled" ] }, { @@ -1238,8 +4003,8 @@ "Description": "When more than 10 percent of targets change from healthy to unhealthy within five minutes, an alert MUST be issued.", "Attributes": [ { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity. ", + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity.", "Section": "CCC.LB.CN06 Secure Health-Check Telemetry", "SubSection": "", "SubSectionObjective": "Monitor health-check endpoints for tampering and alert on abnormal status changes.", @@ -1248,7 +4013,7 @@ "tlp-amber", "tlp-red" ], - "Recommendation": "Instrument metrics for health check results and target removal events. Configure monitoring alarms to alert on abnormal spikes in unhealthy targets. ", + "Recommendation": "Instrument metrics for health check results and target removal events. Configure monitoring alarms to alert on abnormal spikes in unhealthy targets.", "SectionThreatMappings": [ { "ReferenceId": "LB", @@ -1276,9 +4041,7 @@ "Checks": [ "elbv2_logging_enabled", "elb_logging_enabled", - "cloudwatch_alarm_actions_enabled", - "cloudtrail_cloudwatch_logging_enabled", - "vpc_flow_logs_enabled" + "cloudwatch_alarm_actions_enabled" ] }, { @@ -1287,7 +4050,7 @@ "Attributes": [ { "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can change or query load-balancer resources. ", + "FamilyDescription": "Controls that restrict who can change or query load-balancer resources.", "Section": "CCC.LB.CN04 Enforce Distribution Policies", "SubSection": "", "SubSectionObjective": "Ensure traffic-splitting weights and algorithms are modified only by trusted identities.", @@ -1296,7 +4059,7 @@ "tlp-amber", "tlp-red" ], - "Recommendation": "Define a list of trusted principals allowed to modify routing configurations. Enforce via conditional access policies, and log changes using audit logging. ", + "Recommendation": "Define a list of trusted principals allowed to modify routing configurations. Enforce via conditional access policies, and log changes using audit logging.", "SectionThreatMappings": [ { "ReferenceId": "LB", @@ -1323,11 +4086,7 @@ ], "Checks": [ "cloudtrail_cloudwatch_logging_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "iam_policy_attached_only_to_group_or_roles", - "iam_group_administrator_access_policy", - "iam_role_administratoraccess_policy", - "iam_user_administrator_access_policy" + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled" ] }, { @@ -1336,7 +4095,7 @@ "Attributes": [ { "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can change or query load-balancer resources. ", + "FamilyDescription": "Controls that restrict who can change or query load-balancer resources.", "Section": "CCC.LB.CN05 Validate Session Affinity", "SubSection": "", "SubSectionObjective": "Configure session persistence to minimise fixation and hijacking risks.", @@ -1345,7 +4104,7 @@ "tlp-amber", "tlp-red" ], - "Recommendation": "Audit CCC.LB.F15 parameters via configuration scans.", + "Recommendation": "Audit CCC.LB.CP15 parameters via configuration scans.", "SectionThreatMappings": [ { "ReferenceId": "LB", @@ -1370,22 +4129,7 @@ ] } ], - "Checks": [ - "iam_user_administrator_access_policy", - "iam_group_administrator_access_policy", - "iam_role_administratoraccess_policy", - "iam_inline_policy_allows_privilege_escalation", - "iam_inline_policy_no_full_access_to_cloudtrail", - "iam_inline_policy_no_full_access_to_kms", - "iam_inline_policy_no_administrative_privileges", - "iam_policy_allows_privilege_escalation", - "iam_customer_attached_policy_no_administrative_privileges", - "iam_aws_attached_policy_no_administrative_privileges", - "iam_policy_no_full_access_to_cloudtrail", - "iam_policy_no_full_access_to_kms", - "iam_customer_unattached_policy_no_administrative_privileges", - "iam_policy_attached_only_to_group_or_roles" - ] + "Checks": [] }, { "Id": "CCC.LB.CN09.AR01", @@ -1393,7 +4137,7 @@ "Attributes": [ { "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can change or query load-balancer resources. ", + "FamilyDescription": "Controls that restrict who can change or query load-balancer resources.", "Section": "CCC.LB.CN09 Restrict Management API Access", "SubSection": "", "SubSectionObjective": "Limit load-balancer API calls to authorised identities and trusted networks.", @@ -1428,9 +4172,9 @@ } ], "Checks": [ + "bedrock_vpc_endpoints_configured", "vpc_endpoint_services_allowed_principals_trust_boundaries", - "vpc_endpoint_connections_trust_boundaries", - "vpc_endpoint_for_ec2_enabled" + "vpc_endpoint_connections_trust_boundaries" ] }, { @@ -1439,7 +4183,7 @@ "Attributes": [ { "FamilyName": "Data", - "FamilyDescription": "Controls that preserve availability and confidentiality of traffic processed by the load balancer. ", + "FamilyDescription": "Controls that preserve availability and confidentiality of traffic processed by the load balancer.", "Section": "CCC.LB.CN02 Auto-Scale Load Balancer Capacity", "SubSection": "", "SubSectionObjective": "Expand load-balancer capacity to maintain availability during traffic spikes.", @@ -1476,9 +4220,7 @@ "Checks": [ "autoscaling_group_capacity_rebalance_enabled", "autoscaling_group_elb_health_check_enabled", - "autoscaling_group_multiple_az", - "autoscaling_group_multiple_instance_types", - "autoscaling_group_using_ec2_launch_template" + "autoscaling_group_multiple_az" ] }, { @@ -1487,7 +4229,7 @@ "Attributes": [ { "FamilyName": "Data", - "FamilyDescription": "Controls that preserve availability and confidentiality of traffic processed by the load balancer. ", + "FamilyDescription": "Controls that preserve availability and confidentiality of traffic processed by the load balancer.", "Section": "CCC.LB.CN07 Scrub Sensitive Headers", "SubSection": "", "SubSectionObjective": "Remove headers that disclose internal details or software versions from HTTP responses.", @@ -1501,7 +4243,7 @@ { "ReferenceId": "LB", "Identifiers": [ - "CCC.TH15" + "CCC.Core.TH15" ] } ], @@ -1564,1733 +4306,7 @@ } ], "Checks": [ - "acm_certificates_expiration_check", - "acm_certificates_transparency_logs_enabled", - "acm_certificates_with_secure_key_algorithms" - ] - }, - { - "Id": "CCC.Logging.CN01.AR01", - "Description": "When a new cloud account is created, provider-level audit and network flow logging MUST be enabled by default and directed to the central sink.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", - "Section": "CCC.Logging.CN01 Centralized and Comprehensive Log Aggregation", - "SubSection": "", - "SubSectionObjective": "Ensure all operational and security logs from across the cloud environment, including applications, operating systems, network traffic, and cloud service activity, are captured automatically and streamed to a central, secure log management service.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Logging.TH07" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.PS-04" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3" - ] - } - ] - } - ], - "Checks": [ - "cloudtrail_multi_region_enabled", - "cloudtrail_cloudwatch_logging_enabled", - "cloudtrail_kms_encryption_enabled", - "cloudtrail_log_file_validation_enabled", - "vpc_flow_logs_enabled", - "cloudtrail_logs_s3_bucket_access_logging_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_s3_dataevents_read_enabled", - "cloudtrail_s3_dataevents_write_enabled", - "apigateway_restapi_logging_enabled", - "apigateway_restapi_authorizers_enabled", - "apigateway_restapi_public", - "apigatewayv2_api_access_logging_enabled" - ] - }, - { - "Id": "CCC.Logging.CN01.AR02", - "Description": "When a new cloud compute resource is deployed, it MUST be configured to forward all relevant logs (e.g., OS, application, service logs) to the central log sink.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", - "Section": "CCC.Logging.CN01 Centralized and Comprehensive Log Aggregation", - "SubSection": "", - "SubSectionObjective": "Ensure all operational and security logs from across the cloud environment, including applications, operating systems, network traffic, and cloud service activity, are captured automatically and streamed to a central, secure log management service.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Logging.TH07" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.PS-04" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3" - ] - } - ] - } - ], - "Checks": [ - "vpc_flow_logs_enabled", - "cloudtrail_cloudwatch_logging_enabled", - "cloudtrail_multi_region_enabled", - "cloudtrail_kms_encryption_enabled", - "cloudtrail_s3_dataevents_read_enabled", - "cloudtrail_s3_dataevents_write_enabled", - "apigateway_restapi_logging_enabled", - "apigatewayv2_api_access_logging_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", - "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible" - ] - }, - { - "Id": "CCC.Logging.CN02.AR01", - "Description": "When a new log bucket or stream is created, its retention policy MUST be configured in accordance with organisation's data retention policy.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", - "Section": "CCC.Logging.CN02 Enforce Data Retention Policy for Logs", - "SubSection": "", - "SubSectionObjective": "Ensure that the retention period configured for logs aligns with the organization's data retention policy.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Logging.TH05" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "GV.PO-01" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-11" - ] - } - ] - } - ], - "Checks": [ - "cloudwatch_log_group_retention_policy_specific_days_enabled" - ] - }, - { - "Id": "CCC.Logging.CN02.AR02", - "Description": "When a query is performed to retrieve log events older than the number of days defined in the organisation's data retention policy, it MUST return an empty result.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", - "Section": "CCC.Logging.CN02 Enforce Data Retention Policy for Logs", - "SubSection": "", - "SubSectionObjective": "Ensure that the retention period configured for logs aligns with the organization's data retention policy.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Logging.TH05" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "GV.PO-01" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-11" - ] - } - ] - } - ], - "Checks": [ - "cloudwatch_log_group_retention_policy_specific_days_enabled" - ] - }, - { - "Id": "CCC.AuditLog.CN08.AR01", - "Description": "When an attempt is made to modify or delete data before the object lock period expires, then the action MUST be denied.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", - "Section": "CCC.Logging.CN03 Enable Object Lock On Log Bucket", - "SubSection": "", - "SubSectionObjective": "Ensure log immutability by enabling Write Once, Read Many (WORM) protection using object lock on log storage buckets. This prevents logs from being modified or deleted during the defined retention period, supporting compliance and forensic integrity.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "Configure object lock policy. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH07" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.PS-04" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-9", - "AU-11" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_object_lock" - ] - }, - { - "Id": "CCC.AuditLog.CN04.AR01", - "Description": "When restricted fields are accessed by unauthorized users, then those fields MUST remain masked.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can access and modify logs. ", - "Section": "CCC.Logging.CN04 Restrict Field And Log Type Access", - "SubSection": "", - "SubSectionObjective": "Configure access to logs to follow the principle of least privilege in particular where technically possible limit the log fields users have access to to prevent accidental exposure to sensitive information such as PII.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "Review field level access controls on log data. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Logging.TH04" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.PS-04" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-6", - "AU-9", - "AC-3", - "PT-2", - "PT-3", - "PT-3" - ] - } - ] - } - ], - "Checks": [ - "cloudwatch_log_group_not_publicly_accessible", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible" - ] - }, - { - "Id": "CCC.Logging.CN05.AR01", - "Description": "When a log storage bucket is created, the bucket's access control settings MUST explicitly deny public read and write access.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can access and modify logs. ", - "Section": "CCC.Logging.CN05 Ensure Log Bucket is Not Publicly Accessible", - "SubSection": "", - "SubSectionObjective": "Ensure that log storage buckets are not publicly accessible to prevent unauthorized access to sensitive log data. In addition, logs should be replicated to another cloud region to enhance availability, durability, and support disaster recovery requirements.", - "Applicability": [ - "tlp-red", - "tlp-amber", - "tlp-green" - ], - "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AA-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "SC-7" - ] - } - ] - } - ], - "Checks": [ - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_multi_region_enabled", - "cloudtrail_logs_s3_bucket_access_logging_enabled", - "cloudtrail_kms_encryption_enabled", - "s3_bucket_public_list_acl", - "s3_bucket_public_write_acl", - "s3_bucket_public_access", - "s3_account_level_public_access_blocks", - "s3_bucket_level_public_access_block" - ] - }, - { - "Id": "CCC.Logging.CN05.AR02", - "Description": "When the URL of a log storage bucket's object is accessed publicly, the action MUST be denied by bucket policy.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can access and modify logs. ", - "Section": "CCC.Logging.CN05 Ensure Log Bucket is Not Publicly Accessible", - "SubSection": "", - "SubSectionObjective": "Ensure that log storage buckets are not publicly accessible to prevent unauthorized access to sensitive log data. In addition, logs should be replicated to another cloud region to enhance availability, durability, and support disaster recovery requirements.", - "Applicability": [ - "tlp-red", - "tlp-amber", - "tlp-green" - ], - "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AA-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "SC-7" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_public_access", - "s3_bucket_public_list_acl", - "s3_bucket_public_write_acl", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "s3_bucket_cross_region_replication", - "s3_account_level_public_access_blocks" - ] - }, - { - "Id": "CCC.Logging.CN06.AR01", - "Description": "When a single principal executes an anomalously high number of log queries, an alert MUST be generated.", - "Attributes": [ - { - "FamilyName": "Logging and Monitoring", - "FamilyDescription": "Controls that collect, alert, and retain logging-related events. ", - "Section": "CCC.Logging.CN06 Detect and Alert on Potential Log Exfiltration", - "SubSection": "", - "SubSectionObjective": "Identify and alert on anomalous data access patterns that may indicate an attempt to exfiltrate log data.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Logging.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-03", - "DE.CM-09" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SI-4", - "CA-7", - "AU-6" - ] - } - ] - } - ], - "Checks": [ - "cloudtrail_threat_detection_enumeration", - "cloudtrail_threat_detection_privilege_escalation" - ] - }, - { - "Id": "CCC.Logging.CN07.AR01", - "Description": "When an audit log event is recorded that corresponds to a modification of the logging service configuration such as disabling a log trail, deleting a log sink, or altering a log forwarding rule, an alert MUST be generated.", - "Attributes": [ - { - "FamilyName": "Logging and Monitoring", - "FamilyDescription": "Controls that collect, alert, and retain logging-related events. ", - "Section": "CCC.Logging.CN07 Detect and Alert on Log Service Tampering", - "SubSection": "", - "SubSectionObjective": "Alert when any component of the critical logging infrastructure is disabled, modified, or deleted, indicating a defense evasion attempt.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH16" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-03", - "DE.CM-09" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SI-4", - "CA-7", - "AU-6" - ] - } - ] - } - ], - "Checks": [ - "cloudtrail_cloudwatch_logging_enabled", - "cloudtrail_insights_exist", - "cloudtrail_log_file_validation_enabled", - "cloudtrail_kms_encryption_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "cloudtrail_multi_region_enabled_logging_management_events", - "cloudwatch_changes_to_network_acls_alarm_configured", - "cloudwatch_changes_to_network_gateways_alarm_configured", - "cloudwatch_changes_to_network_route_tables_alarm_configured", - "cloudwatch_changes_to_vpcs_alarm_configured" - ] - }, - { - "Id": "CCC.ObjStor.CN01.AR01", - "Description": "When a request is made to read a protected bucket, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN01 Prevent Unencrypted Requests", - "SubSection": "CCC.ObjStor.C01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", - "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption that can impact data availability and integrity.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01", - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-04", - "DCS-06" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_kms_encryption", - "kms_key_not_publicly_accessible", - "iam_policy_no_full_access_to_kms", - "storagegateway_fileshare_encryption_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN01.AR02", - "Description": "When a request is made to read a protected object, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN01 Prevent Unencrypted Requests", - "SubSection": "CCC.ObjStor.C01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", - "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption that can impact data availability and integrity.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01", - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-04", - "DCS-06" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "kms_key_not_publicly_accessible", - "kms_cmk_not_deleted_unintentionally", - "iam_policy_no_full_access_to_kms", - "iam_inline_policy_no_full_access_to_kms" - ] - }, - { - "Id": "CCC.ObjStor.CN01.AR03", - "Description": "When a request is made to write to a bucket, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN01 Prevent Unencrypted Requests", - "SubSection": "CCC.ObjStor.C01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", - "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption that can impact data availability and integrity.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01", - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-04", - "DCS-06" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_kms_encryption", - "iam_policy_no_full_access_to_kms", - "kms_key_not_publicly_accessible", - "kms_cmk_not_deleted_unintentionally", - "storagegateway_fileshare_encryption_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN01.AR04", - "Description": "When a request is made to write to an object, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN01 Prevent Unencrypted Requests", - "SubSection": "CCC.ObjStor.C01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", - "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption that can impact data availability and integrity.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01", - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-04", - "DCS-06" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "iam_policy_no_full_access_to_kms", - "iam_inline_policy_no_full_access_to_kms", - "kms_cmk_not_deleted_unintentionally", - "kms_key_not_publicly_accessible", - "kms_cmk_not_multi_region" - ] - }, - { - "Id": "CCC.ObjStor.CN03.AR01", - "Description": "When an object storage bucket deletion is attempted, the bucket MUST be fully recoverable for a set time-frame after deletion is requested.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "CCC.ObjStor.C03 Prevent Bucket Deletion Through Irrevocable Bucket Retention Policy", - "SubSectionObjective": "Ensure that object storage bucket is not deleted after creation, and that the preventative measure cannot be unset.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_object_versioning", - "s3_bucket_object_lock", - "s3_bucket_lifecycle_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN03.AR02", - "Description": "When an attempt is made to modify the retention policy for an object storage bucket, the service MUST prevent the policy from being modified.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "CCC.ObjStor.C03 Prevent Bucket Deletion Through Irrevocable Bucket Retention Policy", - "SubSectionObjective": "Ensure that object storage bucket is not deleted after creation, and that the preventative measure cannot be unset.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_object_versioning", - "s3_bucket_lifecycle_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN04.AR01", - "Description": "When an object is uploaded to the object storage system, the object MUST automatically receive a default retention policy that prevents premature deletion or modification.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN04 Log All Access and Changes", - "SubSection": "CCC.ObjStor.C04 Objects have an Effective Retention Policy by Default", - "SubSectionObjective": "Ensure that all objects stored in the object storage system have a retention policy applied by default, preventing premature deletion or modification of objects and ensuring compliance with data retention regulations.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "kinesis_stream_data_retention_period", - "s3_bucket_object_versioning", - "s3_bucket_object_lock" - ] - }, - { - "Id": "CCC.ObjStor.CN04.AR02", - "Description": "When an attempt is made to delete or modify an object that is subject to an active retention policy, the service MUST prevent the action from being completed.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN04 Log All Access and Changes", - "SubSection": "CCC.ObjStor.C04 Objects have an Effective Retention Policy by Default", - "SubSectionObjective": "Ensure that all objects stored in the object storage system have a retention policy applied by default, preventing premature deletion or modification of objects and ensuring compliance with data retention regulations.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "kinesis_stream_data_retention_period", - "dynamodb_table_deletion_protection_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN05.AR01", - "Description": "When an object is uploaded to the object storage bucket, the object MUST be stored with a unique identifier.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN05 Prevent Access from Untrusted Entities", - "SubSection": "CCC.ObjStor.C05 Versioning is Enabled for All Objects in the Bucket", - "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_object_versioning", - "s3_bucket_object_lock" - ] - }, - { - "Id": "CCC.ObjStor.CN05.AR02", - "Description": "When an object is modified, the service MUST assign a new unique identifier to the modified object to differentiate it from the previous version.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN05 Prevent Access from Untrusted Entities", - "SubSection": "CCC.ObjStor.C05 Versioning is Enabled for All Objects in the Bucket", - "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_object_versioning", - "s3_bucket_object_lock", - "iam_rotate_access_key_90_days", - "ecr_repositories_tag_immutability" - ] - }, - { - "Id": "CCC.ObjStor.CN05.AR03", - "Description": "When an object is modified, the service MUST allow for recovery of previous versions of the object.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN05 Prevent Access from Untrusted Entities", - "SubSection": "CCC.ObjStor.C05 Versioning is Enabled for All Objects in the Bucket", - "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_object_versioning", - "dynamodb_tables_pitr_enabled", - "backup_recovery_point_encrypted" - ] - }, - { - "Id": "CCC.ObjStor.CN05.AR04", - "Description": "When an object is deleted, the service MUST retain other versions of the object to allow for recovery of previous versions.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN05 Prevent Access from Untrusted Entities", - "SubSection": "CCC.ObjStor.C05 Versioning is Enabled for All Objects in the Bucket", - "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_object_versioning", - "kinesis_stream_data_retention_period", - "kms_cmk_not_deleted_unintentionally" - ] - }, - { - "Id": "CCC.ObjStor.CN06.AR01", - "Description": "When an object storage bucket is accessed, the service MUST store access logs in a separate data store.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN06 Prevent Deployment in Restricted Regions", - "SubSection": "CCC.ObjStor.C06 Access Logs are Stored in a Separate Data Store", - "SubSectionObjective": "Ensure that access logs for object storage buckets are stored in a separate data store to protect against unauthorized access, tampering, or deletion of logs (Logbuckets are exempt from this requirement, but must be tlp-red).", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH07", - "CCC.TH09" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-6" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-07", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.15.0" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-9", - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "cloudtrail_s3_dataevents_read_enabled", - "cloudtrail_logs_s3_bucket_access_logging_enabled", - "s3_bucket_server_access_logging_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN02.AR01", - "Description": "When a permission set is allowed for an object in a bucket, the service MUST allow the same permission set to access all objects in the same bucket.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN02 Ensure Data Encryption at Rest for All Stored Data", - "SubSection": "CCC.ObjStor.C02 Enforce Uniform Bucket-level Access to Prevent Inconsistent Permissions", - "SubSectionObjective": "Ensure that uniform bucket-level access is enforced across all object storage buckets. This prevents the use of ad-hoc or inconsistent object-level permissions, ensuring centralized, consistent, and secure access management in accordance with the principle of least privilege.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "AC-6" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-09" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_public_write_acl" - ] - }, - { - "Id": "CCC.ObjStor.CN02.AR02", - "Description": "When a permission set is denied for an object in a bucket, the service MUST deny the same permission set to access all objects in the same bucket.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN02 Ensure Data Encryption at Rest for All Stored Data", - "SubSection": "CCC.ObjStor.C02 Enforce Uniform Bucket-level Access to Prevent Inconsistent Permissions", - "SubSectionObjective": "Ensure that uniform bucket-level access is enforced across all object storage buckets. This prevents the use of ad-hoc or inconsistent object-level permissions, ensuring centralized, consistent, and secure access management in accordance with the principle of least privilege.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "AC-6" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-09" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_public_write_acl", - "s3_bucket_acl_prohibited", - "s3_bucket_public_access" - ] - }, - { - "Id": "CCC.Monitor.CN01.AR01", - "Description": "When an External Monitoring system exceeds the anticipated rate of monitoring checks then Rate Limiting MUST be applied and an Audit Alert MUST be generated.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that collect, alert, and retain events from other monitoring services.", - "Section": "CCC.Monitor.CN01 Rate Limiting on External Monitoring", - "SubSection": "", - "SubSectionObjective": "Prevent DoS attacks using External Monitoring tools.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Monitor.TH03" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.IR-01", - "DE.CM-01" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-5", - "SC-7" - ] - } - ] - } - ], - "Checks": [ - "cloudtrail_threat_detection_enumeration", - "cloudtrail_threat_detection_llm_jacking", - "cloudtrail_threat_detection_privilege_escalation", - "cloudtrail_cloudwatch_logging_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "cloudwatch_alarm_actions_alarm_state_configured", - "cloudtrail_multi_region_enabled_logging_management_events" - ] - }, - { - "Id": "CCC.Monitor.CN02.AR01", - "Description": "When an Custom or User-Defined Metric starts to flood a collector, then a rate limit MUST be applied to reduce the network impact of traffic and an alert must triggered.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that collect, alert, and retain events from other monitoring services.", - "Section": "CCC.Monitor.CN02 Rate Limiting on Metric Generation", - "SubSection": "", - "SubSectionObjective": "Prevent Malicious Actor or misconfiguration from flooding services with metric data.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Monitor.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-01" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-5(2)", - "CA-7", - "SI-4" - ] - } - ] - } - ], - "Checks": [ - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", - "cloudwatch_changes_to_network_acls_alarm_configured", - "cloudwatch_changes_to_network_gateways_alarm_configured", - "cloudwatch_changes_to_network_route_tables_alarm_configured", - "cloudwatch_changes_to_vpcs_alarm_configured", - "cloudwatch_alarm_actions_enabled", - "cloudwatch_alarm_actions_alarm_state_configured", - "cloudwatch_log_metric_filter_authentication_failures", - "cloudwatch_log_metric_filter_unauthorized_api_calls", - "cloudwatch_log_metric_filter_policy_changes", - "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", - "cloudwatch_log_metric_filter_security_group_changes" - ] - }, - { - "Id": "CCC.Monitor.CN03.AR01", - "Description": "When external systems have approved access to internal systems not normally available for public access then they MUST be secured to prevent unauthorised access jumping through to the internal systems and only allow access to specific internal services.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", - "Section": "CCC.Monitor.CN03 Access External Monitoring", - "SubSection": "", - "SubSectionObjective": "Control access to Synthetic monitoring solutions using API keys or Certificate based authentication to ensure they don't become an attack path, preventing monitoring systems from forging network requests to gain access to internal systems.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Monitor.TH04" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-06", - "PR.IR-01", - "PR.AA-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "awslambda_function_url_public", - "apigateway_restapi_public", - "apigateway_restapi_authorizers_enabled", - "apigateway_restapi_public_with_authorizer", - "apigateway_restapi_client_certificate_enabled", - "apigatewayv2_api_authorizers_enabled" - ] - }, - { - "Id": "CCC.Monitor.CN04.AR01", - "Description": "When monitoring dashboards display degraded services which may become potential targets then the dashboard MUST be protected from unauthorised access.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", - "Section": "CCC.Monitor.CN04 Restrict access to Monitoring Dashboards", - "SubSection": "", - "SubSectionObjective": "Control access to Monitoring Dashboards and reports to ensure they don't highlight an attack path.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Monitor.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-09", - "DE.AE-03" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SI-4", - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "cloudwatch_log_group_not_publicly_accessible", - "cloudwatch_log_group_kms_encryption_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_cloudwatch_logging_enabled" - ] - }, - { - "Id": "CCC.Monitor.CN05.AR01", - "Description": "When monitoring services have generated an alert, the service MUST ensure only authorised responders silence or acknowledge the alert.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", - "Section": "CCC.Monitor.CN05 Restrict access to silence or acknowledge an alert", - "SubSection": "", - "SubSectionObjective": "Ensure only a subset of users can silence or acknowledge alerts to prevent attackers hiding their activity.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH10" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.IR-01", - "PR.AA-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "iam_administrator_access_with_mfa", - "iam_root_mfa_enabled", - "iam_group_administrator_access_policy", - "iam_policy_attached_only_to_group_or_roles", - "iam_inline_policy_allows_privilege_escalation", - "iam_inline_policy_no_full_access_to_cloudtrail" - ] - }, - { - "Id": "CCC.Monitor.CN06.AR01", - "Description": "When systems push metrics or traces they MUST be authenticated for that particular type of metric or trace", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", - "Section": "CCC.Monitor.CN06 Metrics pushed for authorised services only", - "SubSection": "", - "SubSectionObjective": "Use IAM to control which types of metrics or traces can be pushed by different system to avoid a compromised system pushing fabricated metrics about a different service", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Monitor.TH05" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AA-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-5" - ] - } - ] - } - ], - "Checks": [ - "awslambda_function_url_public", - "awslambda_function_not_publicly_accessible", - "apigateway_restapi_authorizers_enabled", - "apigatewayv2_api_authorizers_enabled", - "apigateway_restapi_public_with_authorizer", - "apigateway_restapi_public", - "cloudwatch_log_group_not_publicly_accessible" + "acm_certificates_expiration_check" ] }, { @@ -3345,11 +4361,61 @@ } ], "Checks": [ - "ec2_securitygroup_default_restrict_traffic", - "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", - "ec2_securitygroup_allow_ingress_from_internet_to_any_port" + "ec2_securitygroup_default_restrict_traffic" ] }, + { + "Id": "CCC.VPC.CN02.AR01", + "Description": "When a resource is created in a public subnet, that resource MUST NOT be assigned an external IP address by default.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.VPC.CN02 Limit Resource Creation in Public Subnet", + "SubSection": "", + "SubSectionObjective": "Restrict the creation of resources in the public subnet with direct access to the internet to minimize attack surfaces.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.VPC.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "SEF-05" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.13.1.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-4" + ] + } + ] + } + ], + "Checks": [] + }, { "Id": "CCC.VPC.CN03.AR01", "Description": "When a VPC peering connection is requested, the service MUST prevent connections from VPCs that are not explicitly allowed.", @@ -3461,6 +4527,390 @@ "vpc_flow_logs_enabled" ] }, + { + "Id": "CCC.KeyMgmt.CN01.AR01", + "Description": "When a key version is scheduled for deletion or disabled, an alert MUST be generated within five minutes.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain key-management events.", + "Section": "CCC.KeyMgmt.CN01 Alert on Key-version Changes", + "SubSection": "", + "SubSectionObjective": "Generate near-real-time alerts when a KMS key version is disabled or scheduled for deletion, enabling rapid investigation and recovery.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Use native event services (e.g., CloudWatch Events, Azure Monitor, Cloud Audit Logs) to route notifications to an incident-response channel.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.KeyMgmt.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "RS.AN-1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "IR-5" + ] + } + ] + } + ], + "Checks": [ + "kms_cmk_not_deleted_unintentionally", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk" + ] + }, + { + "Id": "CCC.KeyMgmt.CN02.AR01", + "Description": "When IAM roles and key policies are reviewed, Decrypt permission MUST be granted exclusively to documented authorised principals.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that enforce least-privilege use of KMS operations.", + "Section": "CCC.KeyMgmt.CN02 Limit Decrypt Permissions", + "SubSection": "", + "SubSectionObjective": "Restrict the Decrypt operation to authorised principals only, applying the principle of least privilege to protect sensitive data.", + "Applicability": [ + "tlp-green" + ], + "Recommendation": "Periodically audit policy documents via automated tooling and report any deviations.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.KeyMgmt.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_inline_policy_no_full_access_to_kms", + "iam_policy_no_full_access_to_kms" + ] + }, + { + "Id": "CCC.KeyMgmt.CN03.AR01", + "Description": "When rotation settings are examined, rotation MUST be enabled with an interval not exceeding 365 days.", + "Attributes": [ + { + "FamilyName": "Key Lifecycle Management", + "FamilyDescription": "Controls that govern creation, rotation, import, and retirement of cryptographic keys.", + "Section": "CCC.KeyMgmt.CN03 Enforce Automatic Rotation", + "SubSection": "", + "SubSectionObjective": "Ensure symmetric keys rotate automatically within policy intervals to reduce exposure of key material.", + "Applicability": [ + "tlp-green" + ], + "Recommendation": "Use cloud-provider rotation features and verify via configuration scanning.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.KeyMgmt.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-12" + ] + } + ] + } + ], + "Checks": [ + "kms_cmk_rotation_enabled" + ] + }, + { + "Id": "CCC.KeyMgmt.CN04.AR01", + "Description": "When a key import request is processed, the key MUST use an approved algorithm (RSA-2048+, EC-P256+) and originate from a certified HSM.", + "Attributes": [ + { + "FamilyName": "Key Lifecycle Management", + "FamilyDescription": "Controls that govern creation, rotation, import, and retirement of cryptographic keys.", + "Section": "CCC.KeyMgmt.CN04 Validate Imported Keys", + "SubSection": "", + "SubSectionObjective": "Accept only externally generated keys that meet approved cryptographic strength and provenance requirements.", + "Applicability": [ + "tlp-green" + ], + "Recommendation": "Implement an approval workflow that validates attestation data before import.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.KeyMgmt.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-28" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.SecMgmt.CN01.AR01", + "Description": "Attempt to use an outdated version of a secret after its rotation period has passed and verify that access is denied.", + "Attributes": [ + { + "FamilyName": "Data Protection", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.SecMgmt.CN01 Enforce Automatic Secret Rotation", + "SubSection": "", + "SubSectionObjective": "Ensure that secrets are automatically rotated on a defined schedule to reduce the risk of secret compromise and unauthorized access.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH14" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-12", + "SC-28" + ] + } + ] + } + ], + "Checks": [ + "secretsmanager_automatic_rotation_enabled", + "secretsmanager_secret_rotated_periodically" + ] + }, + { + "Id": "CCC.SecMgmt.CN02.AR01", + "Description": "Attempt to retrieve a secret from an unauthorized region and verify that access is denied.", + "Attributes": [ + { + "FamilyName": "Data Protection", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.SecMgmt.CN02 Enforce Secret Replication Policies", + "SubSection": "", + "SubSectionObjective": "Ensure that secrets are replicated only to authorized locations as per organizational data residency and compliance requirements.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH03", + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-5" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", + "SC-7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.DataWar.CN01.AR01", + "Description": "Attempt to access underlying database tables directly without using managed views and verify that access is denied.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.DataWar.CN01 Enforce Use of Managed Views for Data Access", + "SubSection": "", + "SubSectionObjective": "Ensure that data access is provided through managed views, restricting users from accessing underlying tables directly and enforcing consistent security policies.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.DataWar.CN02.AR01", + "Description": "Attempt to query sensitive columns without the necessary permissions and verify that access is denied or data is masked.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.DataWar.CN02 Enforce Column-Level Security Policies", + "SubSection": "", + "SubSectionObjective": "Ensure that access to sensitive data columns is restricted based on user roles, preventing unauthorized access to sensitive information.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.DataWar.CN03.AR01", + "Description": "Attempt to query data rows that the user should not have access to and verify that access is denied or data is not returned.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.DataWar.CN03 Enforce Row-Level Security Policies", + "SubSection": "", + "SubSectionObjective": "Ensure that access to data rows is restricted based on user roles or attributes, preventing unauthorized access to specific subsets of data.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, { "Id": "CCC.Vector.CN01.AR01", "Description": "When a vector embedding is submitted for indexing, the system MUST validate that it matches expected schema, dimension, and format profiles.", @@ -3484,7 +4934,7 @@ "Identifiers": [ "CCC.Vector.TH02", "CCC.Vector.TH05", - "CCC.TH12" + "CCC.Core.TH12" ] } ], @@ -3523,7 +4973,7 @@ "Identifiers": [ "CCC.Vector.TH02", "CCC.Vector.TH04", - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -3538,17 +4988,9 @@ } ], "Checks": [ - "iam_group_administrator_access_policy", - "iam_user_administrator_access_policy", - "iam_role_administratoraccess_policy", - "iam_administrator_access_with_mfa", - "iam_inline_policy_allows_privilege_escalation", - "iam_inline_policy_no_full_access_to_cloudtrail", - "iam_inline_policy_no_full_access_to_kms", - "iam_policy_attached_only_to_group_or_roles", - "iam_customer_attached_policy_no_administrative_privileges", - "iam_customer_unattached_policy_no_administrative_privileges", - "iam_no_custom_policy_permissive_role_assumption" + "opensearch_service_domains_access_control_enabled", + "opensearch_service_domains_internal_user_database_enabled", + "iam_role_administratoraccess_policy" ] }, { @@ -3571,7 +5013,7 @@ "ReferenceId": "CCC", "Identifiers": [ "CCC.Vector.TH03", - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -3610,7 +5052,7 @@ "ReferenceId": "CCC", "Identifiers": [ "CCC.Vector.TH02", - "CCC.TH12" + "CCC.Core.TH12" ] } ], @@ -3646,8 +5088,8 @@ "ReferenceId": "CCC", "Identifiers": [ "CCC.Vector.TH04", - "CCC.TH09", - "CCC.TH04" + "CCC.Core.TH09", + "CCC.Core.TH04" ] } ], @@ -3685,7 +5127,7 @@ "ReferenceId": "CCC", "Identifiers": [ "CCC.Vector.TH05", - "CCC.TH06" + "CCC.Core.TH06" ] } ], @@ -3730,457 +5172,25 @@ "Checks": [] }, { - "Id": "CCC.Core.CN01.AR01", - "Description": "When a port is exposed for non-SSH network traffic, all traffic MUST include a TLS handshake AND be encrypted using TLS 1.3 or higher.", + "Id": "CCC.RDMS.CN01.AR02", + "Description": "When an attempt is made to authenticate to the database using known default credentials, the authentication attempt must fail and no access should be granted.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN01 Password Management", "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Most cloud services enable TLS 1.3 by default. Where it is not already set, ensure that your services are configured or updated accordingly. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "cloudfront_distributions_origin_traffic_encrypted", - "cloudfront_distributions_https_enabled", - "cloudfront_distributions_https_sni_enabled", - "s3_bucket_secure_transport_policy", - "dms_endpoint_redis_in_transit_encryption_enabled", - "kafka_cluster_in_transit_encryption_enabled", - "transfer_server_in_transit_encryption_enabled", - "redshift_cluster_in_transit_encryption_enabled", - "rds_instance_transport_encrypted" - ] - }, - { - "Id": "CCC.Core.CN01.AR02", - "Description": "When a port is exposed for SSH network traffic, all traffic MUST include a SSH handshake AND be encrypted using SSHv2 or higher.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", - "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Any time port 22 is exposed, ensure that it has a properly implemented SSH server with SSHv2 enabled and configured with strong ciphers. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "ec2_instance_port_ssh_exposed_to_internet", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", - "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", - "ec2_networkacl_allow_ingress_tcp_port_22" - ] - }, - { - "Id": "CCC.Core.CN01.AR03", - "Description": "When the service receives unencrypted traffic, then it MUST either block the request or automatically redirect it to the secure equivalent.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", - "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Review firewall, load balancer, and application configurations to ensure insecure protocols such as HTTP, FTP, and Telnet are not exposed. Where possible, implement automatic redirection to secure protocols such as HTTPS, SFTP, SSH, and regularly scan for protocol drift. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "cloudfront_distributions_https_enabled", - "cloudfront_distributions_https_sni_enabled", - "cloudfront_distributions_origin_traffic_encrypted", - "opensearch_service_domains_https_communications_enforced", - "transfer_server_in_transit_encryption_enabled", - "s3_bucket_secure_transport_policy", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23" - ] - }, - { - "Id": "CCC.Core.CN01.AR07", - "Description": "When a port is exposed, the service MUST ensure that the protocol and service officially assigned to that port number by the IANA Service Name and Transport Protocol Port Number Registry, and no other, is run on that port.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", - "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Reference the IANA Service Name and Transport Protocol Port Number Registry for more information about correct protocol-to-port assignments. Avoid running non-standard services on well-known ports. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "cloudfront_distributions_origin_traffic_encrypted", - "rds_instance_transport_encrypted", - "redshift_cluster_in_transit_encryption_enabled", - "kafka_cluster_in_transit_encryption_enabled", - "transfer_server_in_transit_encryption_enabled", - "dms_endpoint_redis_in_transit_encryption_enabled", - "dms_endpoint_ssl_enabled", - "s3_bucket_secure_transport_policy" - ] - }, - { - "Id": "CCC.Core.CN01.AR08", - "Description": "When a service transmits data using TLS, mutual TLS (mTLS) MUST be implemented to require both client and server certificate authentication for all connections.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", - "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Configure mTLS for all endpoints that process or transmit sensitive data. Ensure both client and server certificates are validated and managed securely. Regularly review certificate authorities and automate certificate rotation where possible. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "apigateway_restapi_client_certificate_enabled", - "cloudfront_distributions_custom_ssl_certificate", - "cloudfront_distributions_https_sni_enabled", - "cloudfront_distributions_origin_traffic_encrypted", - "acm_certificates_expiration_check", - "acm_certificates_with_secure_key_algorithms", - "acm_certificates_transparency_logs_enabled", - "s3_bucket_secure_transport_policy", - "dms_endpoint_ssl_enabled", - "dms_endpoint_redis_in_transit_encryption_enabled", - "transfer_server_in_transit_encryption_enabled", - "kafka_cluster_in_transit_encryption_enabled", - "kafka_cluster_mutual_tls_authentication_enabled" - ] - }, - { - "Id": "CCC.Core.CN13.AR01", - "Description": "When a port is exposed that uses certificate-based encryption, the service MUST only use valid, unexpired certificates issued by a trusted certificate authority.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH18" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "acm_certificates_expiration_check", - "acm_certificates_transparency_logs_enabled", - "acm_certificates_with_secure_key_algorithms" - ] - }, - { - "Id": "CCC.Core.CN13.AR02", - "Description": "When a port is exposed that uses certificate-based encryption, the service MUST rotate active certificates within 180 days of issuance.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", + "SubSectionObjective": "Ensure default vendor-supplied DB administrator credentials are replaced with strong, unique passwords and that these credentials are properly managed using a secure password or secrets management solution.", "Applicability": [ + "tlp-red", "tlp-amber" ], - "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH18" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "acm_certificates_expiration_check" - ] - }, - { - "Id": "CCC.Core.CN13.AR03", - "Description": "When a port is exposed that uses certificate-based encryption, the service MUST rotate active certificates within 90 days of issuance.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH18" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "acm_certificates_expiration_check" - ] - }, - { - "Id": "CCC.Core.CN06.AR01", - "Description": "When the service is running, its region and availability zone MUST be included in a list of explicitly trusted or approved locations within the trust perimeter.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN06 Restrict Deployments to Trust Perimeter", - "SubSection": "", - "SubSectionObjective": "Ensure that the service and its child resources are only deployed on infrastructure in locations that are explicitly included within a defined trust perimeter.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Maintain an up-to-date list of trusted and approved regions based on organizational policies. Validate the service's deployment location is included in this list. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH03" + "CCC.RDMS.TH01" ] } ], @@ -4188,19 +5198,92 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" + "PR.AA-01" ] }, { - "ReferenceId": "CCM", + "ReferenceId": "NIST_800_53", "Identifiers": [ - "DSP-19" + "AC-2" + ] + } + ] + } + ], + "Checks": [ + "rds_cluster_default_admin", + "rds_instance_default_admin" + ] + }, + { + "Id": "CCC.RDMS.CN02.AR01", + "Description": "When repeated failed login attempts are made in a short timeframe, the account must be locked out or rate-limited to prevent further login attempts.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN02 Account Lockout and Rate-Limiting", + "SubSection": "", + "SubSectionObjective": "Ensure the database enforces lockouts or rate-limiting after a specified number of failed authentication attempts. This prevents brute force or password-guessing attacks from succeeding.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.RDMS.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-1" ] }, { - "ReferenceId": "ISO_27001", + "ReferenceId": "NIST_800_53", "Identifiers": [ - "2013 A.11.1.1" + "AC-7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.RDMS.CN04.AR01", + "Description": "When there is an attempt to perform a backup or restore, then the attempt must fail with an access denied message if credentials or roles that are not explicitly authorized for backup/restore functions.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN04 Access Control for Backup and Restore Operations", + "SubSection": "", + "SubSectionObjective": "Restrict who can initiate, manage, and validate database backup or restore operations through strict role-based or least-privilege access. Prevents accidental or malicious restorations, protecting data integrity and availability.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.RDMS.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" ] }, { @@ -4213,33 +5296,30 @@ } ], "Checks": [ - "organizations_scp_check_deny_regions", - "vpc_endpoint_connections_trust_boundaries", - "vpc_endpoint_services_allowed_principals_trust_boundaries" + "iam_inline_policy_no_administrative_privileges", + "iam_customer_attached_policy_no_administrative_privileges" ] }, { - "Id": "CCC.Core.CN06.AR02", - "Description": "When a child resource is deployed, its region and availability zone MUST be included in a list of explicitly trusted or approved locations within the trust perimeter.", + "Id": "CCC.RDMS.CN05.AR01", + "Description": "When an attempt is made to share a snapshot with an unauthorized account, the sharing request must be denied.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN06 Restrict Deployments to Trust Perimeter", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN05 Restrict Snapshot Sharing to Authorized Accounts", "SubSection": "", - "SubSectionObjective": "Ensure that the service and its child resources are only deployed on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "SubSectionObjective": "Ensure database snapshots can only be shared with explicitly authorized accounts, thereby minimizing the risk of data exposure or exfiltration.", "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-red", + "tlp-amber" ], - "Recommendation": "Maintain an up-to-date list of trusted and approved regions based on organizational policies. Validate that child resources can only be deployed to locations included in this list. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH03" + "CCC.RDMS.TH05" ] } ], @@ -4247,24 +5327,105 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-19" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.11.1.1" + "PR.DS-10" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ + "AC-4" + ] + } + ] + } + ], + "Checks": [ + "rds_snapshots_public_access", + "neptune_cluster_public_snapshot", + "documentdb_cluster_public_snapshot", + "ec2_ami_public" + ] + }, + { + "Id": "CCC.RDMS.CN03.AR01", + "Description": "When backups are disabled, paused, or fail to run as scheduled, an alert must be triggered and logged.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN03 Enforce and Monitor Automated Backups", + "SubSection": "", + "SubSectionObjective": "Ensure database backups are automatically scheduled, actively monitored, and promptly reported if any disruptions occur. This helps maintain data integrity, facilitates disaster recovery, and supports business continuity when a system failure or breach occurs.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.RDMS.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IP-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "CP-9" + ] + } + ] + } + ], + "Checks": [ + "rds_instance_backup_enabled", + "rds_cluster_critical_event_subscription", + "neptune_cluster_backup_enabled", + "documentdb_cluster_backup_enabled" + ] + }, + { + "Id": "CCC.Build.CN01.AR01", + "Description": "Attempt to initiate a build using an unauthorized build agent and verify that the build is rejected.", + "Attributes": [ + { + "FamilyName": "Access Control", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.Build.CN01 Restrict Allowed Build Agents", + "SubSection": "", + "SubSectionObjective": "Ensure that builds are executed only on authorized build agents to maintain control over the build environment and prevent unauthorized code execution.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", "AC-6" ] } @@ -4272,92 +5433,31 @@ } ], "Checks": [ - "organizations_scp_check_deny_regions", - "vpc_endpoint_services_allowed_principals_trust_boundaries", - "vpc_endpoint_connections_trust_boundaries" + "codebuild_project_uses_allowed_github_organizations", + "codebuild_project_user_controlled_buildspec", + "codebuild_project_no_secrets_in_variables" ] }, { - "Id": "CCC.Core.CN08.AR01", - "Description": "When data is created or modified, the data MUST have a complete and recoverable duplicate that is stored in a physically separate data center.", + "Id": "CCC.Build.CN02.AR01", + "Description": "Attempt to trigger a build from an unauthorized external service or repository and verify that the build does not start.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN08 Replicate Data to Multiple Locations", + "FamilyName": "Access Control", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.Build.CN02 Restrict Allowed External Services for Build Triggers", "SubSection": "", - "SubSectionObjective": "Ensure that data is replicated across multiple physical locations to protect against data loss due to hardware failures, natural disasters, or other catastrophic events.", + "SubSectionObjective": "Ensure that builds can only be triggered by authorized external services or repositories to prevent unauthorized code execution or tampering.", "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Implement automated data replication processes to ensure that data is consistently duplicated in another region or availability zone. Regularly test data recovery from the replicated location to ensure integrity and availability. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.PT-5" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "BCR-08", - "BCR-10", - "BCR-11" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "CP-2", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_cross_region_replication", - "cloudtrail_multi_region_enabled", - "backup_plans_exist", - "backup_vaults_exist", - "backup_vaults_encrypted", - "dynamodb_table_protected_by_backup_plan", - "rds_cluster_protected_by_backup_plan", - "rds_instance_protected_by_backup_plan" - ] - }, - { - "Id": "CCC.Core.CN08.AR02", - "Description": "When data is replicated into a second location, the service MUST be able to accurately represent the replication locations, replication status, and data synchronization status.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN08 Replicate Data to Multiple Locations", - "SubSection": "", - "SubSectionObjective": "Ensure that data is replicated across multiple physical locations to protect against data loss due to hardware failures, natural disasters, or other catastrophic events.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-red", + "tlp-amber" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06" + "CCC.Core.TH01" ] } ], @@ -4365,41 +5465,165 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.PT-5" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "BCR-08", - "BCR-10", - "BCR-11" + "PR.AC-4" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "CP-2", - "CP-10" + "AC-3", + "AC-6" ] } ] } ], "Checks": [ - "s3_bucket_cross_region_replication" + "codebuild_project_uses_allowed_github_organizations", + "codebuild_project_source_repo_url_no_sensitive_credentials" ] }, { - "Id": "CCC.Core.CN09.AR01", - "Description": "When the service is operational, its logs and any child resource logs MUST NOT be accessible from the resource they record access to.", + "Id": "CCC.Build.CN03.AR01", + "Description": "Attempt to access the build environment from an external network and verify that access is denied.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.Build.CN03 Deny External Network Access for Build Environments", "SubSection": "", - "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "SubSectionObjective": "Ensure that build environments do not have external network access to prevent unauthorized external access and data exfiltration.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02", + "CCC.Core.TH05" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-5" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7", + "SC-5" + ] + } + ] + } + ], + "Checks": [ + "codebuild_project_not_publicly_accessible" + ] + }, + { + "Id": "CCC.CntrReg.CN01.AR01", + "Description": "Attempt to push an artifact with known vulnerabilities to the registry and observe if it is flagged or rejected by the vulnerability scanning process.", + "Attributes": [ + { + "FamilyName": "Risk Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.CntrReg.CN01 Implement Vulnerability Scanning for Artifacts", + "SubSection": "", + "SubSectionObjective": "Ensure that container images and artifacts stored in the container registry are scanned for vulnerabilities to identify and remediate security issues before deployment.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.CntrReg.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "ID.RA-1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "RA-5", + "SI-5" + ] + } + ] + } + ], + "Checks": [ + "ecr_registry_scan_images_on_push_enabled", + "ecr_repositories_scan_vulnerabilities_in_latest_image" + ] + }, + { + "Id": "CCC.CntrReg.CN02.AR01", + "Description": "Confirm that artifacts older than the specified retention period are automatically deleted from the registry.", + "Attributes": [ + { + "FamilyName": "Data Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.CntrReg.CN02 Implement Cleanup Policies for Artifacts", + "SubSection": "", + "SubSectionObjective": "Ensure that unused or outdated artifacts are cleaned up according to defined policies to manage storage effectively and reduce security risks associated with outdated versions.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH14" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IP-6" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-12" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.IAM.CN01.AR01", + "Description": "When an identity policy for a non-administrative principal is evaluated, it MUST NOT grant permissions for creating credentials or generating temporary session tokens.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN01 Restrict IAM User Credentials Creation", + "SubSection": "", + "SubSectionObjective": "Prevent non-administrative principals from creating new long-lived credentials like access keys or generating temporary session tokens. This blocks a common privilege escalation and persistence vector.", "Applicability": [ "tlp-clear", "tlp-green", @@ -4411,9 +5635,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07", - "CCC.TH09", - "CCC.TH04" + "CCC.IAM.TH03" ] } ], @@ -4421,61 +5643,52 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-6" + "PR.AA-05" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AU-9" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-02", - "LOG-04", - "LOG-09" + "AC-2", + "AC-3", + "AC-5", + "AC-6" ] } ] } ], "Checks": [ - "cloudtrail_bucket_requires_mfa_delete", - "cloudtrail_log_file_validation_enabled", - "cloudtrail_kms_encryption_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_cloudwatch_logging_enabled", - "cloudtrail_multi_region_enabled", - "cloudtrail_s3_dataevents_read_enabled", - "cloudtrail_s3_dataevents_write_enabled" + "iam_no_root_access_key", + "iam_user_no_setup_initial_access_key", + "iam_user_two_active_access_key", + "iam_aws_attached_policy_no_administrative_privileges", + "iam_customer_attached_policy_no_administrative_privileges", + "iam_inline_policy_no_administrative_privileges" ] }, { - "Id": "CCC.Core.CN09.AR02", - "Description": "When the service is operational, disabling the logs for the service or its child resources MUST NOT be possible without also disabling the corresponding resource.", + "Id": "CCC.IAM.CN01.AR02", + "Description": "When a non-administrative principal attempts to create new credentials or a temporary session token, the service MUST deny the action.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN01 Restrict IAM User Credentials Creation", "SubSection": "", - "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "SubSectionObjective": "Prevent non-administrative principals from creating new long-lived credentials like access keys or generating temporary session tokens. This blocks a common privilege escalation and persistence vector.", "Applicability": [ "tlp-clear", "tlp-green", "tlp-amber", "tlp-red" ], - "Recommendation": "No normal business operations should disable logs, as this could indicate an attempt to cover up unauthorized access. Ensure that logging mechanisms are tightly integrated with service operations, so that logging cannot be disabled without stopping the service itself. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07", - "CCC.TH09", - "CCC.TH04" + "CCC.IAM.TH03" ] } ], @@ -4483,63 +5696,52 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-6" + "PR.AA-05" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AU-9" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-02", - "LOG-04", - "LOG-09" + "AC-2", + "AC-3", + "AC-5", + "AC-6" ] } ] } ], "Checks": [ - "cloudtrail_kms_encryption_enabled", - "cloudtrail_cloudwatch_logging_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_logs_s3_bucket_access_logging_enabled", - "cloudtrail_s3_dataevents_read_enabled", - "cloudtrail_s3_dataevents_write_enabled", - "cloudtrail_log_file_validation_enabled", - "vpc_flow_logs_enabled", - "apigateway_restapi_logging_enabled", - "apigatewayv2_api_access_logging_enabled", - "cloudwatch_log_group_not_publicly_accessible", - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled" + "iam_no_root_access_key", + "iam_user_no_setup_initial_access_key", + "iam_user_two_active_access_key", + "iam_aws_attached_policy_no_administrative_privileges", + "iam_customer_attached_policy_no_administrative_privileges", + "iam_inline_policy_no_administrative_privileges" ] }, { - "Id": "CCC.Core.CN09.AR03", - "Description": "When the service is operational, any attempt to redirect logs for the service or its child resources MUST NOT be possible without halting operation of the corresponding resource and publishing corresponding events to monitored channels.", + "Id": "CCC.IAM.CN02.AR01", + "Description": "When an identity policy for a non-administrative principal is evaluated, it MUST NOT grant permissions for creating, updating, or attaching policies.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN02 Restrict IAM Policies Modification", "SubSection": "", - "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "SubSectionObjective": "Ensure that only designated administrative accounts have the ability to create, modify, or attach policies that define permissions for other identities.", "Applicability": [ + "tlp-clear", + "tlp-green", "tlp-amber", "tlp-red" ], - "Recommendation": "No normal business operations should result in the redirection of logs, as this could indicate an attempt to cover up unauthorized access. Ensure that logging configurations are immutable during service operation so that any changes require stopping the service and publishing corresponding events to monitored channels. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07", - "CCC.TH09", - "CCC.TH04" + "CCC.IAM.TH06" ] } ], @@ -4547,55 +5749,91 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-6" + "PR.AA-05" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AU-9" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-02", - "LOG-04", - "LOG-09" + "AC-2", + "AC-3", + "AC-5", + "AC-6" ] } ] } ], "Checks": [ - "cloudtrail_insights_exist", - "cloudtrail_logs_s3_bucket_access_logging_enabled", - "cloudtrail_cloudwatch_logging_enabled", - "cloudtrail_s3_dataevents_read_enabled", - "cloudtrail_s3_dataevents_write_enabled", - "cloudtrail_kms_encryption_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_log_file_validation_enabled", - "cloudwatch_log_group_kms_encryption_enabled", - "cloudwatch_log_group_not_publicly_accessible", - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "cloudwatch_changes_to_network_acls_alarm_configured", - "cloudwatch_changes_to_network_gateways_alarm_configured", - "cloudwatch_changes_to_network_route_tables_alarm_configured", - "cloudwatch_changes_to_vpcs_alarm_configured", - "s3_bucket_server_access_logging_enabled" + "iam_aws_attached_policy_no_administrative_privileges", + "iam_customer_attached_policy_no_administrative_privileges", + "iam_customer_unattached_policy_no_administrative_privileges", + "iam_policy_allows_privilege_escalation", + "iam_inline_policy_allows_privilege_escalation" ] }, { - "Id": "CCC.Core.CN10.AR01", - "Description": "When data is replicated, the service MUST ensure that replication only occurs to destinations that are explicitly included within the defined trust perimeter.", + "Id": "CCC.IAM.CN02.AR02", + "Description": "When a non-administrative principal attempts to create, update, or attach policies, the service MUST deny the action.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN10 Restrict Data Replication to Trust Perimeter", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN02 Restrict IAM Policies Modification", "SubSection": "", - "SubSectionObjective": "Ensure that data is only replicated on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "SubSectionObjective": "Ensure that only designated administrative accounts have the ability to create, modify, or attach policies that define permissions for other identities.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3", + "AC-5", + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_aws_attached_policy_no_administrative_privileges", + "iam_customer_attached_policy_no_administrative_privileges", + "iam_customer_unattached_policy_no_administrative_privileges", + "iam_policy_allows_privilege_escalation", + "iam_inline_policy_allows_privilege_escalation" + ] + }, + { + "Id": "CCC.IAM.CN03.AR01", + "Description": "When a policy is created or updated that grants a principal permission to assume a role or impersonate a service identity, the principal MUST NOT contain a wildcard or be public/anonymous.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN03 Restrict Role Assumption / Delegation", + "SubSection": "", + "SubSectionObjective": "Limit which principals can assume a role or impersonate a service identity to only those required. This prevents unintended cross-account or public access by securing the \"who can act as this identity\" boundary.", "Applicability": [ "tlp-green", "tlp-amber", @@ -4606,7 +5844,1511 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH04" + "CCC.IAM.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_role_cross_account_readonlyaccess_policy", + "iam_role_cross_service_confused_deputy_prevention", + "iam_no_custom_policy_permissive_role_assumption" + ] + }, + { + "Id": "CCC.IAM.CN03.AR02", + "Description": "When an external or unauthenticated principal tries to assume a role or impersonate a service identity, the service MUST deny the action.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN03 Restrict Role Assumption / Delegation", + "SubSection": "", + "SubSectionObjective": "Limit which principals can assume a role or impersonate a service identity to only those required. This prevents unintended cross-account or public access by securing the \"who can act as this identity\" boundary.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_role_cross_account_readonlyaccess_policy", + "iam_role_cross_service_confused_deputy_prevention", + "iam_no_custom_policy_permissive_role_assumption" + ] + }, + { + "Id": "CCC.IAM.CN04.AR01", + "Description": "When an IAM policy is created or updated, it MUST NOT contain allow statements with wildcard permissions, unless the statement is restricted by a condition.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN04 Restrict Wildcard Usage in IAM Policies", + "SubSection": "", + "SubSectionObjective": "Limit the use of wildcard permissions in IAM policies to prevent overly broad access from being granted by default.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01", + "CCC.IAM.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-6" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_aws_attached_policy_no_administrative_privileges", + "iam_customer_attached_policy_no_administrative_privileges", + "iam_customer_unattached_policy_no_administrative_privileges", + "iam_inline_policy_no_administrative_privileges", + "iam_policy_no_full_access_to_kms", + "iam_policy_no_full_access_to_cloudtrail", + "iam_inline_policy_no_full_access_to_kms", + "iam_inline_policy_no_full_access_to_cloudtrail" + ] + }, + { + "Id": "CCC.IAM.CN05.AR01", + "Description": "When a new cloud account is provisioned, a password policy MUST be configured for IAM users following the minimum PCI DSS v4.0.1 configurations.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN05 Strong Password Policies for IAM Users", + "SubSection": "", + "SubSectionObjective": "Ensure that the password policies for IAM users have strong configurations.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "When a new cloud account is provisioned, a password policy must be configured for all IAM users to align with the minimum requirements defined in PCI DSS v4.0.1. This includes, at a minimum: strength: 0 # Not yet specified - reference-id: A password length of at least 12 characters. strength: 0 # Not yet specified - reference-id: A mix of upper- and lower-case letters, numbers, and special characters. strength: 0 # Not yet specified - reference-id: Prevention of the use of previously used passwords (password history). strength: 0 # Not yet specified - reference-id: Password expiration at a defined interval (e.g., every 90 days). strength: 0 # Not yet specified - reference-id: Account lockout after a defined number of failed login attempts.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "IA-5" + ] + }, + { + "ReferenceId": "PCI-DSS", + "Identifiers": [ + "8.3.9", + "8.6.3" + ] + } + ] + } + ], + "Checks": [ + "iam_password_policy_minimum_length_14", + "iam_password_policy_uppercase", + "iam_password_policy_lowercase", + "iam_password_policy_symbol", + "iam_password_policy_number", + "iam_password_policy_expires_passwords_within_90_days_or_less", + "iam_password_policy_reuse_24" + ] + }, + { + "Id": "CCC.IAM.CN06.AR01", + "Description": "When a static credential such as an access key has existed for 90 days or more, it MUST be rotated.", + "Attributes": [ + { + "FamilyName": "Identity Provisioning and Lifecycle", + "FamilyDescription": "Controls related to the provisioning and lifecycle of IAM identities.", + "Section": "CCC.IAM.CN06 Maximum Age for Long-Term Static Credentials", + "SubSection": "", + "SubSectionObjective": "Ensure that long-lived static credentials like access keys are programmatically rotated within a defined time period to limit the window of opportunity if compromised.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "When a static credential such as an access key has existed for 90 days or more, it must be automatically rotated to reduce the risk of compromise due to long-term exposure. Organizations should implement automated checks to identify aging credentials and enforce rotation policies. Additionally, access key usage should be regularly monitored, and credentials that are no longer in use should be deactivated or deleted promptly. Where possible, prefer temporary, short-lived credentials over long-lived static ones to further minimize risk.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH09", + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2" + ] + } + ] + } + ], + "Checks": [ + "iam_rotate_access_key_90_days" + ] + }, + { + "Id": "CCC.IAM.CN07.AR01", + "Description": "When a user account is disabled or deleted in the organization's IdP, the corresponding cloud identity and its access policies MUST be disabled or deleted within 24 hours.", + "Attributes": [ + { + "FamilyName": "Identity Provisioning and Lifecycle", + "FamilyDescription": "Controls related to the provisioning and lifecycle of IAM identities.", + "Section": "CCC.IAM.CN07 Automate Identity De-provisioning", + "SubSection": "", + "SubSectionObjective": "Ensure that when an identity is terminated in the central Identity Provider (IdP), ts corresponding access to cloud resources is revoked automatically.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH10", + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2" + ] + } + ] + } + ], + "Checks": [ + "iam_user_console_access_unused", + "iam_user_accesskey_unused" + ] + }, + { + "Id": "CCC.IAM.CN08.AR01", + "Description": "When an IAM user has credentials, such as passwords or access keys, that have not been used for 90 days or more, the unused credentials MUST be removed or deactivated.", + "Attributes": [ + { + "FamilyName": "Identity Provisioning and Lifecycle", + "FamilyDescription": "Controls related to the provisioning and lifecycle of IAM identities.", + "Section": "CCC.IAM.CN08 Maximum Age for Unused Credentials", + "SubSection": "", + "SubSectionObjective": "Ensure that unused IAM credentals are removed to reduce exposure in the event of potential compromise.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "IAM user credentials (such as passwords or access keys) that have not been used for 90 days or more must be automatically removed or deactivated.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH11", + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2" + ] + } + ] + } + ], + "Checks": [ + "iam_user_accesskey_unused", + "iam_user_console_access_unused" + ] + }, + { + "Id": "CCC.IAM.CN09.AR01", + "Description": "When a human user accesses the cloud environment, they MUST authenticate through the organization's federated IdP via a standard protocol (e.g., SAML, OIDC).", + "Attributes": [ + { + "FamilyName": "Identity Provisioning and Lifecycle", + "FamilyDescription": "Controls related to the provisioning and lifecycle of IAM identities.", + "Section": "CCC.IAM.CN09 Enforce Federated Single Sign-On (SSO) for Human Users", + "SubSection": "", + "SubSectionObjective": "Ensure that all human users must authenticate through a central, federated Identity Provider (IdP) to access the cloud environment. This eliminates cloud-native user accounts with long-lived passwords, centralizes authentication controls, and simplifies lifecycle management.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01", + "CCC.IAM.TH09" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "IA-2" + ] + } + ] + } + ], + "Checks": [ + "iam_check_saml_providers_sts" + ] + }, + { + "Id": "CCC.IAM.CN10.AR01", + "Description": "When suspicious API requests are detected, real time alerts MUST be generated to notify security personnel.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain IAM-related events.", + "Section": "CCC.IAM.CN10 Alert On Anomalous Behaviour", + "SubSection": "", + "SubSectionObjective": "Ensure that logs and associated alerts are generated when anomalous API requests are made by a single identity, such as API requests commonly associated with privilege escalation tactics, originating from an external or malicious IP address or performed by a previously dormant identity, which may indicate that credentals may be compromised, as well as for password brute-force attempts and account lockouts.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4", + "SI-5", + "AC-2" + ] + } + ] + } + ], + "Checks": [ + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_log_metric_filter_root_usage", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "guardduty_is_enabled" + ] + }, + { + "Id": "CCC.IAM.CN10.AR02", + "Description": "When suspicious API requests are detected, the associated events MUST be logged, including the source details, time, and nature of the activity.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain IAM-related events.", + "Section": "CCC.IAM.CN10 Alert On Anomalous Behaviour", + "SubSection": "", + "SubSectionObjective": "Ensure that logs and associated alerts are generated when anomalous API requests are made by a single identity, such as API requests commonly associated with privilege escalation tactics, originating from an external or malicious IP address or performed by a previously dormant identity, which may indicate that credentals may be compromised, as well as for password brute-force attempts and account lockouts.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4", + "SI-5", + "AC-2" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_multi_region_enabled", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_unauthorized_api_calls" + ] + }, + { + "Id": "CCC.IAM.CN11.AR01", + "Description": "When a cloud account or organization is provisioned, the native automated access and usage analysis services MUST be enabled to continuously monitor for external or public access to resources, and unused access.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain IAM-related events.", + "Section": "CCC.IAM.CN11 Enable Continuous IAM Access and Usage Analysis", + "SubSection": "", + "SubSectionObjective": "Enable and configure the cloud provider's native access and usage analysis services to continuously monitor for external access paths and internal unused access.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH02", + "CCC.IAM.TH10", + "CCC.IAM.TH11" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "ID.RA-01", + "ID.IM-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "CA-7", + "RA-5" + ] + } + ] + } + ], + "Checks": [ + "accessanalyzer_enabled", + "accessanalyzer_enabled_without_findings" + ] + }, + { + "Id": "CCC.GenAI.CN01.AR01", + "Description": "Untrusted input such as user queries, RAG data or tool output MUST be validated before it is passed to a GenAI model.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN01 Model Input Filtering and Sanitisation", + "SubSection": "", + "SubSectionObjective": "Inspect and validate input before it is passed to a GenAI model in order to filter or sanitise adversarial queries and prevent sensitive data leakage.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-003", + "AIR-PREV-017", + "AIR-PREV-002", + "AIR-DET-001" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Input Validation and Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0020", + "AML.M0021", + "AML.M0015" + ] + } + ] + } + ], + "Checks": [ + "bedrock_guardrail_prompt_attack_filter_enabled", + "bedrock_guardrail_sensitive_information_filter_enabled", + "bedrock_agent_guardrail_enabled" + ] + }, + { + "Id": "CCC.GenAI.CN01.AR02", + "Description": "If malicious patterns such as prompt injection or sensitive data are detected during input validation, the input MUST be blocked or sanitised.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN01 Model Input Filtering and Sanitisation", + "SubSection": "", + "SubSectionObjective": "Inspect and validate input before it is passed to a GenAI model in order to filter or sanitise adversarial queries and prevent sensitive data leakage.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-003", + "AIR-PREV-017", + "AIR-PREV-002", + "AIR-DET-001" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Input Validation and Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0020", + "AML.M0021", + "AML.M0015" + ] + } + ] + } + ], + "Checks": [ + "bedrock_guardrail_prompt_attack_filter_enabled", + "bedrock_guardrail_sensitive_information_filter_enabled", + "bedrock_agent_guardrail_enabled" + ] + }, + { + "Id": "CCC.GenAI.CN02.AR01", + "Description": "GenAI model output MUST be validated for format conformance, malicious patterns, sensitive data and inapropriate content before being passed to users, application or plugins.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN02 Model Output Filtering and Sanitisation", + "SubSection": "", + "SubSectionObjective": "Inspect and validate GenAI model output before passing it to users, applications or plugins in order to filter or sanitise insecure or unreliable output and prevent sensitive data leakage.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH03", + "CCC.GenAI.TH04", + "CCC.GenAI.TH05", + "CCC.GenAI.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-003", + "AIR-PREV-017", + "AIR-PREV-002", + "AIR-DET-001" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Output Validation and Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0020", + "AML.M0002" + ] + } + ] + } + ], + "Checks": [ + "bedrock_guardrail_sensitive_information_filter_enabled", + "bedrock_agent_guardrail_enabled" + ] + }, + { + "Id": "CCC.GenAI.CN02.AR02", + "Description": "In the event of policy violations, the AI-generated content MUST be redacted, encoded or rejected.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN02 Model Output Filtering and Sanitisation", + "SubSection": "", + "SubSectionObjective": "Inspect and validate GenAI model output before passing it to users, applications or plugins in order to filter or sanitise insecure or unreliable output and prevent sensitive data leakage.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH03", + "CCC.GenAI.TH04", + "CCC.GenAI.TH05", + "CCC.GenAI.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-003", + "AIR-PREV-017", + "AIR-PREV-002", + "AIR-DET-001" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Output Validation and Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0020", + "AML.M0002" + ] + } + ] + } + ], + "Checks": [ + "bedrock_guardrail_sensitive_information_filter_enabled", + "bedrock_agent_guardrail_enabled" + ] + }, + { + "Id": "CCC.GenAI.CN03.AR01", + "Description": "When data is designated for model training or RAG ingestion, then its source MUST be explicitly approved and its provenance documented.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN03 Data Provenance and Source Vetting", + "SubSection": "", + "SubSectionObjective": "Ensure that all data for training, fine-tuning or RAG comes from trusted, approved sources and is authorised for the intended purposes in order to prevent the initial introduction of malicious content or leaked sensitive data.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH02", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-006" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Training Data Management" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0025" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN03.AR02", + "Description": "Data from unvetted sources MUST NOT be used in production systems.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN03 Data Provenance and Source Vetting", + "SubSection": "", + "SubSectionObjective": "Ensure that all data for training, fine-tuning or RAG comes from trusted, approved sources and is authorised for the intended purposes in order to prevent the initial introduction of malicious content or leaked sensitive data.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH02", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-006" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Training Data Management" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0025" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN04.AR01", + "Description": "When data is ingested for training, fine-tuning or conversion to vector embeddings, it MUST be validated for sensitive information or malicious content.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN04 Sanitisation of Ingested Data", + "SubSection": "", + "SubSectionObjective": "Validate and sanitise all data ingested by GenAI systems from extenal sources or internal knowledge bases, whether for training, conversion to vector embeddings, or real-time retireval, in order to remove or redact poisoned or sensitive data before further processing.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH02", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-002" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Training Data Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0007" + ] + } + ] + } + ], + "Checks": [ + "bedrock_guardrail_sensitive_information_filter_enabled" + ] + }, + { + "Id": "CCC.GenAI.CN04.AR02", + "Description": "If sensitive data or malicious content is detected, it must be rejected, redacted or flagged for manual review.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN04 Sanitisation of Ingested Data", + "SubSection": "", + "SubSectionObjective": "Validate and sanitise all data ingested by GenAI systems from extenal sources or internal knowledge bases, whether for training, conversion to vector embeddings, or real-time retireval, in order to remove or redact poisoned or sensitive data before further processing.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH02", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-002" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Training Data Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0007" + ] + } + ] + } + ], + "Checks": [ + "bedrock_guardrail_sensitive_information_filter_enabled" + ] + }, + { + "Id": "CCC.GenAI.CN05.AR01", + "Description": "When a RAG-enabled system generates a response containing information retrieved from its knowledge base, then the response MUST include a verifiable citation that links back to the specific source document.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN05 Citations and Source Traceability", + "SubSection": "", + "SubSectionObjective": "Require the GenAI system to provide citations or direct links back to the source documents used to generate a response, in to enhance the transparency, trustworthiness, and verifiability of AI-generated content.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH09", + "CCC.GenAI.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-DET-013" + ] + } + ] + } + ], + "Checks": [ + "bedrock_model_invocation_logging_enabled", + "bedrock_model_invocation_logs_encryption_enabled" + ] + }, + { + "Id": "CCC.GenAI.CN06.AR01", + "Description": "When an LLM invokes an external tool (e.g., an API, a plugin), then the tool MUST operate with the least privileges required for performing its intended functionality.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.GenAI.CN06 Least Privilege for Plugins", + "SubSection": "", + "SubSectionObjective": "Restricts the permissions of any external tools the GenAI system can call to limit the potential damage if an agent is coerced to perform unintended actions or vulnerabilities in the tools are exploited.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH07", + "CCC.GenAI.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Agent Permissions" + ] + } + ] + } + ], + "Checks": [ + "bedrock_api_key_no_administrative_privileges", + "bedrock_api_key_no_long_term_credentials" + ] + }, + { + "Id": "CCC.GenAI.CN07.AR01", + "Description": "When an application makes an API call to a foundational model in a production environment, then it MUST specify an explicit version identifier.", + "Attributes": [ + { + "FamilyName": "Configuration Management", + "FamilyDescription": "The Configuration Management control family involves establishing, maintaining and monitoring the configuration of the service and related applications and infrastructure to ensure consistency, secure defaults and compliance.", + "Section": "CCC.GenAI.CN07 Model Version Pinning", + "SubSection": "", + "SubSectionObjective": "Mandate that applications are locked (\"pinned\") to a specific, tested version of a foundational model to prevent unexpected behaviour changes introduced by provider-side updates.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH10" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-010" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN08.AR01", + "Description": "When a new AI model is considered for production deployment, it MUST undergo a formal red teaming and quality assurance review.", + "Attributes": [ + { + "FamilyName": "Model Assurance and Evaluation", + "FamilyDescription": "The Model Assurance and Evaluation control family encompasses the proactiveand continuous processes of testing and validating the AI model's behavior to ensure it aligns with safety, ethical, and quality standards.", + "Section": "CCC.GenAI.CN08 Quality Control and Red Teaming", + "SubSection": "", + "SubSectionObjective": "Establish a formal program for quality evaluation and adversarial testing (red teaming) to ensure GenAI system meet all business, quality, security and compliance requirements before getting deployed into production environments.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH02", + "CCC.GenAI.TH04", + "CCC.GenAI.TH08", + "CCC.GenAI.TH10" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-005" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Adversarial Training and Testing", + "Red Teaming", + "Product Governance" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0008" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN08.AR02", + "Description": "If model quality review or red teaming identifies an issue that exceeds the organization's risk tolerance, the model MUST NOT be deployed until the issue is remediated.", + "Attributes": [ + { + "FamilyName": "Model Assurance and Evaluation", + "FamilyDescription": "The Model Assurance and Evaluation control family encompasses the proactiveand continuous processes of testing and validating the AI model's behavior to ensure it aligns with safety, ethical, and quality standards.", + "Section": "CCC.GenAI.CN08 Quality Control and Red Teaming", + "SubSection": "", + "SubSectionObjective": "Establish a formal program for quality evaluation and adversarial testing (red teaming) to ensure GenAI system meet all business, quality, security and compliance requirements before getting deployed into production environments.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH02", + "CCC.GenAI.TH04", + "CCC.GenAI.TH08", + "CCC.GenAI.TH10" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-005" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Adversarial Training and Testing", + "Red Teaming", + "Product Governance" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0008" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN01.AR01", + "Description": "Verify that only authorized users can access MLDE resources, and that access modes are properly defined and enforced.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN01 Define Access Mode for ML Development Environments", + "SubSection": "", + "SubSectionObjective": "Ensure that access to Machine Learning Development Environment (MLDE) resources is strictly defined and controlled. Only authorized users with appropriate permissions can access these environments, mitigating the risk of unauthorized access, data leakage, or service disruption.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01", + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.1.1", + "2013 A.9.2.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-02" + ] + } + ] + } + ], + "Checks": [ + "sagemaker_notebook_instance_root_access_disabled", + "sagemaker_notebook_instance_vpc_settings_configured" + ] + }, + { + "Id": "CCC.MLDE.CN03.AR01", + "Description": "Verify that root access is disabled on MLDE instances containing sensitive data.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN03 Disable Root Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent users from obtaining root access on MLDE instances to reduce the risk of unauthorized system modifications and potential security breaches.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08", + "IAM-12" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.2.3" + ] + } + ] + } + ], + "Checks": [ + "sagemaker_notebook_instance_root_access_disabled" + ] + }, + { + "Id": "CCC.MLDE.CN03.AR02", + "Description": "For MLDE instances without sensitive data, ensure that root access is only enabled when necessary and properly authorized.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN03 Disable Root Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent users from obtaining root access on MLDE instances to reduce the risk of unauthorized system modifications and potential security breaches.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08", + "IAM-12" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.2.3" + ] + } + ] + } + ], + "Checks": [ + "sagemaker_notebook_instance_root_access_disabled" + ] + }, + { + "Id": "CCC.MLDE.CN04.AR01", + "Description": "Verify that terminal access is disabled on MLDE instances containing sensitive data.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN04 Disable Terminal Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent users from accessing the terminal on MLDE instances to limit the risk of unauthorized commands and potential system compromise.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.2.3" + ] + } + ] + } + ], + "Checks": [ + "sagemaker_notebook_instance_root_access_disabled" + ] + }, + { + "Id": "CCC.MLDE.CN04.AR02", + "Description": "For MLDE instances without sensitive data, ensure that terminal access is only enabled when necessary and properly authorized.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN04 Disable Terminal Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent users from accessing the terminal on MLDE instances to limit the risk of unauthorized commands and potential system compromise.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.2.3" + ] + } + ] + } + ], + "Checks": [ + "sagemaker_notebook_instance_root_access_disabled" + ] + }, + { + "Id": "CCC.MLDE.CN02.AR01", + "Description": "Confirm that file download functionality is disabled on MLDE instances containing sensitive data.", + "Attributes": [ + { + "FamilyName": "Data Protection", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN02 Disable File Downloads on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent unauthorized file downloads from MLDE instances to protect sensitive data from being exfiltrated.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH02", + "CCC.Core.TH02" ] } ], @@ -4620,166 +7362,98 @@ { "ReferenceId": "CCM", "Identifiers": [ - "DSP-10", - "DSP-19" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-4" - ] - } - ] - } - ], - "Checks": [ - "vpc_endpoint_services_allowed_principals_trust_boundaries", - "vpc_endpoint_connections_trust_boundaries", - "s3_bucket_cross_account_access" - ] - }, - { - "Id": "CCC.Core.CN02.AR01", - "Description": "When data is stored, it MUST be encrypted using the latest industry-standard encryption methods.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN02 Encrypt Data for Storage", - "SubSection": "", - "SubSectionObjective": "Ensure that all data stored is encrypted at rest using strong encryption algorithms.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "UEM-08", - "DSP-17" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-13", - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "s3_bucket_default_encryption", - "s3_bucket_kms_encryption", - "firehose_stream_encrypted_at_rest", - "backup_vaults_encrypted", - "backup_recovery_point_encrypted", - "cloudtrail_kms_encryption_enabled", - "cloudwatch_log_group_kms_encryption_enabled", - "opensearch_service_domains_encryption_at_rest_enabled", - "opensearch_service_domains_node_to_node_encryption_enabled", - "kafka_cluster_encryption_at_rest_uses_cmk", - "kinesis_stream_encrypted_at_rest", - "dynamodb_tables_kms_cmk_encryption_enabled", - "dynamodb_accelerator_cluster_encryption_enabled", - "ec2_ebs_default_encryption", - "ec2_ebs_volume_encryption", - "storagegateway_fileshare_encryption_enabled" - ] - }, - { - "Id": "CCC.Core.CN11.AR01", - "Description": "When encryption keys are used, the service MUST verify that all encryption keys use the latest industry-standard cryptographic algorithms.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH16" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" + "DSI-05", + "DSI-07" ] }, { "ReferenceId": "ISO_27001", "Identifiers": [ - "2013 A.10.1.2" + "2013 A.13.2.1" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "SC-12", - "SC-17" + "SC-7", + "SC-8" ] } ] } ], - "Checks": [ - "kms_cmk_rotation_enabled", - "kms_cmk_not_deleted_unintentionally", - "kms_cmk_not_multi_region", - "kms_cmk_are_used" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN11.AR02", - "Description": "When encryption keys are used, the service MUST rotate active keys within 180 days of issuance.", + "Id": "CCC.MLDE.CN02.AR02", + "Description": "For MLDE instances without sensitive data, ensure that file downloads are monitored and logged.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", + "FamilyName": "Data Protection", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN02 Disable File Downloads on MLDE Instances", "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "SubSectionObjective": "Prevent unauthorized file downloads from MLDE instances to protect sensitive data from being exfiltrated.", "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH02", + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-5" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSI-05", + "DSI-07" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.13.2.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7", + "SC-8" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN05.AR01", + "Description": "Verify that only approved VM and container images can be selected when creating MLDE instances.", + "Attributes": [ + { + "FamilyName": "Configuration Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN05 Restrict Environment Options on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Limit the virtual machine and container image options available when creating new MLDE instances to approved and secure configurations.", + "Applicability": [ + "tlp-red", "tlp-amber" ], "Recommendation": "", @@ -4787,7 +7461,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH16" + "CCC.MLDE.TH04" ] } ], @@ -4795,52 +7469,43 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" + "PR.IP-1" ] }, { "ReferenceId": "CCM", "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" + "TVM-02" ] }, { "ReferenceId": "ISO_27001", "Identifiers": [ - "2013 A.10.1.2" + "2013 A.12.5.1" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "SC-12", - "SC-17" + "CM-2" ] } ] } ], - "Checks": [ - "kms_cmk_rotation_enabled", - "kms_cmk_not_deleted_unintentionally", - "kms_cmk_not_multi_region", - "kms_cmk_are_used" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN11.AR03", - "Description": "When encrypting data, the service MUST verify that customer-managed encryption keys (CMEKs) are used.", + "Id": "CCC.MLDE.CN05.AR02", + "Description": "Attempt to create an MLDE instance with an unapproved image and confirm that it is denied.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "", - "SubSection": "CCC.Core.CN11 Protect Encryption Keys", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "FamilyName": "Configuration Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN05 Restrict Environment Options on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Limit the virtual machine and container image options available when creating new MLDE instances to approved and secure configurations.", "Applicability": [ - "tlp-amber", "tlp-red" ], "Recommendation": "", @@ -4848,7 +7513,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH16" + "CCC.MLDE.TH04" ] } ], @@ -4856,62 +7521,109 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" + "PR.IP-1" ] }, { "ReferenceId": "CCM", "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" + "TVM-02" ] }, { "ReferenceId": "ISO_27001", "Identifiers": [ - "2013 A.10.1.2" + "2013 A.12.5.1" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "SC-12", - "SC-17" + "CM-2" ] } ] } ], - "Checks": [ - "kms_cmk_not_deleted_unintentionally", - "kms_cmk_rotation_enabled", - "kms_cmk_not_multi_region", - "kms_cmk_are_used" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN11.AR04", - "Description": "When encryption keys are accessed, the service MUST verify that access to encryption keys is restricted to authorized personnel and services, following the principle of least privilege.", + "Id": "CCC.MLDE.CN06.AR01", + "Description": "Verify that automatic scheduled upgrades are enabled on user-managed MLDE instances containing sensitive data.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", + "FamilyName": "Vulnerability Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN06 Require Automatic Scheduled Upgrades on User-Managed MLDE Instances", "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "SubSectionObjective": "Ensure that MLDE instances are kept up-to-date with the latest security patches by enforcing automatic scheduled upgrades.", "Applicability": [ - "tlp-clear", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH04", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IP-12" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "TVM-01", + "TVM-02" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.12.6.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN06.AR02", + "Description": "Ensure that the upgrade schedule is appropriately configured and does not interfere with critical operations.", + "Attributes": [ + { + "FamilyName": "Vulnerability Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN06 Require Automatic Scheduled Upgrades on User-Managed MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Ensure that MLDE instances are kept up-to-date with the latest security patches by enforcing automatic scheduled upgrades.", + "Applicability": [ + "tlp-red", + "tlp-amber", "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-clear" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH16" + "CCC.MLDE.TH04", + "CCC.Core.TH06" ] } ], @@ -4919,109 +7631,43 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" + "PR.IP-12" ] }, { "ReferenceId": "CCM", "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" + "TVM-01", + "TVM-02" ] }, { "ReferenceId": "ISO_27001", "Identifiers": [ - "2013 A.10.1.2" + "2013 A.12.6.1" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "SC-12", - "SC-17" + "SI-2" ] } ] } ], - "Checks": [ - "kms_cmk_rotation_enabled", - "kms_cmk_not_deleted_unintentionally", - "kms_cmk_not_multi_region", - "kms_cmk_are_used" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN11.AR05", - "Description": "When encryption keys are used, the service MUST rotate active keys within 365 days of issuance.", + "Id": "CCC.MLDE.CN07.AR01", + "Description": "Verify that MLDE instances containing sensitive data cannot be accessed via public IP addresses.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN07 Restrict Public IP Access on MLDE Instances", "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", - "Applicability": [ - "tlp-clear", - "tlp-green" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH16" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-12", - "SC-17" - ] - } - ] - } - ], - "Checks": [ - "kms_cmk_rotation_enabled", - "kms_cmk_not_multi_region" - ] - }, - { - "Id": "CCC.Core.CN11.AR06", - "Description": "When encryption keys are used, the service MUST rotate active keys within 90 days of issuance.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "SubSectionObjective": "Prevent public IP access to MLDE instances to reduce exposure to the internet and enhance security.", "Applicability": [ "tlp-red" ], @@ -5030,444 +7676,8 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH16" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-12", - "SC-17" - ] - } - ] - } - ], - "Checks": [ - "kms_cmk_rotation_enabled" - ] - }, - { - "Id": "CCC.Core.CN14.AR01", - "Description": "When backups are created for disaster recovery purposes, the storage mechanism MUST NOT allow modification or deletion within 30 days of creation.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN14 Maintain Recent Backups", - "SubSection": "", - "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Use immutable storage solutions where possible. Implement backup retention policies that enforce a minimum retention period of 30 days. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "neptune_cluster_backup_enabled" - ] - }, - { - "Id": "CCC.Core.CN14.AR02", - "Description": "When backups are created for disaster recovery purposes, the most recent backup MUST have a creation date within the past 30 days.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN14 Maintain Recent Backups", - "SubSection": "", - "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber" - ], - "Recommendation": "Implement automated backup processes to ensure that backups are created regularly. Monitor backup schedules and verify that the most recent backup creation date is within the last 30 days. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "backup_vaults_exist", - "backup_plans_exist", - "backup_reportplans_exist", - "backup_vaults_encrypted", - "backup_recovery_point_encrypted", - "neptune_cluster_backup_enabled", - "rds_instance_backup_enabled", - "rds_instance_protected_by_backup_plan", - "rds_cluster_protected_by_backup_plan", - "dynamodb_table_protected_by_backup_plan" - ] - }, - { - "Id": "CCC.Core.CN14.AR02", - "Description": "When backups are created for disaster recovery purposes, the most recent backup MUST have a creation date within the past 14 days.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN14 Maintain Recent Backups", - "SubSection": "", - "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "Implement automated backup processes to ensure that backups are created regularly. Monitor backup schedules and verify that the most recent backup creation date is within the last 14 days. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "backup_vaults_exist", - "backup_vaults_encrypted", - "backup_plans_exist", - "backup_reportplans_exist", - "backup_recovery_point_encrypted", - "neptune_cluster_backup_enabled" - ] - }, - { - "Id": "CCC.Core.CN03.AR01", - "Description": "When an entity attempts to modify the service through a user interface, the authentication process MUST require multiple identifying factors for authentication.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "", - "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-14" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-03", - "IAM-08" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "IA-2" - ] - } - ] - } - ], - "Checks": [ - "iam_root_mfa_enabled", - "iam_root_hardware_mfa_enabled", - "iam_user_mfa_enabled_console_access", - "iam_administrator_access_with_mfa", - "cognito_user_pool_mfa_enabled" - ] - }, - { - "Id": "CCC.Core.CN03.AR02", - "Description": "When an entity attempts to modify the service through an API endpoint, the authentication process MUST require a credential such as an API key or token AND originate from within the trust perimeter.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "", - "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-14" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-03", - "IAM-08" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "IA-2" - ] - } - ] - } - ], - "Checks": [ - "iam_root_mfa_enabled", - "iam_root_hardware_mfa_enabled", - "iam_user_mfa_enabled_console_access", - "iam_administrator_access_with_mfa", - "cognito_user_pool_mfa_enabled" - ] - }, - { - "Id": "CCC.Core.CN03.AR03", - "Description": "When an entity attempts to view information on the service through a user interface, the authentication process MUST require multiple identifying factors from the user.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "", - "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-14" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-03", - "IAM-08" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "IA-2" - ] - } - ] - } - ], - "Checks": [ - "cognito_user_pool_mfa_enabled", - "iam_root_mfa_enabled", - "iam_root_hardware_mfa_enabled", - "iam_user_mfa_enabled_console_access", - "iam_user_hardware_mfa_enabled", - "iam_administrator_access_with_mfa" - ] - }, - { - "Id": "CCC.Core.CN03.AR04", - "Description": "When an entity attempts to view information on the service through an API endpoint, the authentication process MUST require a credential such as an API key or token AND originate from within the trust perimeter.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "", - "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-14" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-03", - "IAM-08" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "IA-2" - ] - } - ] - } - ], - "Checks": [ - "iam_user_mfa_enabled_console_access", - "iam_root_mfa_enabled", - "iam_root_hardware_mfa_enabled", - "iam_user_hardware_mfa_enabled", - "iam_administrator_access_with_mfa", - "apigateway_restapi_authorizers_enabled", - "apigateway_restapi_public_with_authorizer" - ] - }, - { - "Id": "CCC.Core.CN05.AR01", - "Description": "When an attempt is made to modify data on the service or a child resource, the service MUST block requests from unauthorized entities.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" + "CCC.MLDE.TH02", + "CCC.VPC.TH02" ] } ], @@ -5481,75 +7691,317 @@ { "ReferenceId": "CCM", "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" + "SEF-05" ] }, { "ReferenceId": "ISO_27001", "Identifiers": [ - "2013 A.13.1.3" + "2013 A.13.1.1" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AC-3" + "SC-7" ] } ] } ], "Checks": [ - "apigateway_restapi_authorizers_enabled", - "apigateway_restapi_public", - "apigateway_restapi_public_with_authorizer", - "apigatewayv2_api_authorizers_enabled", - "awslambda_function_url_public", + "sagemaker_notebook_instance_without_direct_internet_access_configured" + ] + }, + { + "Id": "CCC.MLDE.CN07.AR02", + "Description": "For MLDE instances without sensitive data requiring public access, ensure that appropriate security controls are in place and access is approved.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN07 Restrict Public IP Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent public IP access to MLDE instances to reduce exposure to the internet and enhance security.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH02", + "CCC.VPC.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "SEF-05" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.13.1.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7" + ] + } + ] + } + ], + "Checks": [ + "sagemaker_notebook_instance_without_direct_internet_access_configured" + ] + }, + { + "Id": "CCC.MLDE.CN08.AR01", + "Description": "Verify that MLDE instances containing sensitive data can only be deployed in approved virtual networks with appropriate security controls.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN08 Restrict Virtual Networks for MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Limit the virtual networks that can be used when creating new MLDE instances to ensure they are deployed within approved and secure network environments.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01", + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-12" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.1.2" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "sagemaker_notebook_instance_vpc_settings_configured", + "sagemaker_models_vpc_settings_configured", + "sagemaker_training_jobs_vpc_settings_configured" + ] + }, + { + "Id": "CCC.MLDE.CN08.AR02", + "Description": "Ensure that MLDE instances without sensitive data are deployed in networks that meet organizational security standards.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN08 Restrict Virtual Networks for MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Limit the virtual networks that can be used when creating new MLDE instances to ensure they are deployed within approved and secure network environments.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01", + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-12" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.1.2" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "sagemaker_notebook_instance_vpc_settings_configured", + "sagemaker_models_vpc_settings_configured", + "sagemaker_training_jobs_vpc_settings_configured" + ] + }, + { + "Id": "CCC.Message.CN01.AR01", + "Description": "Attempt to publish a message without using a customer-managed encryption key and verify that the message is rejected or not stored.", + "Attributes": [ + { + "FamilyName": "Encryption", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.Message.CN01 Use Customer-Managed Encryption Keys (CMEK) for Messages", + "SubSection": "", + "SubSectionObjective": "Ensure that messages are encrypted using customer-managed encryption keys (CMEK) to provide enhanced control over encryption processes and keys, meeting compliance and security requirements.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-12", + "SC-13" + ] + } + ] + } + ], + "Checks": [ + "sns_topics_kms_encryption_at_rest_enabled", + "sqs_queues_server_side_encryption_enabled", + "kafka_cluster_encryption_at_rest_uses_cmk" + ] + }, + { + "Id": "CCC.SvlsComp.CN01.AR01", + "Description": "Attempt to access the serverless function over the public internet and verify that access is denied.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.SvlsComp.CN01 Enforce Use of Private Endpoints for Serverless Function", + "SubSection": "", + "SubSectionObjective": "Ensure that the serverless function is accessible only through a private endpoint, allowing it to communicate securely within a virtual private network and preventing unauthorized external access.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-5" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7", + "SC-8" + ] + } + ] + } + ], + "Checks": [ + "awslambda_function_inside_vpc", "awslambda_function_not_publicly_accessible", - "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", - "s3_bucket_public_access", - "s3_bucket_public_list_acl", - "s3_bucket_public_write_acl", - "s3_bucket_cross_account_access", - "s3_account_level_public_access_blocks", - "iam_policy_no_full_access_to_cloudtrail", - "iam_policy_no_full_access_to_kms", - "iam_inline_policy_no_full_access_to_cloudtrail", - "iam_inline_policy_no_full_access_to_kms", - "iam_role_administratoraccess_policy", - "iam_group_administrator_access_policy", - "iam_user_administrator_access_policy", - "iam_policy_attached_only_to_group_or_roles", - "iam_role_cross_account_readonlyaccess_policy", - "iam_role_cross_service_confused_deputy_prevention" + "awslambda_function_url_public" ] }, { - "Id": "CCC.Core.CN05.AR02", - "Description": "When administrative access or configuration change is attempted on the service or a child resource, the service MUST refuse requests from unauthorized entities.", + "Id": "CCC.SvlsComp.CN02.AR01", + "Description": "Send requests to invoke the function up to the allowed threshold and confirm they are successful; then send additional requests exceeding the threshold from the same entity and verify that they are denied.", "Attributes": [ { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "FamilyName": "Availability", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.SvlsComp.CN02 Implement Function Invocation Rate Limits", "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "SubSectionObjective": "Ensure that function invocation is limited to a specified threshold from any single entity, preventing resource exhaustion and denial of service attacks.", "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-red", + "tlp-amber" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH12" ] } ], @@ -5557,650 +8009,19 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" + "PR.DS-4" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AC-3" + "SC-5" ] } ] } ], - "Checks": [ - "iam_root_mfa_enabled", - "iam_root_hardware_mfa_enabled", - "iam_avoid_root_usage", - "iam_user_mfa_enabled_console_access", - "iam_administrator_access_with_mfa", - "iam_group_administrator_access_policy", - "iam_role_administratoraccess_policy", - "iam_inline_policy_no_full_access_to_cloudtrail", - "iam_inline_policy_no_full_access_to_kms", - "iam_policy_no_full_access_to_cloudtrail", - "iam_policy_no_full_access_to_kms", - "iam_policy_allows_privilege_escalation", - "iam_inline_policy_allows_privilege_escalation", - "iam_customer_attached_policy_no_administrative_privileges", - "iam_customer_unattached_policy_no_administrative_privileges", - "iam_aws_attached_policy_no_administrative_privileges", - "iam_password_policy_minimum_length_14", - "iam_password_policy_uppercase", - "iam_password_policy_lowercase", - "iam_password_policy_symbol", - "iam_password_policy_number", - "iam_password_policy_expires_passwords_within_90_days_or_less", - "iam_password_policy_reuse_24", - "iam_check_saml_providers_sts", - "iam_policy_attached_only_to_group_or_roles" - ] - }, - { - "Id": "CCC.Core.CN05.AR03", - "Description": "When administrative access or configuration change is attempted on the service or a child resource in a multi-tenant environment, the service MUST refuse requests across tenant boundaries unless the origin is explicitly included in a pre-approved allowlist.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "vpc_endpoint_services_allowed_principals_trust_boundaries", - "vpc_endpoint_connections_trust_boundaries", - "iam_role_cross_service_confused_deputy_prevention", - "iam_role_cross_account_readonlyaccess_policy", - "s3_bucket_cross_account_access", - "eventbridge_bus_cross_account_access", - "eventbridge_schema_registry_cross_account_access" - ] - }, - { - "Id": "CCC.Core.CN05.AR04", - "Description": "When data is requested from outside the trust perimeter, the service MUST refuse requests from unauthorized entities.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "", - "SubSection": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "accessanalyzer_enabled", - "accessanalyzer_enabled_without_findings", - "vpc_endpoint_connections_trust_boundaries", - "vpc_endpoint_services_allowed_principals_trust_boundaries", - "s3_bucket_cross_account_access", - "s3_bucket_public_access", - "iam_administrator_access_with_mfa", - "iam_group_administrator_access_policy", - "iam_user_administrator_access_policy", - "iam_inline_policy_allows_privilege_escalation", - "iam_inline_policy_no_full_access_to_kms", - "iam_inline_policy_no_full_access_to_cloudtrail", - "iam_policy_no_full_access_to_kms", - "iam_policy_no_full_access_to_cloudtrail", - "iam_policy_attached_only_to_group_or_roles", - "iam_user_mfa_enabled_console_access" - ] - }, - { - "Id": "CCC.Core.CN05.AR05", - "Description": "When any request is made from outside the trust perimeter, the service MUST NOT provide any response that may indicate the service exists.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "awslambda_function_url_public", - "apigateway_restapi_public", - "apigateway_restapi_public_with_authorizer", - "s3_bucket_public_list_acl", - "s3_bucket_public_write_acl", - "s3_bucket_public_access", - "s3_bucket_policy_public_write_access", - "sns_topics_not_publicly_accessible", - "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", - "ec2_networkacl_allow_ingress_any_port", - "ec2_networkacl_allow_ingress_tcp_port_22", - "ec2_networkacl_allow_ingress_tcp_port_3389", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", - "ec2_securitygroup_default_restrict_traffic", - "vpc_endpoint_services_allowed_principals_trust_boundaries", - "vpc_endpoint_connections_trust_boundaries", - "vpc_endpoint_for_ec2_enabled" - ] - }, - { - "Id": "CCC.Core.CN05.AR06", - "Description": "When any request is made to the service or a child resource, the service MUST refuse requests from unauthorized entities.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "vpc_endpoint_connections_trust_boundaries", - "vpc_endpoint_services_allowed_principals_trust_boundaries", - "iam_role_cross_service_confused_deputy_prevention", - "iam_role_cross_account_readonlyaccess_policy", - "iam_policy_attached_only_to_group_or_roles", - "iam_group_administrator_access_policy", - "iam_user_mfa_enabled_console_access", - "iam_root_mfa_enabled", - "iam_root_hardware_mfa_enabled", - "iam_no_root_access_key", - "iam_administrator_access_with_mfa", - "iam_root_credentials_management_enabled", - "iam_check_saml_providers_sts", - "iam_user_hardware_mfa_enabled" - ] - }, - { - "Id": "CCC.Core.CN04.AR01", - "Description": "When administrative access or configuration change is attempted on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN04 Log All Access and Changes", - "SubSection": "", - "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-08" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3", - "AU-12" - ] - } - ] - } - ], - "Checks": [ - "cloudtrail_multi_region_enabled", - "cloudtrail_cloudwatch_logging_enabled", - "cloudtrail_log_file_validation_enabled", - "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", - "cloudwatch_log_metric_filter_authentication_failures", - "cloudwatch_log_metric_filter_unauthorized_api_calls", - "cloudwatch_log_metric_filter_root_usage", - "cloudwatch_log_metric_filter_sign_in_without_mfa", - "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", - "cloudwatch_log_metric_filter_policy_changes", - "cloudwatch_log_metric_filter_security_group_changes", - "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", - "cloudwatch_changes_to_network_acls_alarm_configured", - "cloudwatch_changes_to_network_gateways_alarm_configured", - "cloudwatch_changes_to_network_route_tables_alarm_configured", - "cloudwatch_changes_to_vpcs_alarm_configured", - "vpc_flow_logs_enabled", - "apigateway_restapi_logging_enabled", - "apigatewayv2_api_access_logging_enabled" - ] - }, - { - "Id": "CCC.Core.CN04.AR02", - "Description": "When any attempt is made to modify data on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN04 Log All Access and Changes", - "SubSection": "", - "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-08" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3", - "AU-12" - ] - } - ] - } - ], - "Checks": [ - "cloudtrail_s3_dataevents_read_enabled", - "cloudtrail_s3_dataevents_write_enabled", - "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", - "cloudtrail_multi_region_enabled_logging_management_events", - "cloudtrail_cloudwatch_logging_enabled", - "vpc_flow_logs_enabled", - "apigateway_restapi_logging_enabled", - "apigatewayv2_api_access_logging_enabled", - "cloudtrail_threat_detection_enumeration", - "cloudtrail_threat_detection_privilege_escalation", - "cloudtrail_threat_detection_llm_jacking" - ] - }, - { - "Id": "CCC.Core.CN04.AR03", - "Description": "When any attempt is made to read data on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN04 Log All Access and Changes", - "SubSection": "", - "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-08" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3", - "AU-12" - ] - } - ] - } - ], - "Checks": [ - "cloudtrail_insights_exist", - "cloudtrail_multi_region_enabled", - "cloudtrail_cloudwatch_logging_enabled", - "cloudtrail_kms_encryption_enabled", - "cloudtrail_log_file_validation_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_s3_dataevents_read_enabled", - "cloudtrail_s3_dataevents_write_enabled", - "cloudwatch_log_group_kms_encryption_enabled", - "cloudwatch_log_group_not_publicly_accessible", - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "cloudwatch_log_metric_filter_authentication_failures", - "apigateway_restapi_logging_enabled", - "apigatewayv2_api_access_logging_enabled", - "vpc_flow_logs_enabled" - ] - }, - { - "Id": "CCC.Core.CN07.AR01", - "Description": "When enumeration activities are detected, the service MUST publish an event to a monitored channel which includes the client identity, time, and nature of the activity.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN07 Alert on Unusual Enumeration Activity", - "SubSection": "", - "SubSectionObjective": "Ensure that logs and associated alerts are generated when unusual enumeration activity is detected that may indicate reconnaissance activities.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Implement event publication mechanisms and alerts for patterns indicative of enumeration activities, such as repeated access attempts, requests, or liveness probes. Configure alerts to notify security teams of any activities that merit further investigation. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH15" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-05", - "SEF-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-6" - ] - } - ] - } - ], - "Checks": [ - "cloudtrail_threat_detection_enumeration" - ] - }, - { - "Id": "CCC.Core.CN07.AR02", - "Description": "When enumeration activities are detected, the service MUST log the client identity, time, and nature of the activity.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN07 Alert on Unusual Enumeration Activity", - "SubSection": "", - "SubSectionObjective": "Ensure that logs and associated alerts are generated when unusual enumeration activity is detected that may indicate reconnaissance activities.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Implement logging mechanisms to capture details of enumeration activities, including client identity, timestamps, and activity nature. Retain logs according to organizational policies, and occasionally review them for patterns that may indicate reconnaissance activities. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH15" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-05", - "SEF-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-6" - ] - } - ] - } - ], - "Checks": [ - "cloudtrail_threat_detection_enumeration" - ] + "Checks": [] } ] } diff --git a/prowler/compliance/aws/cisa_aws.json b/prowler/compliance/aws/cisa_aws.json index 6dc8edab68..fa8e27a061 100644 --- a/prowler/compliance/aws/cisa_aws.json +++ b/prowler/compliance/aws/cisa_aws.json @@ -183,6 +183,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "elbv2_ssl_listeners", "iam_no_custom_policy_permissive_role_assumption", "iam_aws_attached_policy_no_administrative_privileges", diff --git a/prowler/compliance/aws/csa_ccm_4.0_aws.json b/prowler/compliance/aws/csa_ccm_4.0_aws.json index 6d0a019c2b..d69ed6fe1d 100644 --- a/prowler/compliance/aws/csa_ccm_4.0_aws.json +++ b/prowler/compliance/aws/csa_ccm_4.0_aws.json @@ -3098,6 +3098,8 @@ } ], "Checks": [ + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_user_two_active_access_key" @@ -3317,6 +3319,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges", @@ -3436,6 +3440,8 @@ } ], "Checks": [ + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_user_no_setup_initial_access_key" @@ -3543,6 +3549,8 @@ } ], "Checks": [ + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_rotate_access_key_90_days", diff --git a/prowler/compliance/aws/ens_rd2022_aws.json b/prowler/compliance/aws/ens_rd2022_aws.json index 1d25aeb3b3..556f0f8868 100644 --- a/prowler/compliance/aws/ens_rd2022_aws.json +++ b/prowler/compliance/aws/ens_rd2022_aws.json @@ -542,6 +542,8 @@ } ], "Checks": [ + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] diff --git a/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json b/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json index 60afe9d4fd..c97c407e61 100644 --- a/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json +++ b/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json @@ -86,6 +86,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_administrator_access_with_mfa", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", @@ -105,6 +107,8 @@ "iam_root_hardware_mfa_enabled", "iam_root_mfa_enabled", "iam_rotate_access_key_90_days", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_user_hardware_mfa_enabled", @@ -319,6 +323,8 @@ "iam_no_root_access_key", "iam_policy_attached_only_to_group_or_roles", "iam_rotate_access_key_90_days", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "organizations_delegated_administrators" diff --git a/prowler/compliance/aws/fedramp_low_revision_4_aws.json b/prowler/compliance/aws/fedramp_low_revision_4_aws.json index e06a2a75ad..cf2c0592e1 100644 --- a/prowler/compliance/aws/fedramp_low_revision_4_aws.json +++ b/prowler/compliance/aws/fedramp_low_revision_4_aws.json @@ -37,6 +37,8 @@ "iam_rotate_access_key_90_days", "iam_user_mfa_enabled_console_access", "iam_user_hardware_mfa_enabled", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "rds_instance_integration_cloudwatch_logs", diff --git a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json index 7ca5160ff3..76b6f9aa6c 100644 --- a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json +++ b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json @@ -30,6 +30,8 @@ "iam_rotate_access_key_90_days", "iam_user_mfa_enabled_console_access", "iam_user_mfa_enabled_console_access", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "securityhub_enabled" @@ -105,6 +107,8 @@ "iam_rotate_access_key_90_days", "iam_user_mfa_enabled_console_access", "iam_user_mfa_enabled_console_access", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -159,6 +163,8 @@ "iam_rotate_access_key_90_days", "iam_user_mfa_enabled_console_access", "iam_user_mfa_enabled_console_access", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -177,6 +183,8 @@ ], "Checks": [ "iam_password_policy_minimum_length_14", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -300,6 +308,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "ec2_ebs_public_snapshot", "ec2_instance_public_ip", "ec2_instance_imdsv2_enabled", @@ -308,6 +318,8 @@ "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges", "iam_no_root_access_key", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "awslambda_function_not_publicly_accessible", diff --git a/prowler/compliance/aws/ffiec_aws.json b/prowler/compliance/aws/ffiec_aws.json index d1bb22f4cc..697d9ee49f 100644 --- a/prowler/compliance/aws/ffiec_aws.json +++ b/prowler/compliance/aws/ffiec_aws.json @@ -418,6 +418,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "ec2_instance_profile_attached", "iam_policy_attached_only_to_group_or_roles", "iam_aws_attached_policy_no_administrative_privileges", diff --git a/prowler/compliance/aws/hipaa_aws.json b/prowler/compliance/aws/hipaa_aws.json index 79d8b16f53..34de052b95 100644 --- a/prowler/compliance/aws/hipaa_aws.json +++ b/prowler/compliance/aws/hipaa_aws.json @@ -265,6 +265,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges" diff --git a/prowler/compliance/aws/iso27001_2013_aws.json b/prowler/compliance/aws/iso27001_2013_aws.json index a817ec22bd..cf7e5bb0a6 100644 --- a/prowler/compliance/aws/iso27001_2013_aws.json +++ b/prowler/compliance/aws/iso27001_2013_aws.json @@ -867,6 +867,8 @@ } ], "Checks": [ + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] diff --git a/prowler/compliance/aws/iso27001_2022_aws.json b/prowler/compliance/aws/iso27001_2022_aws.json index 3dbac3482c..cb64f4bdd8 100644 --- a/prowler/compliance/aws/iso27001_2022_aws.json +++ b/prowler/compliance/aws/iso27001_2022_aws.json @@ -245,6 +245,8 @@ "iam_policy_attached_only_to_group_or_roles", "iam_user_mfa_enabled_console_access", "iam_root_mfa_enabled", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_rotate_access_key_90_days", "iam_user_accesskey_unused", "iam_user_console_access_unused", @@ -306,6 +308,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges" @@ -970,6 +974,9 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", + "bedrock_full_access_policy_attached", "ec2_instance_profile_attached", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", @@ -1454,6 +1461,7 @@ "Checks": [ "awslambda_function_inside_vpc", "awslambda_function_vpc_multi_az", + "bedrock_vpc_endpoints_configured", "cloudwatch_changes_to_vpcs_alarm_configured", "ec2_transitgateway_auto_accept_vpc_attachments", "networkfirewall_in_all_vpc", @@ -1548,6 +1556,7 @@ "Checks": [ "awslambda_function_inside_vpc", "awslambda_function_vpc_multi_az", + "bedrock_vpc_endpoints_configured", "cloudwatch_changes_to_vpcs_alarm_configured", "ec2_transitgateway_auto_accept_vpc_attachments", "networkfirewall_in_all_vpc", @@ -1642,6 +1651,7 @@ "Checks": [ "awslambda_function_inside_vpc", "awslambda_function_vpc_multi_az", + "bedrock_vpc_endpoints_configured", "cloudwatch_changes_to_vpcs_alarm_configured", "ec2_transitgateway_auto_accept_vpc_attachments", "networkfirewall_in_all_vpc", diff --git a/prowler/compliance/aws/kisa_isms_p_2023_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_aws.json index 5c7d31e82e..cb37e8bf67 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_aws.json @@ -1389,6 +1389,9 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings", + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", + "bedrock_full_access_policy_attached", "iam_administrator_access_with_mfa", "iam_avoid_root_usage", "iam_aws_attached_policy_no_administrative_privileges", @@ -1614,6 +1617,7 @@ "sagemaker_notebook_instance_without_direct_internet_access_configured", "sagemaker_training_jobs_network_isolation_enabled", "sagemaker_training_jobs_vpc_settings_configured", + "bedrock_vpc_endpoints_configured", "vpc_endpoint_connections_trust_boundaries", "vpc_endpoint_for_ec2_enabled", "vpc_peering_routing_tables_with_least_privilege", diff --git a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json index b539fc9073..d1dbcb64b2 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json @@ -1389,6 +1389,9 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings", + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", + "bedrock_full_access_policy_attached", "iam_administrator_access_with_mfa", "iam_avoid_root_usage", "iam_aws_attached_policy_no_administrative_privileges", @@ -1613,6 +1616,7 @@ "sagemaker_notebook_instance_without_direct_internet_access_configured", "sagemaker_training_jobs_network_isolation_enabled", "sagemaker_training_jobs_vpc_settings_configured", + "bedrock_vpc_endpoints_configured", "vpc_endpoint_connections_trust_boundaries", "vpc_endpoint_for_ec2_enabled", "vpc_peering_routing_tables_with_least_privilege", diff --git a/prowler/compliance/aws/mitre_attack_aws.json b/prowler/compliance/aws/mitre_attack_aws.json index e6bcad5870..93545b3870 100644 --- a/prowler/compliance/aws/mitre_attack_aws.json +++ b/prowler/compliance/aws/mitre_attack_aws.json @@ -169,6 +169,8 @@ "iam_customer_unattached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges", "iam_no_expired_server_certificates_stored", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_no_root_access_key", diff --git a/prowler/compliance/aws/nis2_aws.json b/prowler/compliance/aws/nis2_aws.json index dc6b75657f..687068e26d 100644 --- a/prowler/compliance/aws/nis2_aws.json +++ b/prowler/compliance/aws/nis2_aws.json @@ -1368,9 +1368,10 @@ "Id": "6.8.2.a", "Description": "consider the functional, logical and physical relationship, including location, between trustworthy systems and services;", "Checks": [ + "bedrock_vpc_endpoints_configured", "iam_role_cross_service_confused_deputy_prevention", - "vpc_endpoint_services_allowed_principals_trust_boundaries", - "vpc_endpoint_connections_trust_boundaries" + "vpc_endpoint_connections_trust_boundaries", + "vpc_endpoint_services_allowed_principals_trust_boundaries" ], "Attributes": [ { @@ -1910,6 +1911,8 @@ "Id": "11.5.4", "Description": "The relevant entities shall regularly review the identities for network and information systems and their users and, if no longer needed, deactivate them without delay.", "Checks": [ + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ], diff --git a/prowler/compliance/aws/nist_800_171_revision_2_aws.json b/prowler/compliance/aws/nist_800_171_revision_2_aws.json index f11f49635d..bd0c84f327 100644 --- a/prowler/compliance/aws/nist_800_171_revision_2_aws.json +++ b/prowler/compliance/aws/nist_800_171_revision_2_aws.json @@ -30,6 +30,8 @@ "iam_no_root_access_key", "iam_user_mfa_enabled_console_access", "iam_user_mfa_enabled_console_access", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "awslambda_function_not_publicly_accessible", @@ -72,6 +74,8 @@ "iam_no_root_access_key", "iam_user_mfa_enabled_console_access", "iam_user_mfa_enabled_console_access", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "awslambda_function_not_publicly_accessible", @@ -152,10 +156,14 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges", "iam_no_root_access_key", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -579,6 +587,8 @@ "Checks": [ "iam_password_policy_reuse_24", "iam_password_policy_expires_passwords_within_90_days_or_less", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] diff --git a/prowler/compliance/aws/nist_800_53_revision_4_aws.json b/prowler/compliance/aws/nist_800_53_revision_4_aws.json index 45639cce62..24bf67fa31 100644 --- a/prowler/compliance/aws/nist_800_53_revision_4_aws.json +++ b/prowler/compliance/aws/nist_800_53_revision_4_aws.json @@ -21,6 +21,8 @@ "guardduty_is_enabled", "iam_password_policy_reuse_24", "iam_rotate_access_key_90_days", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "securityhub_enabled" @@ -39,6 +41,8 @@ } ], "Checks": [ + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -110,6 +114,8 @@ "iam_root_mfa_enabled", "iam_no_root_access_key", "iam_rotate_access_key_90_days", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "rds_instance_integration_cloudwatch_logs", @@ -222,6 +228,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "ec2_ebs_public_snapshot", "ec2_instance_public_ip", "ec2_instance_imdsv2_enabled", @@ -230,6 +238,8 @@ "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges", "iam_no_root_access_key", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "awslambda_function_url_public", diff --git a/prowler/compliance/aws/nist_800_53_revision_5_aws.json b/prowler/compliance/aws/nist_800_53_revision_5_aws.json index a83dc9f331..12a55a9359 100644 --- a/prowler/compliance/aws/nist_800_53_revision_5_aws.json +++ b/prowler/compliance/aws/nist_800_53_revision_5_aws.json @@ -29,6 +29,8 @@ "iam_rotate_access_key_90_days", "iam_user_mfa_enabled_console_access", "iam_user_mfa_enabled_console_access", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "secretsmanager_automatic_rotation_enabled" @@ -49,6 +51,8 @@ ], "Checks": [ "iam_password_policy_minimum_length_14", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -68,6 +72,8 @@ ], "Checks": [ "iam_password_policy_minimum_length_14", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -87,6 +93,8 @@ ], "Checks": [ "iam_password_policy_minimum_length_14", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -106,6 +114,8 @@ ], "Checks": [ "iam_password_policy_minimum_length_14", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -124,6 +134,8 @@ ], "Checks": [ "iam_password_policy_minimum_length_14", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -233,6 +245,8 @@ } ], "Checks": [ + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -269,6 +283,8 @@ } ], "Checks": [ + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] @@ -843,6 +859,8 @@ "iam_rotate_access_key_90_days", "iam_user_mfa_enabled_console_access", "iam_user_mfa_enabled_console_access", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "secretsmanager_automatic_rotation_enabled" @@ -1168,6 +1186,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "ec2_ebs_public_snapshot", "ec2_instance_public_ip", "ec2_instance_imdsv2_enabled", @@ -1177,6 +1197,8 @@ "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges", "iam_no_root_access_key", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "awslambda_function_not_publicly_accessible", diff --git a/prowler/compliance/aws/nist_csf_1.1_aws.json b/prowler/compliance/aws/nist_csf_1.1_aws.json index b7e5bbe6e5..cb21e19757 100644 --- a/prowler/compliance/aws/nist_csf_1.1_aws.json +++ b/prowler/compliance/aws/nist_csf_1.1_aws.json @@ -575,6 +575,8 @@ "iam_inline_policy_no_administrative_privileges", "iam_no_root_access_key", "iam_rotate_access_key_90_days", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "secretsmanager_automatic_rotation_enabled" @@ -628,10 +630,14 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges", "iam_no_root_access_key", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused" ] diff --git a/prowler/compliance/aws/nist_csf_2.0_aws.json b/prowler/compliance/aws/nist_csf_2.0_aws.json index 591e558bb1..fb9b1da4f1 100644 --- a/prowler/compliance/aws/nist_csf_2.0_aws.json +++ b/prowler/compliance/aws/nist_csf_2.0_aws.json @@ -705,6 +705,8 @@ "iam_root_mfa_enabled", "iam_no_root_access_key", "iam_user_console_access_unused", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_two_active_access_key", "iam_root_credentials_management_enabled", @@ -774,6 +776,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_administrator_access_with_mfa", "iam_group_administrator_access_policy", "iam_user_administrator_access_policy", @@ -784,6 +788,7 @@ "iam_customer_attached_policy_no_administrative_privileges", "iam_customer_unattached_policy_no_administrative_privileges", "accessanalyzer_enabled_without_findings", + "bedrock_full_access_policy_attached", "eventbridge_bus_cross_account_access", "eventbridge_bus_exposed", "iam_policy_no_full_access_to_cloudtrail", @@ -805,6 +810,7 @@ "Checks": [ "vpc_subnet_different_az", "vpc_subnet_separate_private_public", + "bedrock_vpc_endpoints_configured", "vpc_endpoint_connections_trust_boundaries", "vpc_peering_routing_tables_with_least_privilege", "ec2_networkacl_unused", diff --git a/prowler/compliance/aws/pci_3.2.1_aws.json b/prowler/compliance/aws/pci_3.2.1_aws.json index 649f32a94c..a313c70ae8 100644 --- a/prowler/compliance/aws/pci_3.2.1_aws.json +++ b/prowler/compliance/aws/pci_3.2.1_aws.json @@ -276,6 +276,7 @@ "ec2_ebs_public_snapshot", "rds_instance_no_public_access", "eks_endpoints_not_publicly_accessible", + "bedrock_vpc_endpoints_configured", "vpc_endpoint_for_ec2_enabled", "s3_account_level_public_access_blocks", "awslambda_function_not_publicly_accessible", @@ -1560,6 +1561,8 @@ "Name": "Observe user accounts to verify that any inactive accounts over 90 days old are either removed or disabled", "Description": "Accounts that are not used regularly are often targets of attack since it is less likely that any changes (such as a changed password) will be noticed. As such, these accounts may be more easily exploited and used to access cardholder data.", "Checks": [ + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_password_policy_reuse_24", "iam_user_accesskey_unused", "iam_user_console_access_unused" diff --git a/prowler/compliance/aws/pci_4.0_aws.json b/prowler/compliance/aws/pci_4.0_aws.json index 8367bc4cb6..dc8fab9140 100644 --- a/prowler/compliance/aws/pci_4.0_aws.json +++ b/prowler/compliance/aws/pci_4.0_aws.json @@ -754,6 +754,7 @@ "Description": "Checks if Service Endpoint for the service provided in rule parameter is created for each Amazon Virtual Private Cloud (Amazon VPC)", "Name": "vpc", "Checks": [ + "bedrock_vpc_endpoints_configured", "vpc_endpoint_for_ec2_enabled" ], "Attributes": [ @@ -1454,6 +1455,7 @@ "Description": "Checks if Service Endpoint for the service provided in rule parameter is created for each Amazon Virtual Private Cloud (Amazon VPC)", "Name": "vpc", "Checks": [ + "bedrock_vpc_endpoints_configured", "vpc_endpoint_for_ec2_enabled" ], "Attributes": [ @@ -2168,6 +2170,7 @@ "Description": "Checks if Service Endpoint for the service provided in rule parameter is created for each Amazon Virtual Private Cloud (Amazon VPC)", "Name": "vpc", "Checks": [ + "bedrock_vpc_endpoints_configured", "vpc_endpoint_for_ec2_enabled" ], "Attributes": [ @@ -2988,6 +2991,7 @@ "Description": "Checks if Service Endpoint for the service provided in rule parameter is created for each Amazon Virtual Private Cloud (Amazon VPC)", "Name": "vpc", "Checks": [ + "bedrock_vpc_endpoints_configured", "vpc_endpoint_for_ec2_enabled" ], "Attributes": [ @@ -3850,6 +3854,7 @@ "Description": "Checks if Service Endpoint for the service provided in rule parameter is created for each Amazon Virtual Private Cloud (Amazon VPC)", "Name": "vpc", "Checks": [ + "bedrock_vpc_endpoints_configured", "vpc_endpoint_for_ec2_enabled" ], "Attributes": [ @@ -21686,6 +21691,7 @@ "Description": "Checks if Service Endpoint for the service provided in rule parameter is created for each Amazon Virtual Private Cloud (Amazon VPC)", "Name": "vpc", "Checks": [ + "bedrock_vpc_endpoints_configured", "vpc_endpoint_for_ec2_enabled" ], "Attributes": [ diff --git a/prowler/compliance/aws/secnumcloud_3.2_aws.json b/prowler/compliance/aws/secnumcloud_3.2_aws.json index 1476ad87e5..4f01db2f3f 100644 --- a/prowler/compliance/aws/secnumcloud_3.2_aws.json +++ b/prowler/compliance/aws/secnumcloud_3.2_aws.json @@ -293,6 +293,8 @@ } ], "Checks": [ + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_no_expired_server_certificates_stored" @@ -311,14 +313,15 @@ } ], "Checks": [ - "iam_policy_allows_privilege_escalation", + "accessanalyzer_enabled", + "bedrock_full_access_policy_attached", + "iam_group_administrator_access_policy", "iam_inline_policy_allows_privilege_escalation", "iam_no_custom_policy_permissive_role_assumption", - "iam_user_two_active_access_key", - "accessanalyzer_enabled", + "iam_policy_allows_privilege_escalation", "iam_role_administratoraccess_policy", "iam_user_administrator_access_policy", - "iam_group_administrator_access_policy" + "iam_user_two_active_access_key" ] }, { @@ -335,6 +338,8 @@ ], "Checks": [ "iam_rotate_access_key_90_days", + "iam_role_access_not_stale_to_bedrock", + "iam_user_access_not_stale_to_bedrock", "iam_user_accesskey_unused", "iam_user_console_access_unused", "accessanalyzer_enabled_without_findings" @@ -1027,6 +1032,7 @@ "ec2_networkacl_allow_ingress_any_port", "vpc_subnet_separate_private_public", "vpc_peering_routing_tables_with_least_privilege", + "bedrock_vpc_endpoints_configured", "vpc_endpoint_connections_trust_boundaries", "vpc_endpoint_services_allowed_principals_trust_boundaries", "elbv2_waf_acl_attached", diff --git a/prowler/compliance/aws/soc2_aws.json b/prowler/compliance/aws/soc2_aws.json index 44d4038528..472d20ab75 100644 --- a/prowler/compliance/aws/soc2_aws.json +++ b/prowler/compliance/aws/soc2_aws.json @@ -205,6 +205,9 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", + "bedrock_full_access_policy_attached", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges" diff --git a/prowler/compliance/azure/ccc_azure.json b/prowler/compliance/azure/ccc_azure.json index 004137ad53..cb87346d13 100644 --- a/prowler/compliance/azure/ccc_azure.json +++ b/prowler/compliance/azure/ccc_azure.json @@ -1,10 +1,1698 @@ { "Framework": "CCC", - "Version": "", + "Version": "v2025.10", "Provider": "Azure", "Name": "Common Cloud Controls Catalog (CCC)", "Description": "Common Cloud Controls Catalog (CCC) for Azure", "Requirements": [ + { + "Id": "CCC.Core.CN01.AR01", + "Description": "When a port is exposed for non-SSH network traffic, all traffic MUST include a TLS handshake AND be encrypted using TLS 1.3 or higher.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Most cloud services enable TLS 1.3 by default. Where it is not already set, ensure that your services are configured or updated accordingly.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [ + "storage_secure_transfer_required_is_enabled", + "storage_ensure_minimum_tls_version_12", + "storage_smb_channel_encryption_with_secure_algorithm", + "storage_smb_protocol_version_is_latest", + "postgresql_flexible_server_enforce_ssl_enabled", + "mysql_flexible_server_ssl_connection_enabled", + "mysql_flexible_server_minimum_tls_version_12", + "sqlserver_recommended_minimal_tls_version", + "app_minimum_tls_version_12", + "app_ensure_http_is_redirected_to_https", + "app_ensure_using_http20", + "app_ftp_deployment_disabled", + "app_function_ftps_deployment_disabled" + ] + }, + { + "Id": "CCC.Core.CN01.AR02", + "Description": "When a port is exposed for SSH network traffic, all traffic MUST include a SSH handshake AND be encrypted using SSHv2 or higher.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Any time port 22 is exposed, ensure that it has a properly implemented SSH server with SSHv2 enabled and configured with strong ciphers.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [ + "network_ssh_internet_access_restricted", + "vm_linux_enforce_ssh_authentication" + ] + }, + { + "Id": "CCC.Core.CN01.AR03", + "Description": "When the service receives unencrypted traffic, then it MUST either block the request or automatically redirect it to the secure equivalent.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Review firewall, load balancer, and application configurations to ensure insecure protocols such as HTTP, FTP, and Telnet are not exposed. Where possible, implement automatic redirection to secure protocols such as HTTPS, SFTP, SSH, and regularly scan for protocol drift.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [ + "app_ensure_http_is_redirected_to_https", + "storage_secure_transfer_required_is_enabled", + "app_ftp_deployment_disabled", + "app_function_ftps_deployment_disabled" + ] + }, + { + "Id": "CCC.Core.CN01.AR07", + "Description": "When a port is exposed, the service MUST ensure that the protocol and service officially assigned to that port number by the IANA Service Name and Transport Protocol Port Number Registry, and no other, is run on that port.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Reference the IANA Service Name and Transport Protocol Port Number Registry for more information about correct protocol-to-port assignments. Avoid running non-standard services on well-known ports.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN01.AR08", + "Description": "When a service transmits data using TLS, mutual TLS (mTLS) MUST be implemented to require both client and server certificate authentication for all connections.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Configure mTLS for all endpoints that process or transmit sensitive data. Ensure both client and server certificates are validated and managed securely. Regularly review certificate authorities and automate certificate rotation where possible.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [ + "app_client_certificates_on" + ] + }, + { + "Id": "CCC.Core.CN13.AR01", + "Description": "When a port is exposed that uses certificate-based encryption, the service MUST only use valid, unexpired certificates issued by a trusted certificate authority.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH18" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "keyvault_key_expiration_set_in_non_rbac", + "keyvault_rbac_key_expiration_set", + "keyvault_non_rbac_secret_expiration_set", + "keyvault_rbac_secret_expiration_set" + ] + }, + { + "Id": "CCC.Core.CN13.AR02", + "Description": "When a port is exposed that uses certificate-based encryption, the service MUST rotate active certificates within 180 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", + "Applicability": [ + "tlp-amber" + ], + "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH18" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "keyvault_key_rotation_enabled" + ] + }, + { + "Id": "CCC.Core.CN13.AR03", + "Description": "When a port is exposed that uses certificate-based encryption, the service MUST rotate active certificates within 90 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH18" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "keyvault_key_rotation_enabled" + ] + }, + { + "Id": "CCC.Core.CN06.AR01", + "Description": "When the service is running, its region and availability zone MUST be included in a list of explicitly trusted or approved locations within the trust perimeter.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN06 Restrict Deployments to Trust Perimeter", + "SubSection": "", + "SubSectionObjective": "Ensure that the service and its child resources are only deployed on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Maintain an up-to-date list of trusted and approved regions based on organizational policies. Validate the service's deployment location is included in this list.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-19" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN06.AR02", + "Description": "When a child resource is deployed, its region and availability zone MUST be included in a list of explicitly trusted or approved locations within the trust perimeter.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN06 Restrict Deployments to Trust Perimeter", + "SubSection": "", + "SubSectionObjective": "Ensure that the service and its child resources are only deployed on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Maintain an up-to-date list of trusted and approved regions based on organizational policies. Validate that child resources can only be deployed to locations included in this list.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-19" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN08.AR01", + "Description": "When data is created or modified, the data MUST have a complete and recoverable duplicate that is stored in a physically separate data center.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN08 Replicate Data to Multiple Locations", + "SubSection": "", + "SubSectionObjective": "Ensure that data is replicated across multiple physical locations to protect against data loss due to hardware failures, natural disasters, or other catastrophic events.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Implement automated data replication processes to ensure that data is consistently duplicated in another region or availability zone. Regularly test data recovery from the replicated location to ensure integrity and availability.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "BCR-08", + "BCR-10", + "BCR-11" + ] + } + ] + } + ], + "Checks": [ + "storage_geo_redundant_enabled", + "vm_backup_enabled" + ] + }, + { + "Id": "CCC.Core.CN08.AR02", + "Description": "When data is replicated into a second location, the service MUST be able to accurately represent the replication locations, replication status, and data synchronization status.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN08 Replicate Data to Multiple Locations", + "SubSection": "", + "SubSectionObjective": "Ensure that data is replicated across multiple physical locations to protect against data loss due to hardware failures, natural disasters, or other catastrophic events.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "BCR-08", + "BCR-10", + "BCR-11" + ] + } + ] + } + ], + "Checks": [ + "storage_geo_redundant_enabled" + ] + }, + { + "Id": "CCC.Core.CN09.AR01", + "Description": "When the service is operational, its logs and any child resource logs MUST NOT be accessible from the resource they record access to.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH07", + "CCC.Core.TH09", + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-02", + "LOG-04", + "LOG-09" + ] + } + ] + } + ], + "Checks": [ + "monitor_storage_account_with_activity_logs_is_private", + "monitor_storage_account_with_activity_logs_cmk_encrypted" + ] + }, + { + "Id": "CCC.Core.CN09.AR02", + "Description": "When the service is operational, disabling the logs for the service or its child resources MUST NOT be possible without also disabling the corresponding resource.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "No normal business operations should disable logs, as this could indicate an attempt to cover up unauthorized access. Ensure that logging mechanisms are tightly integrated with service operations, so that logging cannot be disabled without stopping the service itself.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH07", + "CCC.Core.TH09", + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-02", + "LOG-04", + "LOG-09" + ] + } + ] + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists", + "monitor_diagnostic_setting_with_appropriate_categories" + ] + }, + { + "Id": "CCC.Core.CN09.AR03", + "Description": "When the service is operational, any attempt to redirect logs for the service or its child resources MUST NOT be possible without halting operation of the corresponding resource and publishing corresponding events to monitored channels.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "No normal business operations should result in the redirection of logs, as this could indicate an attempt to cover up unauthorized access. Ensure that logging configurations are immutable during service operation so that any changes require stopping the service and publishing corresponding events to monitored channels.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH07", + "CCC.Core.TH09", + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-02", + "LOG-04", + "LOG-09" + ] + } + ] + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists" + ] + }, + { + "Id": "CCC.Core.CN10.AR01", + "Description": "When data is replicated, the service MUST ensure that replication only occurs to destinations that are explicitly included within the defined trust perimeter.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN10 Restrict Data Replication to Trust Perimeter", + "SubSection": "", + "SubSectionObjective": "Ensure that data is only replicated on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-10", + "DSP-19" + ] + } + ] + } + ], + "Checks": [ + "storage_cross_tenant_replication_disabled" + ] + }, + { + "Id": "CCC.Core.CN02.AR01", + "Description": "When data is stored, it MUST be encrypted using the latest industry-standard encryption methods.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN02 Encrypt Data for Storage", + "SubSection": "", + "SubSectionObjective": "Ensure that all data stored is encrypted at rest using strong encryption algorithms.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "UEM-08", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_infrastructure_encryption_is_enabled", + "storage_ensure_encryption_with_customer_managed_keys", + "vm_ensure_attached_disks_encrypted_with_cmk", + "vm_ensure_unattached_disks_encrypted_with_cmk", + "sqlserver_tde_encryption_enabled", + "sqlserver_tde_encrypted_with_cmk", + "databricks_workspace_cmk_encryption_enabled", + "monitor_storage_account_with_activity_logs_cmk_encrypted" + ] + }, + { + "Id": "CCC.Core.CN11.AR01", + "Description": "When encryption keys are used, the service MUST verify that all encryption keys use the latest industry-standard cryptographic algorithms.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "storage_smb_channel_encryption_with_secure_algorithm" + ] + }, + { + "Id": "CCC.Core.CN11.AR02", + "Description": "When encryption keys are used, the service MUST rotate active keys within 180 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "keyvault_key_rotation_enabled" + ] + }, + { + "Id": "CCC.Core.CN11.AR03", + "Description": "When encrypting data, the service MUST verify that customer-managed encryption keys (CMEKs) are used.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "storage_ensure_encryption_with_customer_managed_keys", + "vm_ensure_attached_disks_encrypted_with_cmk", + "vm_ensure_unattached_disks_encrypted_with_cmk", + "sqlserver_tde_encrypted_with_cmk", + "databricks_workspace_cmk_encryption_enabled" + ] + }, + { + "Id": "CCC.Core.CN11.AR04", + "Description": "When encryption keys are accessed, the service MUST verify that access to encryption keys is restricted to authorized personnel and services, following the principle of least privilege.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "keyvault_rbac_enabled", + "keyvault_private_endpoints", + "keyvault_access_only_through_private_endpoints", + "keyvault_logging_enabled", + "keyvault_recoverable" + ] + }, + { + "Id": "CCC.Core.CN11.AR05", + "Description": "When encryption keys are used, the service MUST rotate active keys within 365 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-clear", + "tlp-green" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "keyvault_key_rotation_enabled" + ] + }, + { + "Id": "CCC.Core.CN11.AR06", + "Description": "When encryption keys are used, the service MUST rotate active keys within 90 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "storage_key_rotation_90_days", + "keyvault_key_rotation_enabled" + ] + }, + { + "Id": "CCC.Core.CN14.AR01", + "Description": "When backups are created for disaster recovery purposes, the storage mechanism MUST NOT allow modification or deletion within 30 days of creation.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN14 Maintain Recent Backups", + "SubSection": "", + "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Use immutable storage solutions where possible. Implement backup retention policies that enforce a minimum retention period of 30 days.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "vm_backup_enabled", + "vm_sufficient_daily_backup_retention_period" + ] + }, + { + "Id": "CCC.Core.CN14.AR02", + "Description": "When backups are created for disaster recovery purposes, the most recent backup MUST have a creation date within the past 30 days.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN14 Maintain Recent Backups", + "SubSection": "", + "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber" + ], + "Recommendation": "Implement automated backup processes to ensure that backups are created regularly. Monitor backup schedules and verify that the most recent backup creation date is within the last 30 days.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "vm_backup_enabled", + "vm_sufficient_daily_backup_retention_period" + ] + }, + { + "Id": "CCC.Core.CN14.AR03", + "Description": "When backups are created for disaster recovery purposes, the most recent backup MUST have a creation date within the past 14 days.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN14 Maintain Recent Backups", + "SubSection": "", + "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "Implement automated backup processes to ensure that backups are created regularly. Monitor backup schedules and verify that the most recent backup creation date is within the last 14 days.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "vm_backup_enabled", + "vm_sufficient_daily_backup_retention_period" + ] + }, + { + "Id": "CCC.Core.CN03.AR01", + "Description": "When an entity attempts to modify the service through a user interface, the authentication process MUST require multiple identifying factors for authentication.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "SubSection": "", + "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-14" + ] + } + ] + } + ], + "Checks": [ + "entra_privileged_user_has_mfa", + "entra_non_privileged_user_has_mfa", + "entra_user_with_vm_access_has_mfa", + "entra_security_defaults_enabled" + ] + }, + { + "Id": "CCC.Core.CN03.AR02", + "Description": "When an entity attempts to modify the service through an API endpoint, the authentication process MUST require a credential such as an API key or token AND originate from within the trust perimeter.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "SubSection": "", + "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-14" + ] + } + ] + } + ], + "Checks": [ + "entra_conditional_access_policy_require_mfa_for_management_api", + "app_function_access_keys_configured", + "app_function_identity_is_configured" + ] + }, + { + "Id": "CCC.Core.CN03.AR03", + "Description": "When an entity attempts to view information on the service through a user interface, the authentication process MUST require multiple identifying factors from the user.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "SubSection": "", + "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-14" + ] + } + ] + } + ], + "Checks": [ + "entra_privileged_user_has_mfa", + "entra_non_privileged_user_has_mfa", + "entra_user_with_vm_access_has_mfa", + "entra_security_defaults_enabled" + ] + }, + { + "Id": "CCC.Core.CN03.AR04", + "Description": "When an entity attempts to view information on the service through an API endpoint, the authentication process MUST require a credential such as an API key or token AND originate from within the trust perimeter.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "SubSection": "", + "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-14" + ] + } + ] + } + ], + "Checks": [ + "entra_conditional_access_policy_require_mfa_for_management_api", + "app_function_access_keys_configured" + ] + }, + { + "Id": "CCC.Core.CN05.AR01", + "Description": "When an attempt is made to modify data on the service or a child resource, the service MUST block requests from unauthorized entities.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "aks_cluster_rbac_enabled", + "aks_clusters_public_access_disabled", + "app_ensure_auth_is_set_up", + "app_register_with_identity", + "app_function_identity_is_configured", + "app_function_identity_without_admin_privileges", + "app_function_not_publicly_accessible", + "iam_role_user_access_admin_restricted", + "iam_subscription_roles_owner_custom_not_created", + "keyvault_rbac_enabled", + "keyvault_private_endpoints", + "keyvault_access_only_through_private_endpoints", + "entra_global_admin_in_less_than_five_users", + "entra_non_privileged_user_has_mfa", + "entra_privileged_user_has_mfa", + "vm_jit_access_enabled" + ] + }, + { + "Id": "CCC.Core.CN05.AR02", + "Description": "When administrative access or configuration change is attempted on the service or a child resource, the service MUST refuse requests from unauthorized entities.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "iam_role_user_access_admin_restricted", + "iam_subscription_roles_owner_custom_not_created", + "iam_custom_role_has_permissions_to_administer_resource_locks", + "entra_global_admin_in_less_than_five_users", + "entra_privileged_user_has_mfa", + "entra_non_privileged_user_has_mfa", + "entra_conditional_access_policy_require_mfa_for_management_api", + "aks_cluster_rbac_enabled", + "containerregistry_admin_user_disabled", + "keyvault_rbac_enabled" + ] + }, + { + "Id": "CCC.Core.CN05.AR03", + "Description": "When administrative access or configuration change is attempted on the service or a child resource in a multi-tenant environment, the service MUST refuse requests across tenant boundaries unless the origin is explicitly included in a pre-approved allowlist.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_cross_tenant_replication_disabled", + "storage_default_to_entra_authorization_enabled", + "entra_trusted_named_locations_exists" + ] + }, + { + "Id": "CCC.Core.CN05.AR04", + "Description": "When data is requested from outside the trust perimeter, the service MUST refuse requests from unauthorized entities.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_default_network_access_rule_is_denied", + "storage_ensure_private_endpoints_in_storage_accounts", + "storage_account_key_access_disabled", + "storage_default_to_entra_authorization_enabled", + "containerregistry_not_publicly_accessible", + "containerregistry_uses_private_link", + "keyvault_private_endpoints", + "keyvault_access_only_through_private_endpoints", + "cosmosdb_account_use_private_endpoints", + "cosmosdb_account_firewall_use_selected_networks", + "sqlserver_unrestricted_inbound_access", + "network_http_internet_access_restricted" + ] + }, + { + "Id": "CCC.Core.CN05.AR05", + "Description": "When any request is made from outside the trust perimeter, the service MUST NOT provide any response that may indicate the service exists.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "aks_clusters_created_with_private_nodes", + "aks_clusters_public_access_disabled", + "app_function_not_publicly_accessible", + "containerregistry_not_publicly_accessible", + "keyvault_private_endpoints", + "storage_ensure_private_endpoints_in_storage_accounts", + "network_http_internet_access_restricted", + "network_rdp_internet_access_restricted", + "network_ssh_internet_access_restricted" + ] + }, + { + "Id": "CCC.Core.CN05.AR06", + "Description": "When any request is made to the service or a child resource, the service MUST refuse requests from unauthorized entities.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "entra_privileged_user_has_mfa", + "entra_non_privileged_user_has_mfa", + "entra_global_admin_in_less_than_five_users", + "entra_conditional_access_policy_require_mfa_for_management_api", + "iam_role_user_access_admin_restricted", + "iam_subscription_roles_owner_custom_not_created", + "keyvault_rbac_enabled", + "vm_jit_access_enabled" + ] + }, + { + "Id": "CCC.Core.CN04.AR01", + "Description": "When administrative access or configuration change is attempted on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN04 Log All Access and Changes", + "SubSection": "", + "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-08" + ] + } + ] + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists", + "monitor_diagnostic_setting_with_appropriate_categories", + "monitor_alert_create_update_nsg", + "monitor_alert_delete_nsg", + "monitor_alert_create_update_public_ip_address_rule", + "monitor_alert_delete_public_ip_address_rule", + "monitor_alert_create_update_security_solution", + "monitor_alert_delete_security_solution", + "monitor_alert_create_policy_assignment", + "monitor_alert_create_update_sqlserver_fr" + ] + }, + { + "Id": "CCC.Core.CN04.AR02", + "Description": "When any attempt is made to modify data on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN04 Log All Access and Changes", + "SubSection": "", + "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-08" + ] + } + ] + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists", + "monitor_diagnostic_setting_with_appropriate_categories", + "keyvault_logging_enabled", + "app_http_logs_enabled" + ] + }, + { + "Id": "CCC.Core.CN04.AR03", + "Description": "When any attempt is made to read data on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN04 Log All Access and Changes", + "SubSection": "", + "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-08" + ] + } + ] + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists", + "monitor_diagnostic_setting_with_appropriate_categories", + "keyvault_logging_enabled", + "app_http_logs_enabled" + ] + }, + { + "Id": "CCC.Core.CN07.AR01", + "Description": "When enumeration activities are detected, the service MUST publish an event to a monitored channel which includes the client identity, time, and nature of the activity.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN07 Alert on Unusual Enumeration Activity", + "SubSection": "", + "SubSectionObjective": "Ensure that logs and associated alerts are generated when unusual enumeration activity is detected that may indicate reconnaissance activities.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Implement event publication mechanisms and alerts for patterns indicative of enumeration activities, such as repeated access attempts, requests, or liveness probes. Configure alerts to notify security teams of any activities that merit further investigation.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH15" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-05", + "SEF-05" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN07.AR02", + "Description": "When enumeration activities are detected, the service MUST log the client identity, time, and nature of the activity.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN07 Alert on Unusual Enumeration Activity", + "SubSection": "", + "SubSectionObjective": "Ensure that logs and associated alerts are generated when unusual enumeration activity is detected that may indicate reconnaissance activities.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Implement logging mechanisms to capture details of enumeration activities, including client identity, timestamps, and activity nature. Retain logs according to organizational policies, and occasionally review them for patterns that may indicate reconnaissance activities.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH15" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-05", + "SEF-05" + ] + } + ] + } + ], + "Checks": [] + }, { "Id": "CCC.AuditLog.CN01.AR01", "Description": "When the signature validation process is performed, then it MUST detect any modification of data.", @@ -18,13 +1706,13 @@ "Applicability": [ "tlp-red" ], - "Recommendation": "Ensure hash of data is included in digital signature. ", + "Recommendation": "Ensure hash of data is included in digital signature.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06", - "CCC.TH07" + "CCC.Core.TH06", + "CCC.Core.TH07" ] } ], @@ -59,13 +1747,13 @@ "Applicability": [ "tlp-red" ], - "Recommendation": "Ensure verification process includes a chained hash function. ", + "Recommendation": "Ensure verification process includes a chained hash function.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06", - "CCC.TH07" + "CCC.Core.TH06", + "CCC.Core.TH07" ] } ], @@ -106,7 +1794,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06" + "CCC.Core.TH06" ] } ], @@ -130,9 +1818,7 @@ ], "Checks": [ "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted" + "monitor_diagnostic_setting_with_appropriate_categories" ] }, { @@ -149,12 +1835,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Ensure alerting is correctly configured ", + "Recommendation": "Ensure alerting is correctly configured", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -175,12 +1861,7 @@ ] } ], - "Checks": [ - "monitor_diagnostic_settings_exists", - "keyvault_logging_enabled", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "monitor_storage_account_with_activity_logs_is_private" - ] + "Checks": [] }, { "Id": "CCC.AuditLog.CN03.AR02", @@ -196,12 +1877,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Ensure alerting is correctly configured ", + "Recommendation": "Ensure alerting is correctly configured", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -222,22 +1903,7 @@ ] } ], - "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "monitor_alert_service_health_exists", - "monitor_alert_create_update_sqlserver_fr", - "monitor_alert_delete_nsg", - "monitor_alert_delete_public_ip_address_rule", - "monitor_alert_delete_security_solution", - "monitor_alert_create_update_nsg", - "monitor_alert_create_update_public_ip_address_rule", - "monitor_alert_create_update_security_solution", - "monitor_alert_create_policy_assignment", - "monitor_alert_delete_policy_assignment" - ] + "Checks": [] }, { "Id": "CCC.AuditLog.CN04.AR01", @@ -253,13 +1919,13 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Configure the audit log bucket to enable server access logging. Ensure the target logging bucket is configured for appropriate security, including restricted access and immutability. ", + "Recommendation": "Configure the audit log bucket to enable server access logging. Ensure the target logging bucket is configured for appropriate security, including restricted access and immutability.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01", - "CCC.TH09" + "CCC.Core.TH01", + "CCC.Core.TH09" ] } ], @@ -281,11 +1947,7 @@ } ], "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "monitor_storage_account_with_activity_logs_is_private", - "storage_blob_public_access_level_is_disabled" + "monitor_diagnostic_settings_exists" ] }, { @@ -302,12 +1964,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Configure audit log exporting. ", + "Recommendation": "Configure audit log exporting.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -331,9 +1993,7 @@ ], "Checks": [ "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted" + "monitor_diagnostic_setting_with_appropriate_categories" ] }, { @@ -351,13 +2011,13 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Configure the audit log bucket's lifecycle rules or object retention settings to enforce the required data retention period. ", + "Recommendation": "Configure the audit log bucket's lifecycle rules or object retention settings to enforce the required data retention period.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06", - "CCC.TH07" + "CCC.Core.TH06", + "CCC.Core.TH07" ] } ], @@ -378,10 +2038,7 @@ ] } ], - "Checks": [ - "storage_ensure_soft_delete_is_enabled", - "monitor_diagnostic_settings_exists" - ] + "Checks": [] }, { "Id": "CCC.AuditLog.CN07.AR01", @@ -398,13 +2055,13 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Enable MFA Delete (or equivalent multi-factor authentication for delete operations) on the audit log bucket. ", + "Recommendation": "Enable MFA Delete (or equivalent multi-factor authentication for delete operations) on the audit log bucket.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06", - "CCC.TH07" + "CCC.Core.TH06", + "CCC.Core.TH07" ] } ], @@ -441,12 +2098,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Configure object lock policy. ", + "Recommendation": "Configure object lock policy.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -467,12 +2124,7 @@ ] } ], - "Checks": [ - "storage_ensure_soft_delete_is_enabled", - "monitor_diagnostic_settings_exists", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "monitor_storage_account_with_activity_logs_is_private" - ] + "Checks": [] }, { "Id": "CCC.AuditLog.CN09.AR01", @@ -488,12 +2140,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Review field level access controls on audit data. ", + "Recommendation": "Review field level access controls on audit data.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -519,9 +2171,7 @@ } ], "Checks": [ - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_diagnostic_settings_exists" + "monitor_storage_account_with_activity_logs_is_private" ] }, { @@ -539,12 +2189,12 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted. ", + "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -568,7 +2218,6 @@ "Checks": [ "storage_blob_public_access_level_is_disabled", "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted", "storage_ensure_private_endpoints_in_storage_accounts" ] }, @@ -587,12 +2236,12 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted. ", + "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -616,944 +2265,16 @@ "Checks": [ "storage_blob_public_access_level_is_disabled", "storage_default_network_access_rule_is_denied", - "storage_ensure_private_endpoints_in_storage_accounts", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "monitor_storage_account_with_activity_logs_is_private" + "storage_ensure_private_endpoints_in_storage_accounts" ] }, - { - "Id": "CCC.Build.CN01.AR01", - "Description": "Attempt to initiate a build using an unauthorized build agent and verify that the build is rejected.", - "Attributes": [ - { - "FamilyName": "Access Control", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.Build.CN01 Restrict Allowed Build Agents", - "SubSection": "", - "SubSectionObjective": "Ensure that builds are executed only on authorized build agents to maintain control over the build environment and prevent unauthorized code execution.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "AC-6" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.Build.CN02.AR01", - "Description": "Attempt to trigger a build from an unauthorized external service or repository and verify that the build does not start.", - "Attributes": [ - { - "FamilyName": "Access Control", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.Build.CN02 Restrict Allowed External Services for Build Triggers", - "SubSection": "", - "SubSectionObjective": "Ensure that builds can only be triggered by authorized external services or repositories to prevent unauthorized code execution or tampering.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "AC-6" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.Build.CN03.AR01", - "Description": "Attempt to access the build environment from an external network and verify that access is denied.", - "Attributes": [ - { - "FamilyName": "Network Security", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.Build.CN03 Deny External Network Access for Build Environments", - "SubSection": "", - "SubSectionObjective": "Ensure that build environments do not have external network access to prevent unauthorized external access and data exfiltration.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02", - "CCC.TH05" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-5" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-7", - "SC-5" - ] - } - ] - } - ], - "Checks": [ - "aks_clusters_public_access_disabled", - "containerregistry_not_publicly_accessible", - "containerregistry_uses_private_link", - "app_function_not_publicly_accessible", - "network_http_internet_access_restricted", - "network_rdp_internet_access_restricted", - "network_ssh_internet_access_restricted", - "network_udp_internet_access_restricted" - ] - }, - { - "Id": "CCC.CntrReg.CN01.AR01", - "Description": "Attempt to push an artifact with known vulnerabilities to the registry and observe if it is flagged or rejected by the vulnerability scanning process.", - "Attributes": [ - { - "FamilyName": "Risk Management", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CntrReg.CN01 Implement Vulnerability Scanning for Artifacts", - "SubSection": "", - "SubSectionObjective": "Ensure that container images and artifacts stored in the container registry are scanned for vulnerabilities to identify and remediate security issues before deployment.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.CntrReg.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "ID.RA-1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "RA-5", - "SI-5" - ] - } - ] - } - ], - "Checks": [ - "defender_container_images_scan_enabled", - "defender_container_images_resolved_vulnerabilities" - ] - }, - { - "Id": "CCC.DataWar.CN01.AR01", - "Description": "Attempt to access underlying database tables directly without using managed views and verify that access is denied.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.DataWar.CN01 Enforce Use of Managed Views for Data Access", - "SubSection": "", - "SubSectionObjective": "Ensure that data access is provided through managed views, restricting users from accessing underlying tables directly and enforcing consistent security policies.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "AC-6" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.DataWar.CN02.AR01", - "Description": "Attempt to query sensitive columns without the necessary permissions and verify that access is denied or data is masked.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.DataWar.CN02 Enforce Column-Level Security Policies", - "SubSection": "", - "SubSectionObjective": "Ensure that access to sensitive data columns is restricted based on user roles, preventing unauthorized access to sensitive information.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "AC-6" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.DataWar.CN03.AR01", - "Description": "Attempt to query data rows that the user should not have access to and verify that access is denied or data is not returned.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.DataWar.CN03 Enforce Row-Level Security Policies", - "SubSection": "", - "SubSectionObjective": "Ensure that access to data rows is restricted based on user roles or attributes, preventing unauthorized access to specific subsets of data.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "AC-6" - ] - } - ] - } - ], - "Checks": [ - "cosmosdb_account_use_aad_and_rbac", - "cosmosdb_account_firewall_use_selected_networks", - "cosmosdb_account_use_private_endpoints", - "sqlserver_unrestricted_inbound_access", - "postgresql_flexible_server_allow_access_services_disabled" - ] - }, - { - "Id": "CCC.KeyMgmt.CN01.AR01", - "Description": "When a key version is scheduled for deletion or disabled, an alert MUST be generated within five minutes.", - "Attributes": [ - { - "FamilyName": "Logging and Metrics Publication", - "FamilyDescription": "Controls that collect, alert, and retain key-management events.", - "Section": "CCC.KeyMgmt.CN01 Alert on Key-version Changes", - "SubSection": "", - "SubSectionObjective": "Generate near-real-time alerts when a KMS key version is disabled or scheduled for deletion, enabling rapid investigation and recovery.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Use native event services (e.g., CloudWatch Events, Azure Monitor, Cloud Audit Logs) to route notifications to an incident-response channel.", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.KeyMgmt.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "RS.AN-1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "IR-5" - ] - } - ] - } - ], - "Checks": [ - "keyvault_logging_enabled", - "monitor_diagnostic_settings_exists", - "monitor_alert_create_update_nsg", - "monitor_alert_create_update_public_ip_address_rule", - "monitor_alert_create_update_security_solution", - "monitor_alert_create_policy_assignment", - "monitor_alert_service_health_exists" - ] - }, - { - "Id": "CCC.KeyMgmt.CN02.AR01", - "Description": "When IAM roles and key policies are reviewed, Decrypt permission MUST be granted exclusively to documented authorised principals.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that enforce least-privilege use of KMS operations.", - "Section": "CCC.KeyMgmt.CN02 Limit Decrypt Permissions", - "SubSection": "", - "SubSectionObjective": "Restrict the Decrypt operation to authorised principals only, applying the principle of least privilege to protect sensitive data.", - "Applicability": [ - "tlp-green" - ], - "Recommendation": "Periodically audit policy documents via automated tooling and report any deviations.", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.KeyMgmt.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-6" - ] - } - ] - } - ], - "Checks": [ - "keyvault_rbac_enabled", - "keyvault_rbac_key_expiration_set", - "keyvault_rbac_secret_expiration_set", - "keyvault_key_expiration_set_in_non_rbac" - ] - }, - { - "Id": "CCC.KeyMgmt.CN03.AR01", - "Description": "When rotation settings are examined, rotation MUST be enabled with an interval not exceeding 365 days.", - "Attributes": [ - { - "FamilyName": "Key Lifecycle Management", - "FamilyDescription": "Controls that govern creation, rotation, import, and retirement of cryptographic keys.", - "Section": "CCC.KeyMgmt.CN03 Enforce Automatic Rotation", - "SubSection": "", - "SubSectionObjective": "Ensure symmetric keys rotate automatically within policy intervals to reduce exposure of key material.", - "Applicability": [ - "tlp-green" - ], - "Recommendation": "Use cloud-provider rotation features and verify via configuration scanning.", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.KeyMgmt.TH03" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-12" - ] - } - ] - } - ], - "Checks": [ - "keyvault_key_rotation_enabled" - ] - }, - { - "Id": "CCC.KeyMgmt.CN04.AR01", - "Description": "When a key import request is processed, the key MUST use an approved algorithm (RSA-2048+, EC-P256+) and originate from a certified HSM.", - "Attributes": [ - { - "FamilyName": "Key Lifecycle Management", - "FamilyDescription": "Controls that govern creation, rotation, import, and retirement of cryptographic keys.", - "Section": "CCC.KeyMgmt.CN04 Validate Imported Keys", - "SubSection": "", - "SubSectionObjective": "Accept only externally generated keys that meet approved cryptographic strength and provenance requirements.", - "Applicability": [ - "tlp-green" - ], - "Recommendation": "Implement an approval workflow that validates attestation data before import.", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.KeyMgmt.TH04" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "keyvault_key_rotation_enabled", - "keyvault_rbac_enabled", - "keyvault_key_expiration_set_in_non_rbac", - "keyvault_rbac_key_expiration_set", - "keyvault_rbac_secret_expiration_set", - "keyvault_private_endpoints", - "keyvault_access_only_through_private_endpoints", - "keyvault_recoverable", - "keyvault_logging_enabled", - "keyvault_non_rbac_secret_expiration_set" - ] - }, - { - "Id": "CCC.LB.CN01.AR01", - "Description": "When a single client sends more than 2000 requests within any 5-minute sliding window, the load balancer MUST throttle all subsequent requests from that client for at least 60 seconds.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity. ", - "Section": "CCC.LB.CN01 Enforce and Detect Rate Limiting", - "SubSection": "", - "SubSectionObjective": "Detect and throttle malicious or excessive requests to prevent downstream resource exhaustion and brute-force activity.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Implement per-IP token-bucket limits with and verify via synthetic traffic tests. ", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH01", - "CCC.LB.TH09" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-1", - "PR.AC-7", - "PR.PT-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-6", - "SC-5", - "AC-7" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.LB.CN01.AR02", - "Description": "When throttling is invoked, the load balancer MUST record the event in the access log within 5 minutes for alerting and trend analysis.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity. ", - "Section": "CCC.LB.CN01 Enforce and Detect Rate Limiting", - "SubSection": "", - "SubSectionObjective": "Detect and throttle malicious or excessive requests to prevent downstream resource exhaustion and brute-force activity.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Enable access logging and configure metric filters on HTTP 429 counts to trigger alerts. ", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH01", - "CCC.LB.TH09" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-1", - "PR.AC-7", - "PR.PT-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-6", - "SC-5", - "AC-7" - ] - } - ] - } - ], - "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "network_flow_log_captured_sent", - "network_watcher_enabled", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted" - ] - }, - { - "Id": "CCC.LB.CN06.AR01", - "Description": "When more than 10 percent of targets change from healthy to unhealthy within five minutes, an alert MUST be issued.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity. ", - "Section": "CCC.LB.CN06 Secure Health-Check Telemetry", - "SubSection": "", - "SubSectionObjective": "Monitor health-check endpoints for tampering and alert on abnormal status changes.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Instrument metrics for health check results and target removal events. Configure monitoring alarms to alert on abnormal spikes in unhealthy targets. ", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH05" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SI-4" - ] - } - ] - } - ], - "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "monitor_alert_service_health_exists", - "monitor_alert_create_update_nsg", - "monitor_alert_create_update_public_ip_address_rule", - "monitor_alert_create_update_security_solution", - "monitor_alert_create_policy_assignment", - "network_flow_log_captured_sent", - "network_watcher_enabled", - "vm_scaleset_associated_with_load_balancer" - ] - }, - { - "Id": "CCC.LB.CN04.AR01", - "Description": "When routing weights change, the request MUST originate from an explicitly defined and trusted identity and MUST be logged.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can change or query load-balancer resources. ", - "Section": "CCC.LB.CN04 Enforce Distribution Policies", - "SubSection": "", - "SubSectionObjective": "Ensure traffic-splitting weights and algorithms are modified only by trusted identities.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Define a list of trusted principals allowed to modify routing configurations. Enforce via conditional access policies, and log changes using audit logging. ", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH03" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_alert_create_update_public_ip_address_rule", - "monitor_alert_create_update_nsg", - "monitor_alert_create_policy_assignment", - "monitor_alert_create_update_security_solution", - "entra_global_admin_in_less_than_five_users", - "entra_non_privileged_user_has_mfa", - "iam_role_user_access_admin_restricted", - "iam_custom_role_has_permissions_to_administer_resource_locks" - ] - }, - { - "Id": "CCC.LB.CN05.AR01", - "Description": "When stickiness is enabled, session cookies MUST expire within 30 minutes of inactivity.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can change or query load-balancer resources. ", - "Section": "CCC.LB.CN05 Validate Session Affinity", - "SubSection": "", - "SubSectionObjective": "Configure session persistence to minimise fixation and hijacking risks.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Audit CCC.LB.F15 parameters via configuration scans.", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH04" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-23" - ] - } - ] - } - ], - "Checks": [ - "iam_role_user_access_admin_restricted", - "iam_subscription_roles_owner_custom_not_created", - "iam_custom_role_has_permissions_to_administer_resource_locks" - ] - }, - { - "Id": "CCC.LB.CN09.AR01", - "Description": "When an API call originates outside the approved CIDR set, the request MUST be denied.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can change or query load-balancer resources. ", - "Section": "CCC.LB.CN09 Restrict Management API Access", - "SubSection": "", - "SubSectionObjective": "Limit load-balancer API calls to authorised identities and trusted networks.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Combine VPC endpoints with IAM condition-key filters for protected APIs.", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH08" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-5" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-7" - ] - } - ] - } - ], - "Checks": [ - "entra_conditional_access_policy_require_mfa_for_management_api", - "entra_global_admin_in_less_than_five_users", - "entra_privileged_user_has_mfa", - "iam_role_user_access_admin_restricted", - "iam_custom_role_has_permissions_to_administer_resource_locks" - ] - }, - { - "Id": "CCC.LB.CN02.AR01", - "Description": "When concurrent connections reach 80 percent of capacity, the autoscaling group MUST add at least one instance within five minutes.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls that preserve availability and confidentiality of traffic processed by the load balancer. ", - "Section": "CCC.LB.CN02 Auto-Scale Load Balancer Capacity", - "SubSection": "", - "SubSectionObjective": "Expand load-balancer capacity to maintain availability during traffic spikes.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Enable autoscaling policies.", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH09" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "ID.BE-5" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "CP-10" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.LB.CN07.AR01", - "Description": "When responses pass through the load balancer, the \"Server\" header MUST be replaced with \"lb\".", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls that preserve availability and confidentiality of traffic processed by the load balancer. ", - "Section": "CCC.LB.CN07 Scrub Sensitive Headers", - "SubSection": "", - "SubSectionObjective": "Remove headers that disclose internal details or software versions from HTTP responses.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Configure header-transformation rules.", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.TH15" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-13" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.LB.CN08.AR01", - "Description": "When a certificate is within 30 days of expiry, automated renewal MUST complete and deploy a new certificate within 24 hours.", - "Attributes": [ - { - "FamilyName": "Encryption", - "FamilyDescription": "Controls that ensure trustworthy TLS certificates and ciphers.", - "Section": "CCC.LB.CN08 Automate Certificate Renewal", - "SubSection": "", - "SubSectionObjective": "Maintain valid TLS certificates by automating renewal and deployment before expiry.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Use certificate-manager auto-renewal workflows.", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH07" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-6" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-17" - ] - } - ] - } - ], - "Checks": [] - }, { "Id": "CCC.Logging.CN01.AR01", "Description": "When a new cloud account is created, provider-level audit and network flow logging MUST be enabled by default and directed to the central sink.", "Attributes": [ { "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", "Section": "CCC.Logging.CN01 Centralized and Comprehensive Log Aggregation", "SubSection": "", "SubSectionObjective": "Ensure all operational and security logs from across the cloud environment, including applications, operating systems, network traffic, and cloud service activity, are captured automatically and streamed to a central, secure log management service.", @@ -1591,10 +2312,7 @@ ], "Checks": [ "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "network_flow_log_captured_sent", - "app_http_logs_enabled", - "appinsights_ensure_is_configured" + "network_flow_log_captured_sent" ] }, { @@ -1603,7 +2321,7 @@ "Attributes": [ { "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", "Section": "CCC.Logging.CN01 Centralized and Comprehensive Log Aggregation", "SubSection": "", "SubSectionObjective": "Ensure all operational and security logs from across the cloud environment, including applications, operating systems, network traffic, and cloud service activity, are captured automatically and streamed to a central, secure log management service.", @@ -1643,8 +2361,7 @@ "monitor_diagnostic_settings_exists", "monitor_diagnostic_setting_with_appropriate_categories", "app_http_logs_enabled", - "keyvault_logging_enabled", - "network_flow_log_captured_sent" + "keyvault_logging_enabled" ] }, { @@ -1653,52 +2370,7 @@ "Attributes": [ { "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", - "Section": "CCC.Logging.CN02 Enforce Data Retention Policy for Logs", - "SubSection": "", - "SubSectionObjective": "Ensure that the retention period configured for logs aligns with the organization's data retention policy.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Logging.TH05" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "GV.PO-01" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-11" - ] - } - ] - } - ], - "Checks": [ - "network_flow_log_more_than_90_days" - ] - }, - { - "Id": "CCC.Logging.CN02.AR02", - "Description": "When a query is performed to retrieve log events older than the number of days defined in the organisation's data retention policy, it MUST return an empty result.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", "Section": "CCC.Logging.CN02 Enforce Data Retention Policy for Logs", "SubSection": "", "SubSectionObjective": "Ensure that the retention period configured for logs aligns with the organization's data retention policy.", @@ -1740,12 +2412,59 @@ ] }, { - "Id": "CCC.AuditLog.CN08.AR01", + "Id": "CCC.Logging.CN02.AR02", + "Description": "When a query is performed to retrieve log events older than the number of days defined in the organisation's data retention policy, it MUST return an empty result.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", + "Section": "CCC.Logging.CN02 Enforce Data Retention Policy for Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that the retention period configured for logs aligns with the organization's data retention policy.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Logging.TH05" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "GV.PO-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-11" + ] + } + ] + } + ], + "Checks": [ + "network_flow_log_more_than_90_days", + "sqlserver_auditing_retention_90_days", + "postgresql_flexible_server_log_retention_days_greater_3" + ] + }, + { + "Id": "CCC.Logging.CN03.AR01", "Description": "When an attempt is made to modify or delete data before the object lock period expires, then the action MUST be denied.", "Attributes": [ { "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", "Section": "CCC.Logging.CN03 Enable Object Lock On Log Bucket", "SubSection": "", "SubSectionObjective": "Ensure log immutability by enabling Write Once, Read Many (WORM) protection using object lock on log storage buckets. This prevents logs from being modified or deleted during the defined retention period, supporting compliance and forensic integrity.", @@ -1753,12 +2472,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Configure object lock policy. ", + "Recommendation": "Configure object lock policy.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -1782,12 +2501,12 @@ "Checks": [] }, { - "Id": "CCC.AuditLog.CN04.AR01", + "Id": "CCC.Logging.CN04.AR01", "Description": "When restricted fields are accessed by unauthorized users, then those fields MUST remain masked.", "Attributes": [ { "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can access and modify logs. ", + "FamilyDescription": "Controls that restrict who can access and modify logs.", "Section": "CCC.Logging.CN04 Restrict Field And Log Type Access", "SubSection": "", "SubSectionObjective": "Configure access to logs to follow the principle of least privilege in particular where technically possible limit the log fields users have access to to prevent accidental exposure to sensitive information such as PII.", @@ -1795,7 +2514,7 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Review field level access controls on log data. ", + "Recommendation": "Review field level access controls on log data.", "SectionThreatMappings": [ { "ReferenceId": "CCC", @@ -1826,10 +2545,7 @@ } ], "Checks": [ - "monitor_diagnostic_settings_exists", - "keyvault_logging_enabled", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted" + "monitor_storage_account_with_activity_logs_is_private" ] }, { @@ -1838,7 +2554,7 @@ "Attributes": [ { "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can access and modify logs. ", + "FamilyDescription": "Controls that restrict who can access and modify logs.", "Section": "CCC.Logging.CN05 Ensure Log Bucket is Not Publicly Accessible", "SubSection": "", "SubSectionObjective": "Ensure that log storage buckets are not publicly accessible to prevent unauthorized access to sensitive log data. In addition, logs should be replicated to another cloud region to enhance availability, durability, and support disaster recovery requirements.", @@ -1847,12 +2563,12 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted. ", + "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -1875,8 +2591,6 @@ ], "Checks": [ "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "monitor_diagnostic_settings_exists", "storage_blob_public_access_level_is_disabled" ] }, @@ -1886,7 +2600,7 @@ "Attributes": [ { "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can access and modify logs. ", + "FamilyDescription": "Controls that restrict who can access and modify logs.", "Section": "CCC.Logging.CN05 Ensure Log Bucket is Not Publicly Accessible", "SubSection": "", "SubSectionObjective": "Ensure that log storage buckets are not publicly accessible to prevent unauthorized access to sensitive log data. In addition, logs should be replicated to another cloud region to enhance availability, durability, and support disaster recovery requirements.", @@ -1895,12 +2609,12 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted. ", + "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -1923,10 +2637,7 @@ ], "Checks": [ "storage_blob_public_access_level_is_disabled", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "monitor_diagnostic_settings_exists", - "storage_geo_redundant_enabled" + "monitor_storage_account_with_activity_logs_is_private" ] }, { @@ -1935,7 +2646,7 @@ "Attributes": [ { "FamilyName": "Logging and Monitoring", - "FamilyDescription": "Controls that collect, alert, and retain logging-related events. ", + "FamilyDescription": "Controls that collect, alert, and retain logging-related events.", "Section": "CCC.Logging.CN06 Detect and Alert on Potential Log Exfiltration", "SubSection": "", "SubSectionObjective": "Identify and alert on anomalous data access patterns that may indicate an attempt to exfiltrate log data.", @@ -1972,9 +2683,7 @@ ] } ], - "Checks": [ - "apim_threat_detection_llm_jacking" - ] + "Checks": [] }, { "Id": "CCC.Logging.CN07.AR01", @@ -1982,7 +2691,7 @@ "Attributes": [ { "FamilyName": "Logging and Monitoring", - "FamilyDescription": "Controls that collect, alert, and retain logging-related events. ", + "FamilyDescription": "Controls that collect, alert, and retain logging-related events.", "Section": "CCC.Logging.CN07 Detect and Alert on Log Service Tampering", "SubSection": "", "SubSectionObjective": "Alert when any component of the critical logging infrastructure is disabled, modified, or deleted, indicating a defense evasion attempt.", @@ -1997,7 +2706,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH16" + "CCC.Core.TH16" ] } ], @@ -2020,921 +2729,14 @@ ] } ], - "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_alert_create_update_nsg", - "monitor_alert_delete_nsg", - "monitor_alert_create_update_public_ip_address_rule", - "monitor_alert_delete_public_ip_address_rule", - "monitor_alert_create_update_security_solution", - "monitor_alert_delete_security_solution", - "monitor_alert_create_policy_assignment", - "monitor_alert_delete_policy_assignment", - "monitor_alert_service_health_exists", - "monitor_alert_create_update_sqlserver_fr", - "monitor_alert_delete_sqlserver_fr" - ] - }, - { - "Id": "CCC.ObjStor.CN01.AR01", - "Description": "When a request is made to read a protected bucket, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN01 Prevent Unencrypted Requests", - "SubSection": "CCC.ObjStor.C01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", - "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption that can impact data availability and integrity.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01", - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-04", - "DCS-06" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "storage_ensure_encryption_with_customer_managed_keys", - "databricks_workspace_cmk_encryption_enabled", - "vm_ensure_attached_disks_encrypted_with_cmk", - "vm_ensure_unattached_disks_encrypted_with_cmk" - ] - }, - { - "Id": "CCC.ObjStor.CN01.AR02", - "Description": "When a request is made to read a protected object, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN01 Prevent Unencrypted Requests", - "SubSection": "CCC.ObjStor.C01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", - "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption that can impact data availability and integrity.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01", - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-04", - "DCS-06" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "storage_ensure_encryption_with_customer_managed_keys", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "keyvault_rbac_enabled", - "keyvault_private_endpoints", - "keyvault_rbac_key_expiration_set", - "keyvault_rbac_secret_expiration_set" - ] - }, - { - "Id": "CCC.ObjStor.CN01.AR03", - "Description": "When a request is made to write to a bucket, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN01 Prevent Unencrypted Requests", - "SubSection": "CCC.ObjStor.C01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", - "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption that can impact data availability and integrity.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01", - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-04", - "DCS-06" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "storage_ensure_encryption_with_customer_managed_keys", - "storage_ensure_private_endpoints_in_storage_accounts", - "keyvault_rbac_enabled", - "keyvault_private_endpoints", - "keyvault_access_only_through_private_endpoints" - ] - }, - { - "Id": "CCC.ObjStor.CN01.AR04", - "Description": "When a request is made to write to an object, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN01 Prevent Unencrypted Requests", - "SubSection": "CCC.ObjStor.C01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", - "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption that can impact data availability and integrity.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01", - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-04", - "DCS-06" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "storage_ensure_encryption_with_customer_managed_keys", - "databricks_workspace_cmk_encryption_enabled", - "monitor_storage_account_with_activity_logs_cmk_encrypted" - ] - }, - { - "Id": "CCC.ObjStor.CN03.AR01", - "Description": "When an object storage bucket deletion is attempted, the bucket MUST be fully recoverable for a set time-frame after deletion is requested.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "CCC.ObjStor.C03 Prevent Bucket Deletion Through Irrevocable Bucket Retention Policy", - "SubSectionObjective": "Ensure that object storage bucket is not deleted after creation, and that the preventative measure cannot be unset.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "storage_blob_versioning_is_enabled", - "storage_ensure_soft_delete_is_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN03.AR02", - "Description": "When an attempt is made to modify the retention policy for an object storage bucket, the service MUST prevent the policy from being modified.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "CCC.ObjStor.C03 Prevent Bucket Deletion Through Irrevocable Bucket Retention Policy", - "SubSectionObjective": "Ensure that object storage bucket is not deleted after creation, and that the preventative measure cannot be unset.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - } - ] - } - ], "Checks": [] }, - { - "Id": "CCC.ObjStor.CN04.AR01", - "Description": "When an object is uploaded to the object storage system, the object MUST automatically receive a default retention policy that prevents premature deletion or modification.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN04 Log All Access and Changes", - "SubSection": "CCC.ObjStor.C04 Objects have an Effective Retention Policy by Default", - "SubSectionObjective": "Ensure that all objects stored in the object storage system have a retention policy applied by default, preventing premature deletion or modification of objects and ensuring compliance with data retention regulations.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "storage_blob_versioning_is_enabled", - "storage_ensure_soft_delete_is_enabled", - "storage_ensure_file_shares_soft_delete_is_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN04.AR02", - "Description": "When an attempt is made to delete or modify an object that is subject to an active retention policy, the service MUST prevent the action from being completed.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN04 Log All Access and Changes", - "SubSection": "CCC.ObjStor.C04 Objects have an Effective Retention Policy by Default", - "SubSectionObjective": "Ensure that all objects stored in the object storage system have a retention policy applied by default, preventing premature deletion or modification of objects and ensuring compliance with data retention regulations.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "storage_ensure_soft_delete_is_enabled", - "storage_ensure_file_shares_soft_delete_is_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN05.AR01", - "Description": "When an object is uploaded to the object storage bucket, the object MUST be stored with a unique identifier.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN05 Prevent Access from Untrusted Entities", - "SubSection": "CCC.ObjStor.C05 Versioning is Enabled for All Objects in the Bucket", - "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - } - ] - } - ], - "Checks": [ - "storage_blob_versioning_is_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN05.AR02", - "Description": "When an object is modified, the service MUST assign a new unique identifier to the modified object to differentiate it from the previous version.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN05 Prevent Access from Untrusted Entities", - "SubSection": "CCC.ObjStor.C05 Versioning is Enabled for All Objects in the Bucket", - "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - } - ] - } - ], - "Checks": [ - "storage_blob_versioning_is_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN05.AR03", - "Description": "When an object is modified, the service MUST allow for recovery of previous versions of the object.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN05 Prevent Access from Untrusted Entities", - "SubSection": "CCC.ObjStor.C05 Versioning is Enabled for All Objects in the Bucket", - "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - } - ] - } - ], - "Checks": [ - "storage_blob_versioning_is_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN05.AR04", - "Description": "When an object is deleted, the service MUST retain other versions of the object to allow for recovery of previous versions.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN05 Prevent Access from Untrusted Entities", - "SubSection": "CCC.ObjStor.C05 Versioning is Enabled for All Objects in the Bucket", - "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - } - ] - } - ], - "Checks": [ - "storage_blob_versioning_is_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN06.AR01", - "Description": "When an object storage bucket is accessed, the service MUST store access logs in a separate data store.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN06 Prevent Deployment in Restricted Regions", - "SubSection": "CCC.ObjStor.C06 Access Logs are Stored in a Separate Data Store", - "SubSectionObjective": "Ensure that access logs for object storage buckets are stored in a separate data store to protect against unauthorized access, tampering, or deletion of logs (Logbuckets are exempt from this requirement, but must be tlp-red).", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH07", - "CCC.TH09" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-6" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-07", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.15.0" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-9", - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted" - ] - }, - { - "Id": "CCC.ObjStor.CN02.AR01", - "Description": "When a permission set is allowed for an object in a bucket, the service MUST allow the same permission set to access all objects in the same bucket.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN02 Ensure Data Encryption at Rest for All Stored Data", - "SubSection": "CCC.ObjStor.C02 Enforce Uniform Bucket-level Access to Prevent Inconsistent Permissions", - "SubSectionObjective": "Ensure that uniform bucket-level access is enforced across all object storage buckets. This prevents the use of ad-hoc or inconsistent object-level permissions, ensuring centralized, consistent, and secure access management in accordance with the principle of least privilege.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "AC-6" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-09" - ] - } - ] - } - ], - "Checks": [ - "storage_blob_public_access_level_is_disabled", - "storage_default_network_access_rule_is_denied", - "storage_ensure_private_endpoints_in_storage_accounts" - ] - }, - { - "Id": "CCC.ObjStor.CN02.AR02", - "Description": "When a permission set is denied for an object in a bucket, the service MUST deny the same permission set to access all objects in the same bucket.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN02 Ensure Data Encryption at Rest for All Stored Data", - "SubSection": "CCC.ObjStor.C02 Enforce Uniform Bucket-level Access to Prevent Inconsistent Permissions", - "SubSectionObjective": "Ensure that uniform bucket-level access is enforced across all object storage buckets. This prevents the use of ad-hoc or inconsistent object-level permissions, ensuring centralized, consistent, and secure access management in accordance with the principle of least privilege.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "AC-6" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-09" - ] - } - ] - } - ], - "Checks": [ - "storage_blob_public_access_level_is_disabled", - "storage_account_key_access_disabled", - "storage_default_to_entra_authorization_enabled", - "storage_ensure_private_endpoints_in_storage_accounts" - ] - }, { "Id": "CCC.Monitor.CN01.AR01", "Description": "When an External Monitoring system exceeds the anticipated rate of monitoring checks then Rate Limiting MUST be applied and an Audit Alert MUST be generated.", "Attributes": [ { - "FamilyName": "Logging & Monitoring", + "FamilyName": "Logging and Monitoring", "FamilyDescription": "Controls that collect, alert, and retain events from other monitoring services.", "Section": "CCC.Monitor.CN01 Rate Limiting on External Monitoring", "SubSection": "", @@ -2972,25 +2774,14 @@ ] } ], - "Checks": [ - "monitor_alert_create_update_sqlserver_fr", - "monitor_alert_delete_nsg", - "monitor_alert_delete_public_ip_address_rule", - "monitor_alert_delete_security_solution", - "monitor_alert_create_policy_assignment", - "monitor_alert_create_update_public_ip_address_rule", - "monitor_alert_create_update_security_solution", - "monitor_alert_create_update_nsg", - "monitor_alert_service_health_exists", - "monitor_diagnostic_settings_exists" - ] + "Checks": [] }, { "Id": "CCC.Monitor.CN02.AR01", "Description": "When an Custom or User-Defined Metric starts to flood a collector, then a rate limit MUST be applied to reduce the network impact of traffic and an alert must triggered.", "Attributes": [ { - "FamilyName": "Logging & Monitoring", + "FamilyName": "Logging and Monitoring", "FamilyDescription": "Controls that collect, alert, and retain events from other monitoring services.", "Section": "CCC.Monitor.CN02 Rate Limiting on Metric Generation", "SubSection": "", @@ -3028,9 +2819,7 @@ ] } ], - "Checks": [ - "apim_threat_detection_llm_jacking" - ] + "Checks": [] }, { "Id": "CCC.Monitor.CN03.AR01", @@ -3075,10 +2864,7 @@ ] } ], - "Checks": [ - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted" - ] + "Checks": [] }, { "Id": "CCC.Monitor.CN04.AR01", @@ -3146,7 +2932,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH10" + "CCC.Core.TH10" ] } ], @@ -3212,11 +2998,1111 @@ ], "Checks": [ "app_function_identity_is_configured", - "app_function_identity_without_admin_privileges", - "app_function_access_keys_configured", - "app_function_not_publicly_accessible", - "app_register_with_identity", - "app_ensure_auth_is_set_up" + "app_function_access_keys_configured" + ] + }, + { + "Id": "CCC.ObjStor.CN01.AR01", + "Description": "When a request is made to read a bucket, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", + "SubSection": "", + "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption, or sensitive data decryption.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-03", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_ensure_encryption_with_customer_managed_keys", + "keyvault_rbac_enabled", + "keyvault_private_endpoints" + ] + }, + { + "Id": "CCC.ObjStor.CN01.AR02", + "Description": "When a request is made to read an object, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", + "SubSection": "", + "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption, or sensitive data decryption.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-03", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_ensure_encryption_with_customer_managed_keys", + "keyvault_rbac_enabled", + "keyvault_private_endpoints" + ] + }, + { + "Id": "CCC.ObjStor.CN01.AR03", + "Description": "When a request is made to write to a bucket, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", + "SubSection": "", + "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption, or sensitive data decryption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-03", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_ensure_encryption_with_customer_managed_keys", + "keyvault_rbac_enabled", + "keyvault_private_endpoints" + ] + }, + { + "Id": "CCC.ObjStor.CN01.AR04", + "Description": "When a request is made to write to an object, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", + "SubSection": "", + "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption, or sensitive data decryption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-03", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_ensure_encryption_with_customer_managed_keys", + "keyvault_rbac_enabled", + "keyvault_private_endpoints" + ] + }, + { + "Id": "CCC.ObjStor.CN03.AR01", + "Description": "When an object storage bucket deletion is attempted, the bucket MUST be fully recoverable for a set time-frame after deletion is requested.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN03 Prevent Bucket Deletion Through Irrevocable Bucket Retention Policy", + "SubSection": "", + "SubSectionObjective": "Ensure that object storage bucket is not deleted after creation, and that the preventative measure cannot be unset.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_ensure_soft_delete_is_enabled", + "storage_blob_versioning_is_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN03.AR02", + "Description": "When an attempt is made to modify the retention policy for an object storage bucket, the service MUST prevent the policy from being modified.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN03 Prevent Bucket Deletion Through Irrevocable Bucket Retention Policy", + "SubSection": "", + "SubSectionObjective": "Ensure that object storage bucket is not deleted after creation, and that the preventative measure cannot be unset.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_ensure_soft_delete_is_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN04.AR01", + "Description": "When an object is uploaded to the object storage system, the object MUST automatically receive a default retention policy that prevents premature deletion or modification.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN04 Objects have an Effective Retention Policy by Default", + "SubSection": "", + "SubSectionObjective": "Ensure that all objects stored in the object storage system have a retention policy applied by default, preventing premature deletion or modification of objects.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + }, + { + "ReferenceId": "CCC.ObjStor", + "Identifiers": [ + "CCC.ObjStor.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_ensure_soft_delete_is_enabled", + "storage_ensure_file_shares_soft_delete_is_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN04.AR02", + "Description": "When an attempt is made to delete or modify an object that is subject to an active retention policy, the service MUST prevent the action from being completed.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN04 Objects have an Effective Retention Policy by Default", + "SubSection": "", + "SubSectionObjective": "Ensure that all objects stored in the object storage system have a retention policy applied by default, preventing premature deletion or modification of objects.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + }, + { + "ReferenceId": "CCC.ObjStor", + "Identifiers": [ + "CCC.ObjStor.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_ensure_soft_delete_is_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN05.AR01", + "Description": "When an object is uploaded to the object storage bucket, the object MUST be stored with a unique identifier.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN05 Versioning is Enabled for All Objects in the Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_blob_versioning_is_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN05.AR02", + "Description": "When an object is modified, the service MUST assign a new unique identifier to the modified object to differentiate it from the previous version.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN05 Versioning is Enabled for All Objects in the Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_blob_versioning_is_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN05.AR03", + "Description": "When an object is modified, the service MUST allow for recovery of previous versions of the object.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN05 Versioning is Enabled for All Objects in the Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_blob_versioning_is_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN05.AR04", + "Description": "When an object is deleted, the service MUST retain other versions of the object to allow for recovery of previous versions.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN05 Versioning is Enabled for All Objects in the Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "storage_blob_versioning_is_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN07.AR01", + "Description": "The object storage service MUST support a configuration option that requires MFA to be successfully completed before any object deletion can be attempted, regardless of the request interface.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN07 Multi-Factor Authentication Is Required for Object Deletion", + "SubSection": "", + "SubSectionObjective": "Ensure that deletion of objects stored in the object storage system is protected by multi-factor authentication (MFA), reducing the risk of accidental, unauthorized, or compromised-credential–based data destruction.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06", + "CCC.Core.TH17" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "IAM-12" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.ObjStor.CN07.AR02", + "Description": "When MFA deletion protection is enabled on a bucket or object namespace, the service MUST deny any deletion request from an identity that has not satisfied the MFA requirement at the time of the request.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN07 Multi-Factor Authentication Is Required for Object Deletion", + "SubSection": "", + "SubSectionObjective": "Ensure that deletion of objects stored in the object storage system is protected by multi-factor authentication (MFA), reducing the risk of accidental, unauthorized, or compromised-credential–based data destruction.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06", + "CCC.Core.TH17" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "IAM-12" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.ObjStor.CN07.AR03", + "Description": "When an attempt is made to delete an object, the service's audit logs MUST clearly record each deletion attempt, including whether MFA was required and whether validation was met.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN07 Multi-Factor Authentication Is Required for Object Deletion", + "SubSection": "", + "SubSectionObjective": "Ensure that deletion of objects stored in the object storage system is protected by multi-factor authentication (MFA), reducing the risk of accidental, unauthorized, or compromised-credential–based data destruction.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06", + "CCC.Core.TH17" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "IAM-12" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.ObjStor.CN02.AR01", + "Description": "When a permission set is allowed for an object in a bucket, the service MUST allow the same permission set to access all objects in the same bucket.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources.", + "Section": "CCC.ObjStor.CN02 Enforce Uniform Bucket-level Access to Prevent Inconsistent Permissions", + "SubSection": "", + "SubSectionObjective": "Ensure that uniform bucket-level access is enforced across all object storage buckets. This prevents the use of ad-hoc or inconsistent object-level permissions, ensuring centralized, consistent, and secure access management in accordance with the principle of least privilege.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08" + ] + } + ] + } + ], + "Checks": [ + "storage_account_key_access_disabled", + "storage_default_to_entra_authorization_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN02.AR02", + "Description": "When a permission set is denied for an object in a bucket, the service MUST deny the same permission set to access all objects in the same bucket.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources.", + "Section": "CCC.ObjStor.CN02 Enforce Uniform Bucket-level Access to Prevent Inconsistent Permissions", + "SubSection": "", + "SubSectionObjective": "Ensure that uniform bucket-level access is enforced across all object storage buckets. This prevents the use of ad-hoc or inconsistent object-level permissions, ensuring centralized, consistent, and secure access management in accordance with the principle of least privilege.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08" + ] + } + ] + } + ], + "Checks": [ + "storage_account_key_access_disabled", + "storage_default_to_entra_authorization_enabled" + ] + }, + { + "Id": "CCC.LB.CN01.AR01", + "Description": "When a single client sends more than 2000 requests within any 5-minute sliding window, the load balancer MUST throttle all subsequent requests from that client for at least 60 seconds.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity.", + "Section": "CCC.LB.CN01 Enforce and Detect Rate Limiting", + "SubSection": "", + "SubSectionObjective": "Detect and throttle malicious or excessive requests to prevent downstream resource exhaustion and brute-force activity.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Implement per-IP token-bucket limits with and verify via synthetic traffic tests.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH01", + "CCC.LB.TH09" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-1", + "PR.AC-7", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-6", + "SC-5", + "AC-7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.LB.CN01.AR02", + "Description": "When throttling is invoked, the load balancer MUST record the event in the access log within 5 minutes for alerting and trend analysis.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity.", + "Section": "CCC.LB.CN01 Enforce and Detect Rate Limiting", + "SubSection": "", + "SubSectionObjective": "Detect and throttle malicious or excessive requests to prevent downstream resource exhaustion and brute-force activity.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Enable access logging and configure metric filters on HTTP 429 counts to trigger alerts.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH01", + "CCC.LB.TH09" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-1", + "PR.AC-7", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-6", + "SC-5", + "AC-7" + ] + } + ] + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists" + ] + }, + { + "Id": "CCC.LB.CN06.AR01", + "Description": "When more than 10 percent of targets change from healthy to unhealthy within five minutes, an alert MUST be issued.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity.", + "Section": "CCC.LB.CN06 Secure Health-Check Telemetry", + "SubSection": "", + "SubSectionObjective": "Monitor health-check endpoints for tampering and alert on abnormal status changes.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Instrument metrics for health check results and target removal events. Configure monitoring alarms to alert on abnormal spikes in unhealthy targets.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH05" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.AE-2" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4" + ] + } + ] + } + ], + "Checks": [ + "monitor_alert_service_health_exists" + ] + }, + { + "Id": "CCC.LB.CN04.AR01", + "Description": "When routing weights change, the request MUST originate from an explicitly defined and trusted identity and MUST be logged.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can change or query load-balancer resources.", + "Section": "CCC.LB.CN04 Enforce Distribution Policies", + "SubSection": "", + "SubSectionObjective": "Ensure traffic-splitting weights and algorithms are modified only by trusted identities.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Define a list of trusted principals allowed to modify routing configurations. Enforce via conditional access policies, and log changes using audit logging.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3" + ] + } + ] + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists" + ] + }, + { + "Id": "CCC.LB.CN05.AR01", + "Description": "When stickiness is enabled, session cookies MUST expire within 30 minutes of inactivity.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can change or query load-balancer resources.", + "Section": "CCC.LB.CN05 Validate Session Affinity", + "SubSection": "", + "SubSectionObjective": "Configure session persistence to minimise fixation and hijacking risks.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Audit CCC.LB.CP15 parameters via configuration scans.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-7" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-23" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.LB.CN09.AR01", + "Description": "When an API call originates outside the approved CIDR set, the request MUST be denied.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can change or query load-balancer resources.", + "Section": "CCC.LB.CN09 Restrict Management API Access", + "SubSection": "", + "SubSectionObjective": "Limit load-balancer API calls to authorised identities and trusted networks.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Combine VPC endpoints with IAM condition-key filters for protected APIs.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH08" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-5" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7" + ] + } + ] + } + ], + "Checks": [ + "entra_conditional_access_policy_require_mfa_for_management_api" + ] + }, + { + "Id": "CCC.LB.CN02.AR01", + "Description": "When concurrent connections reach 80 percent of capacity, the autoscaling group MUST add at least one instance within five minutes.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "Controls that preserve availability and confidentiality of traffic processed by the load balancer.", + "Section": "CCC.LB.CN02 Auto-Scale Load Balancer Capacity", + "SubSection": "", + "SubSectionObjective": "Expand load-balancer capacity to maintain availability during traffic spikes.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Enable autoscaling policies.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH09" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "ID.BE-5" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "CP-10" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.LB.CN07.AR01", + "Description": "When responses pass through the load balancer, the \"Server\" header MUST be replaced with \"lb\".", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "Controls that preserve availability and confidentiality of traffic processed by the load balancer.", + "Section": "CCC.LB.CN07 Scrub Sensitive Headers", + "SubSection": "", + "SubSectionObjective": "Remove headers that disclose internal details or software versions from HTTP responses.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Configure header-transformation rules.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.Core.TH15" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-13" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.LB.CN08.AR01", + "Description": "When a certificate is within 30 days of expiry, automated renewal MUST complete and deploy a new certificate within 24 hours.", + "Attributes": [ + { + "FamilyName": "Encryption", + "FamilyDescription": "Controls that ensure trustworthy TLS certificates and ciphers.", + "Section": "CCC.LB.CN08 Automate Certificate Renewal", + "SubSection": "", + "SubSectionObjective": "Maintain valid TLS certificates by automating renewal and deployment before expiry.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Use certificate-manager auto-renewal workflows.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH07" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-17" + ] + } + ] + } + ], + "Checks": [ + "keyvault_key_rotation_enabled" ] }, { @@ -3272,6 +4158,58 @@ ], "Checks": [] }, + { + "Id": "CCC.VPC.CN02.AR01", + "Description": "When a resource is created in a public subnet, that resource MUST NOT be assigned an external IP address by default.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.VPC.CN02 Limit Resource Creation in Public Subnet", + "SubSection": "", + "SubSectionObjective": "Restrict the creation of resources in the public subnet with direct access to the internet to minimize attack surfaces.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.VPC.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "SEF-05" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.13.1.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-4" + ] + } + ] + } + ], + "Checks": [] + }, { "Id": "CCC.VPC.CN03.AR01", "Description": "When a VPC peering connection is requested, the service MUST prevent connections from VPCs that are not explicitly allowed.", @@ -3379,10 +4317,393 @@ ], "Checks": [ "network_flow_log_captured_sent", - "network_flow_log_more_than_90_days", "network_watcher_enabled" ] }, + { + "Id": "CCC.KeyMgmt.CN01.AR01", + "Description": "When a key version is scheduled for deletion or disabled, an alert MUST be generated within five minutes.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain key-management events.", + "Section": "CCC.KeyMgmt.CN01 Alert on Key-version Changes", + "SubSection": "", + "SubSectionObjective": "Generate near-real-time alerts when a KMS key version is disabled or scheduled for deletion, enabling rapid investigation and recovery.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Use native event services (e.g., CloudWatch Events, Azure Monitor, Cloud Audit Logs) to route notifications to an incident-response channel.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.KeyMgmt.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "RS.AN-1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "IR-5" + ] + } + ] + } + ], + "Checks": [ + "keyvault_logging_enabled", + "keyvault_recoverable" + ] + }, + { + "Id": "CCC.KeyMgmt.CN02.AR01", + "Description": "When IAM roles and key policies are reviewed, Decrypt permission MUST be granted exclusively to documented authorised principals.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that enforce least-privilege use of KMS operations.", + "Section": "CCC.KeyMgmt.CN02 Limit Decrypt Permissions", + "SubSection": "", + "SubSectionObjective": "Restrict the Decrypt operation to authorised principals only, applying the principle of least privilege to protect sensitive data.", + "Applicability": [ + "tlp-green" + ], + "Recommendation": "Periodically audit policy documents via automated tooling and report any deviations.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.KeyMgmt.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "keyvault_rbac_enabled", + "keyvault_access_only_through_private_endpoints" + ] + }, + { + "Id": "CCC.KeyMgmt.CN03.AR01", + "Description": "When rotation settings are examined, rotation MUST be enabled with an interval not exceeding 365 days.", + "Attributes": [ + { + "FamilyName": "Key Lifecycle Management", + "FamilyDescription": "Controls that govern creation, rotation, import, and retirement of cryptographic keys.", + "Section": "CCC.KeyMgmt.CN03 Enforce Automatic Rotation", + "SubSection": "", + "SubSectionObjective": "Ensure symmetric keys rotate automatically within policy intervals to reduce exposure of key material.", + "Applicability": [ + "tlp-green" + ], + "Recommendation": "Use cloud-provider rotation features and verify via configuration scanning.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.KeyMgmt.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-12" + ] + } + ] + } + ], + "Checks": [ + "keyvault_key_rotation_enabled" + ] + }, + { + "Id": "CCC.KeyMgmt.CN04.AR01", + "Description": "When a key import request is processed, the key MUST use an approved algorithm (RSA-2048+, EC-P256+) and originate from a certified HSM.", + "Attributes": [ + { + "FamilyName": "Key Lifecycle Management", + "FamilyDescription": "Controls that govern creation, rotation, import, and retirement of cryptographic keys.", + "Section": "CCC.KeyMgmt.CN04 Validate Imported Keys", + "SubSection": "", + "SubSectionObjective": "Accept only externally generated keys that meet approved cryptographic strength and provenance requirements.", + "Applicability": [ + "tlp-green" + ], + "Recommendation": "Implement an approval workflow that validates attestation data before import.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.KeyMgmt.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-28" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.SecMgmt.CN01.AR01", + "Description": "Attempt to use an outdated version of a secret after its rotation period has passed and verify that access is denied.", + "Attributes": [ + { + "FamilyName": "Data Protection", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.SecMgmt.CN01 Enforce Automatic Secret Rotation", + "SubSection": "", + "SubSectionObjective": "Ensure that secrets are automatically rotated on a defined schedule to reduce the risk of secret compromise and unauthorized access.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH14" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-12", + "SC-28" + ] + } + ] + } + ], + "Checks": [ + "keyvault_rbac_secret_expiration_set", + "keyvault_non_rbac_secret_expiration_set" + ] + }, + { + "Id": "CCC.SecMgmt.CN02.AR01", + "Description": "Attempt to retrieve a secret from an unauthorized region and verify that access is denied.", + "Attributes": [ + { + "FamilyName": "Data Protection", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.SecMgmt.CN02 Enforce Secret Replication Policies", + "SubSection": "", + "SubSectionObjective": "Ensure that secrets are replicated only to authorized locations as per organizational data residency and compliance requirements.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH03", + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-5" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", + "SC-7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.DataWar.CN01.AR01", + "Description": "Attempt to access underlying database tables directly without using managed views and verify that access is denied.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.DataWar.CN01 Enforce Use of Managed Views for Data Access", + "SubSection": "", + "SubSectionObjective": "Ensure that data access is provided through managed views, restricting users from accessing underlying tables directly and enforcing consistent security policies.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.DataWar.CN02.AR01", + "Description": "Attempt to query sensitive columns without the necessary permissions and verify that access is denied or data is masked.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.DataWar.CN02 Enforce Column-Level Security Policies", + "SubSection": "", + "SubSectionObjective": "Ensure that access to sensitive data columns is restricted based on user roles, preventing unauthorized access to sensitive information.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.DataWar.CN03.AR01", + "Description": "Attempt to query data rows that the user should not have access to and verify that access is denied or data is not returned.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.DataWar.CN03 Enforce Row-Level Security Policies", + "SubSection": "", + "SubSectionObjective": "Ensure that access to data rows is restricted based on user roles or attributes, preventing unauthorized access to specific subsets of data.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, { "Id": "CCC.Vector.CN01.AR01", "Description": "When a vector embedding is submitted for indexing, the system MUST validate that it matches expected schema, dimension, and format profiles.", @@ -3406,7 +4727,7 @@ "Identifiers": [ "CCC.Vector.TH02", "CCC.Vector.TH05", - "CCC.TH12" + "CCC.Core.TH12" ] } ], @@ -3445,7 +4766,7 @@ "Identifiers": [ "CCC.Vector.TH02", "CCC.Vector.TH04", - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -3459,13 +4780,7 @@ ] } ], - "Checks": [ - "iam_role_user_access_admin_restricted", - "iam_custom_role_has_permissions_to_administer_resource_locks", - "iam_subscription_roles_owner_custom_not_created", - "app_function_identity_without_admin_privileges", - "app_function_identity_is_configured" - ] + "Checks": [] }, { "Id": "CCC.Vector.CN03.AR01", @@ -3487,7 +4802,7 @@ "ReferenceId": "CCC", "Identifiers": [ "CCC.Vector.TH03", - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -3526,7 +4841,7 @@ "ReferenceId": "CCC", "Identifiers": [ "CCC.Vector.TH02", - "CCC.TH12" + "CCC.Core.TH12" ] } ], @@ -3562,8 +4877,8 @@ "ReferenceId": "CCC", "Identifiers": [ "CCC.Vector.TH04", - "CCC.TH09", - "CCC.TH04" + "CCC.Core.TH09", + "CCC.Core.TH04" ] } ], @@ -3601,7 +4916,7 @@ "ReferenceId": "CCC", "Identifiers": [ "CCC.Vector.TH05", - "CCC.TH06" + "CCC.Core.TH06" ] } ], @@ -3646,451 +4961,25 @@ "Checks": [] }, { - "Id": "CCC.Core.CN01.AR01", - "Description": "When a port is exposed for non-SSH network traffic, all traffic MUST include a TLS handshake AND be encrypted using TLS 1.3 or higher.", + "Id": "CCC.RDMS.CN01.AR02", + "Description": "When an attempt is made to authenticate to the database using known default credentials, the authentication attempt must fail and no access should be granted.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN01 Password Management", "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Most cloud services enable TLS 1.3 by default. Where it is not already set, ensure that your services are configured or updated accordingly. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "app_minimum_tls_version_12", - "app_ensure_http_is_redirected_to_https", - "storage_secure_transfer_required_is_enabled", - "storage_ensure_minimum_tls_version_12" - ] - }, - { - "Id": "CCC.Core.CN01.AR02", - "Description": "When a port is exposed for SSH network traffic, all traffic MUST include a SSH handshake AND be encrypted using SSHv2 or higher.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", - "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Any time port 22 is exposed, ensure that it has a properly implemented SSH server with SSHv2 enabled and configured with strong ciphers. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "network_ssh_internet_access_restricted", - "vm_linux_enforce_ssh_authentication", - "app_minimum_tls_version_12", - "storage_secure_transfer_required_is_enabled", - "storage_ensure_minimum_tls_version_12", - "app_ensure_http_is_redirected_to_https" - ] - }, - { - "Id": "CCC.Core.CN01.AR03", - "Description": "When the service receives unencrypted traffic, then it MUST either block the request or automatically redirect it to the secure equivalent.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", - "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Review firewall, load balancer, and application configurations to ensure insecure protocols such as HTTP, FTP, and Telnet are not exposed. Where possible, implement automatic redirection to secure protocols such as HTTPS, SFTP, SSH, and regularly scan for protocol drift. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "app_ensure_http_is_redirected_to_https", - "app_minimum_tls_version_12", - "storage_secure_transfer_required_is_enabled", - "storage_ensure_minimum_tls_version_12", - "app_ftp_deployment_disabled" - ] - }, - { - "Id": "CCC.Core.CN01.AR07", - "Description": "When a port is exposed, the service MUST ensure that the protocol and service officially assigned to that port number by the IANA Service Name and Transport Protocol Port Number Registry, and no other, is run on that port.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", - "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Reference the IANA Service Name and Transport Protocol Port Number Registry for more information about correct protocol-to-port assignments. Avoid running non-standard services on well-known ports. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "app_minimum_tls_version_12", - "app_ensure_http_is_redirected_to_https", - "app_ensure_using_http20", - "storage_smb_channel_encryption_with_secure_algorithm", - "storage_secure_transfer_required_is_enabled", - "storage_ensure_minimum_tls_version_12", - "storage_smb_protocol_version_is_latest" - ] - }, - { - "Id": "CCC.Core.CN01.AR08", - "Description": "When a service transmits data using TLS, mutual TLS (mTLS) MUST be implemented to require both client and server certificate authentication for all connections.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", - "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Configure mTLS for all endpoints that process or transmit sensitive data. Ensure both client and server certificates are validated and managed securely. Regularly review certificate authorities and automate certificate rotation where possible. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "app_client_certificates_on", - "app_minimum_tls_version_12", - "app_ensure_http_is_redirected_to_https" - ] - }, - { - "Id": "CCC.Core.CN13.AR01", - "Description": "When a port is exposed that uses certificate-based encryption, the service MUST only use valid, unexpired certificates issued by a trusted certificate authority.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH18" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "app_client_certificates_on", - "app_minimum_tls_version_12", - "keyvault_key_expiration_set_in_non_rbac", - "keyvault_key_rotation_enabled", - "keyvault_rbac_key_expiration_set", - "keyvault_rbac_secret_expiration_set", - "keyvault_non_rbac_secret_expiration_set" - ] - }, - { - "Id": "CCC.Core.CN13.AR02", - "Description": "When a port is exposed that uses certificate-based encryption, the service MUST rotate active certificates within 180 days of issuance.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", + "SubSectionObjective": "Ensure default vendor-supplied DB administrator credentials are replaced with strong, unique passwords and that these credentials are properly managed using a secure password or secrets management solution.", "Applicability": [ + "tlp-red", "tlp-amber" ], - "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH18" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "keyvault_key_rotation_enabled", - "keyvault_key_expiration_set_in_non_rbac", - "keyvault_rbac_key_expiration_set", - "keyvault_non_rbac_secret_expiration_set", - "keyvault_rbac_secret_expiration_set", - "storage_ensure_encryption_with_customer_managed_keys" - ] - }, - { - "Id": "CCC.Core.CN13.AR03", - "Description": "When a port is exposed that uses certificate-based encryption, the service MUST rotate active certificates within 90 days of issuance.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH18" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "app_client_certificates_on", - "app_ensure_http_is_redirected_to_https", - "app_minimum_tls_version_12" - ] - }, - { - "Id": "CCC.Core.CN06.AR01", - "Description": "When the service is running, its region and availability zone MUST be included in a list of explicitly trusted or approved locations within the trust perimeter.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN06 Restrict Deployments to Trust Perimeter", - "SubSection": "", - "SubSectionObjective": "Ensure that the service and its child resources are only deployed on infrastructure in locations that are explicitly included within a defined trust perimeter.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Maintain an up-to-date list of trusted and approved regions based on organizational policies. Validate the service's deployment location is included in this list. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH03" + "CCC.RDMS.TH01" ] } ], @@ -4098,19 +4987,89 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" + "PR.AA-01" ] }, { - "ReferenceId": "CCM", + "ReferenceId": "NIST_800_53", "Identifiers": [ - "DSP-19" + "AC-2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.RDMS.CN02.AR01", + "Description": "When repeated failed login attempts are made in a short timeframe, the account must be locked out or rate-limited to prevent further login attempts.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN02 Account Lockout and Rate-Limiting", + "SubSection": "", + "SubSectionObjective": "Ensure the database enforces lockouts or rate-limiting after a specified number of failed authentication attempts. This prevents brute force or password-guessing attacks from succeeding.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.RDMS.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-1" ] }, { - "ReferenceId": "ISO_27001", + "ReferenceId": "NIST_800_53", "Identifiers": [ - "2013 A.11.1.1" + "AC-7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.RDMS.CN04.AR01", + "Description": "When there is an attempt to perform a backup or restore, then the attempt must fail with an access denied message if credentials or roles that are not explicitly authorized for backup/restore functions.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN04 Access Control for Backup and Restore Operations", + "SubSection": "", + "SubSectionObjective": "Restrict who can initiate, manage, and validate database backup or restore operations through strict role-based or least-privilege access. Prevents accidental or malicious restorations, protecting data integrity and availability.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.RDMS.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" ] }, { @@ -4122,44 +5081,28 @@ ] } ], - "Checks": [ - "aks_clusters_public_access_disabled", - "aks_clusters_created_with_private_nodes", - "containerregistry_not_publicly_accessible", - "containerregistry_uses_private_link", - "keyvault_private_endpoints", - "keyvault_access_only_through_private_endpoints", - "storage_ensure_private_endpoints_in_storage_accounts", - "network_http_internet_access_restricted", - "network_rdp_internet_access_restricted", - "network_ssh_internet_access_restricted", - "network_udp_internet_access_restricted", - "network_watcher_enabled", - "entra_trusted_named_locations_exists" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN06.AR02", - "Description": "When a child resource is deployed, its region and availability zone MUST be included in a list of explicitly trusted or approved locations within the trust perimeter.", + "Id": "CCC.RDMS.CN05.AR01", + "Description": "When an attempt is made to share a snapshot with an unauthorized account, the sharing request must be denied.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN06 Restrict Deployments to Trust Perimeter", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN05 Restrict Snapshot Sharing to Authorized Accounts", "SubSection": "", - "SubSectionObjective": "Ensure that the service and its child resources are only deployed on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "SubSectionObjective": "Ensure database snapshots can only be shared with explicitly authorized accounts, thereby minimizing the risk of data exposure or exfiltration.", "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-red", + "tlp-amber" ], - "Recommendation": "Maintain an up-to-date list of trusted and approved regions based on organizational policies. Validate that child resources can only be deployed to locations included in this list. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH03" + "CCC.RDMS.TH05" ] } ], @@ -4167,24 +5110,312 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-19" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.11.1.1" + "PR.DS-10" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ + "AC-4" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.RDMS.CN03.AR01", + "Description": "When backups are disabled, paused, or fail to run as scheduled, an alert must be triggered and logged.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN03 Enforce and Monitor Automated Backups", + "SubSection": "", + "SubSectionObjective": "Ensure database backups are automatically scheduled, actively monitored, and promptly reported if any disruptions occur. This helps maintain data integrity, facilitates disaster recovery, and supports business continuity when a system failure or breach occurs.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.RDMS.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IP-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "CP-9" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Build.CN01.AR01", + "Description": "Attempt to initiate a build using an unauthorized build agent and verify that the build is rejected.", + "Attributes": [ + { + "FamilyName": "Access Control", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.Build.CN01 Restrict Allowed Build Agents", + "SubSection": "", + "SubSectionObjective": "Ensure that builds are executed only on authorized build agents to maintain control over the build environment and prevent unauthorized code execution.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Build.CN02.AR01", + "Description": "Attempt to trigger a build from an unauthorized external service or repository and verify that the build does not start.", + "Attributes": [ + { + "FamilyName": "Access Control", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.Build.CN02 Restrict Allowed External Services for Build Triggers", + "SubSection": "", + "SubSectionObjective": "Ensure that builds can only be triggered by authorized external services or repositories to prevent unauthorized code execution or tampering.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Build.CN03.AR01", + "Description": "Attempt to access the build environment from an external network and verify that access is denied.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.Build.CN03 Deny External Network Access for Build Environments", + "SubSection": "", + "SubSectionObjective": "Ensure that build environments do not have external network access to prevent unauthorized external access and data exfiltration.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02", + "CCC.Core.TH05" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-5" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7", + "SC-5" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.CntrReg.CN01.AR01", + "Description": "Attempt to push an artifact with known vulnerabilities to the registry and observe if it is flagged or rejected by the vulnerability scanning process.", + "Attributes": [ + { + "FamilyName": "Risk Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.CntrReg.CN01 Implement Vulnerability Scanning for Artifacts", + "SubSection": "", + "SubSectionObjective": "Ensure that container images and artifacts stored in the container registry are scanned for vulnerabilities to identify and remediate security issues before deployment.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.CntrReg.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "ID.RA-1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "RA-5", + "SI-5" + ] + } + ] + } + ], + "Checks": [ + "defender_container_images_scan_enabled", + "defender_container_images_resolved_vulnerabilities" + ] + }, + { + "Id": "CCC.CntrReg.CN02.AR01", + "Description": "Confirm that artifacts older than the specified retention period are automatically deleted from the registry.", + "Attributes": [ + { + "FamilyName": "Data Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.CntrReg.CN02 Implement Cleanup Policies for Artifacts", + "SubSection": "", + "SubSectionObjective": "Ensure that unused or outdated artifacts are cleaned up according to defined policies to manage storage effectively and reduce security risks associated with outdated versions.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH14" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IP-6" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-12" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.IAM.CN01.AR01", + "Description": "When an identity policy for a non-administrative principal is evaluated, it MUST NOT grant permissions for creating credentials or generating temporary session tokens.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN01 Restrict IAM User Credentials Creation", + "SubSection": "", + "SubSectionObjective": "Prevent non-administrative principals from creating new long-lived credentials like access keys or generating temporary session tokens. This blocks a common privilege escalation and persistence vector.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3", + "AC-5", "AC-6" ] } @@ -4192,127 +5423,20 @@ } ], "Checks": [ - "entra_trusted_named_locations_exists" + "entra_policy_default_users_cannot_create_security_groups", + "entra_policy_ensure_default_user_cannot_create_apps" ] }, { - "Id": "CCC.Core.CN08.AR01", - "Description": "When data is created or modified, the data MUST have a complete and recoverable duplicate that is stored in a physically separate data center.", + "Id": "CCC.IAM.CN01.AR02", + "Description": "When a non-administrative principal attempts to create new credentials or a temporary session token, the service MUST deny the action.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN08 Replicate Data to Multiple Locations", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN01 Restrict IAM User Credentials Creation", "SubSection": "", - "SubSectionObjective": "Ensure that data is replicated across multiple physical locations to protect against data loss due to hardware failures, natural disasters, or other catastrophic events.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Implement automated data replication processes to ensure that data is consistently duplicated in another region or availability zone. Regularly test data recovery from the replicated location to ensure integrity and availability. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.PT-5" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "BCR-08", - "BCR-10", - "BCR-11" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "CP-2", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "storage_geo_redundant_enabled", - "vm_backup_enabled", - "vm_sufficient_daily_backup_retention_period" - ] - }, - { - "Id": "CCC.Core.CN08.AR02", - "Description": "When data is replicated into a second location, the service MUST be able to accurately represent the replication locations, replication status, and data synchronization status.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN08 Replicate Data to Multiple Locations", - "SubSection": "", - "SubSectionObjective": "Ensure that data is replicated across multiple physical locations to protect against data loss due to hardware failures, natural disasters, or other catastrophic events.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.PT-5" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "BCR-08", - "BCR-10", - "BCR-11" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "CP-2", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "storage_geo_redundant_enabled" - ] - }, - { - "Id": "CCC.Core.CN09.AR01", - "Description": "When the service is operational, its logs and any child resource logs MUST NOT be accessible from the resource they record access to.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", - "SubSection": "", - "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "SubSectionObjective": "Prevent non-administrative principals from creating new long-lived credentials like access keys or generating temporary session tokens. This blocks a common privilege escalation and persistence vector.", "Applicability": [ "tlp-clear", "tlp-green", @@ -4324,9 +5448,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07", - "CCC.TH09", - "CCC.TH04" + "CCC.IAM.TH03" ] } ], @@ -4334,57 +5456,48 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-6" + "PR.AA-05" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AU-9" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-02", - "LOG-04", - "LOG-09" + "AC-2", + "AC-3", + "AC-5", + "AC-6" ] } ] } ], "Checks": [ - "monitor_diagnostic_settings_exists", - "network_flow_log_captured_sent", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted" + "entra_policy_default_users_cannot_create_security_groups", + "entra_policy_ensure_default_user_cannot_create_apps" ] }, { - "Id": "CCC.Core.CN09.AR02", - "Description": "When the service is operational, disabling the logs for the service or its child resources MUST NOT be possible without also disabling the corresponding resource.", + "Id": "CCC.IAM.CN02.AR01", + "Description": "When an identity policy for a non-administrative principal is evaluated, it MUST NOT grant permissions for creating, updating, or attaching policies.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN02 Restrict IAM Policies Modification", "SubSection": "", - "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "SubSectionObjective": "Ensure that only designated administrative accounts have the ability to create, modify, or attach policies that define permissions for other identities.", "Applicability": [ "tlp-clear", "tlp-green", "tlp-amber", "tlp-red" ], - "Recommendation": "No normal business operations should disable logs, as this could indicate an attempt to cover up unauthorized access. Ensure that logging mechanisms are tightly integrated with service operations, so that logging cannot be disabled without stopping the service itself. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07", - "CCC.TH09", - "CCC.TH04" + "CCC.IAM.TH06" ] } ], @@ -4392,57 +5505,49 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-6" + "PR.AA-05" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AU-9" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-02", - "LOG-04", - "LOG-09" + "AC-2", + "AC-3", + "AC-5", + "AC-6" ] } ] } ], "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "monitor_storage_account_with_activity_logs_is_private", - "keyvault_logging_enabled", - "app_http_logs_enabled" + "iam_role_user_access_admin_restricted", + "iam_subscription_roles_owner_custom_not_created", + "iam_custom_role_has_permissions_to_administer_resource_locks" ] }, { - "Id": "CCC.Core.CN09.AR03", - "Description": "When the service is operational, any attempt to redirect logs for the service or its child resources MUST NOT be possible without halting operation of the corresponding resource and publishing corresponding events to monitored channels.", + "Id": "CCC.IAM.CN02.AR02", + "Description": "When a non-administrative principal attempts to create, update, or attach policies, the service MUST deny the action.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN02 Restrict IAM Policies Modification", "SubSection": "", - "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "SubSectionObjective": "Ensure that only designated administrative accounts have the ability to create, modify, or attach policies that define permissions for other identities.", "Applicability": [ + "tlp-clear", + "tlp-green", "tlp-amber", "tlp-red" ], - "Recommendation": "No normal business operations should result in the redirection of logs, as this could indicate an attempt to cover up unauthorized access. Ensure that logging configurations are immutable during service operation so that any changes require stopping the service and publishing corresponding events to monitored channels. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07", - "CCC.TH09", - "CCC.TH04" + "CCC.IAM.TH06" ] } ], @@ -4450,46 +5555,37 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-6" + "PR.AA-05" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AU-9" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-02", - "LOG-04", - "LOG-09" + "AC-2", + "AC-3", + "AC-5", + "AC-6" ] } ] } ], "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "network_flow_log_captured_sent", - "app_http_logs_enabled", - "keyvault_logging_enabled" + "iam_role_user_access_admin_restricted", + "iam_subscription_roles_owner_custom_not_created", + "iam_custom_role_has_permissions_to_administer_resource_locks" ] }, { - "Id": "CCC.Core.CN10.AR01", - "Description": "When data is replicated, the service MUST ensure that replication only occurs to destinations that are explicitly included within the defined trust perimeter.", + "Id": "CCC.IAM.CN03.AR01", + "Description": "When a policy is created or updated that grants a principal permission to assume a role or impersonate a service identity, the principal MUST NOT contain a wildcard or be public/anonymous.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN10 Restrict Data Replication to Trust Perimeter", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN03 Restrict Role Assumption / Delegation", "SubSection": "", - "SubSectionObjective": "Ensure that data is only replicated on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "SubSectionObjective": "Limit which principals can assume a role or impersonate a service identity to only those required. This prevents unintended cross-account or public access by securing the \"who can act as this identity\" boundary.", "Applicability": [ "tlp-green", "tlp-amber", @@ -4500,7 +5596,1438 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH04" + "CCC.IAM.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_role_user_access_admin_restricted" + ] + }, + { + "Id": "CCC.IAM.CN03.AR02", + "Description": "When an external or unauthenticated principal tries to assume a role or impersonate a service identity, the service MUST deny the action.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN03 Restrict Role Assumption / Delegation", + "SubSection": "", + "SubSectionObjective": "Limit which principals can assume a role or impersonate a service identity to only those required. This prevents unintended cross-account or public access by securing the \"who can act as this identity\" boundary.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_role_user_access_admin_restricted" + ] + }, + { + "Id": "CCC.IAM.CN04.AR01", + "Description": "When an IAM policy is created or updated, it MUST NOT contain allow statements with wildcard permissions, unless the statement is restricted by a condition.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN04 Restrict Wildcard Usage in IAM Policies", + "SubSection": "", + "SubSectionObjective": "Limit the use of wildcard permissions in IAM policies to prevent overly broad access from being granted by default.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01", + "CCC.IAM.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-6" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.IAM.CN05.AR01", + "Description": "When a new cloud account is provisioned, a password policy MUST be configured for IAM users following the minimum PCI DSS v4.0.1 configurations.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN05 Strong Password Policies for IAM Users", + "SubSection": "", + "SubSectionObjective": "Ensure that the password policies for IAM users have strong configurations.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "When a new cloud account is provisioned, a password policy must be configured for all IAM users to align with the minimum requirements defined in PCI DSS v4.0.1. This includes, at a minimum: strength: 0 # Not yet specified - reference-id: A password length of at least 12 characters. strength: 0 # Not yet specified - reference-id: A mix of upper- and lower-case letters, numbers, and special characters. strength: 0 # Not yet specified - reference-id: Prevention of the use of previously used passwords (password history). strength: 0 # Not yet specified - reference-id: Password expiration at a defined interval (e.g., every 90 days). strength: 0 # Not yet specified - reference-id: Account lockout after a defined number of failed login attempts.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "IA-5" + ] + }, + { + "ReferenceId": "PCI-DSS", + "Identifiers": [ + "8.3.9", + "8.6.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.IAM.CN06.AR01", + "Description": "When a static credential such as an access key has existed for 90 days or more, it MUST be rotated.", + "Attributes": [ + { + "FamilyName": "Identity Provisioning and Lifecycle", + "FamilyDescription": "Controls related to the provisioning and lifecycle of IAM identities.", + "Section": "CCC.IAM.CN06 Maximum Age for Long-Term Static Credentials", + "SubSection": "", + "SubSectionObjective": "Ensure that long-lived static credentials like access keys are programmatically rotated within a defined time period to limit the window of opportunity if compromised.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "When a static credential such as an access key has existed for 90 days or more, it must be automatically rotated to reduce the risk of compromise due to long-term exposure. Organizations should implement automated checks to identify aging credentials and enforce rotation policies. Additionally, access key usage should be regularly monitored, and credentials that are no longer in use should be deactivated or deleted promptly. Where possible, prefer temporary, short-lived credentials over long-lived static ones to further minimize risk.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH09", + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.IAM.CN07.AR01", + "Description": "When a user account is disabled or deleted in the organization's IdP, the corresponding cloud identity and its access policies MUST be disabled or deleted within 24 hours.", + "Attributes": [ + { + "FamilyName": "Identity Provisioning and Lifecycle", + "FamilyDescription": "Controls related to the provisioning and lifecycle of IAM identities.", + "Section": "CCC.IAM.CN07 Automate Identity De-provisioning", + "SubSection": "", + "SubSectionObjective": "Ensure that when an identity is terminated in the central Identity Provider (IdP), ts corresponding access to cloud resources is revoked automatically.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH10", + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.IAM.CN08.AR01", + "Description": "When an IAM user has credentials, such as passwords or access keys, that have not been used for 90 days or more, the unused credentials MUST be removed or deactivated.", + "Attributes": [ + { + "FamilyName": "Identity Provisioning and Lifecycle", + "FamilyDescription": "Controls related to the provisioning and lifecycle of IAM identities.", + "Section": "CCC.IAM.CN08 Maximum Age for Unused Credentials", + "SubSection": "", + "SubSectionObjective": "Ensure that unused IAM credentals are removed to reduce exposure in the event of potential compromise.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "IAM user credentials (such as passwords or access keys) that have not been used for 90 days or more must be automatically removed or deactivated.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH11", + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.IAM.CN09.AR01", + "Description": "When a human user accesses the cloud environment, they MUST authenticate through the organization's federated IdP via a standard protocol (e.g., SAML, OIDC).", + "Attributes": [ + { + "FamilyName": "Identity Provisioning and Lifecycle", + "FamilyDescription": "Controls related to the provisioning and lifecycle of IAM identities.", + "Section": "CCC.IAM.CN09 Enforce Federated Single Sign-On (SSO) for Human Users", + "SubSection": "", + "SubSectionObjective": "Ensure that all human users must authenticate through a central, federated Identity Provider (IdP) to access the cloud environment. This eliminates cloud-native user accounts with long-lived passwords, centralizes authentication controls, and simplifies lifecycle management.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01", + "CCC.IAM.TH09" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "IA-2" + ] + } + ] + } + ], + "Checks": [ + "entra_security_defaults_enabled" + ] + }, + { + "Id": "CCC.IAM.CN10.AR01", + "Description": "When suspicious API requests are detected, real time alerts MUST be generated to notify security personnel.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain IAM-related events.", + "Section": "CCC.IAM.CN10 Alert On Anomalous Behaviour", + "SubSection": "", + "SubSectionObjective": "Ensure that logs and associated alerts are generated when anomalous API requests are made by a single identity, such as API requests commonly associated with privilege escalation tactics, originating from an external or malicious IP address or performed by a previously dormant identity, which may indicate that credentals may be compromised, as well as for password brute-force attempts and account lockouts.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4", + "SI-5", + "AC-2" + ] + } + ] + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists" + ] + }, + { + "Id": "CCC.IAM.CN10.AR02", + "Description": "When suspicious API requests are detected, the associated events MUST be logged, including the source details, time, and nature of the activity.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain IAM-related events.", + "Section": "CCC.IAM.CN10 Alert On Anomalous Behaviour", + "SubSection": "", + "SubSectionObjective": "Ensure that logs and associated alerts are generated when anomalous API requests are made by a single identity, such as API requests commonly associated with privilege escalation tactics, originating from an external or malicious IP address or performed by a previously dormant identity, which may indicate that credentals may be compromised, as well as for password brute-force attempts and account lockouts.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4", + "SI-5", + "AC-2" + ] + } + ] + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists", + "monitor_diagnostic_setting_with_appropriate_categories" + ] + }, + { + "Id": "CCC.IAM.CN11.AR01", + "Description": "When a cloud account or organization is provisioned, the native automated access and usage analysis services MUST be enabled to continuously monitor for external or public access to resources, and unused access.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain IAM-related events.", + "Section": "CCC.IAM.CN11 Enable Continuous IAM Access and Usage Analysis", + "SubSection": "", + "SubSectionObjective": "Enable and configure the cloud provider's native access and usage analysis services to continuously monitor for external access paths and internal unused access.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH02", + "CCC.IAM.TH10", + "CCC.IAM.TH11" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "ID.RA-01", + "ID.IM-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "CA-7", + "RA-5" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN01.AR01", + "Description": "Untrusted input such as user queries, RAG data or tool output MUST be validated before it is passed to a GenAI model.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN01 Model Input Filtering and Sanitisation", + "SubSection": "", + "SubSectionObjective": "Inspect and validate input before it is passed to a GenAI model in order to filter or sanitise adversarial queries and prevent sensitive data leakage.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-003", + "AIR-PREV-017", + "AIR-PREV-002", + "AIR-DET-001" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Input Validation and Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0020", + "AML.M0021", + "AML.M0015" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN01.AR02", + "Description": "If malicious patterns such as prompt injection or sensitive data are detected during input validation, the input MUST be blocked or sanitised.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN01 Model Input Filtering and Sanitisation", + "SubSection": "", + "SubSectionObjective": "Inspect and validate input before it is passed to a GenAI model in order to filter or sanitise adversarial queries and prevent sensitive data leakage.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-003", + "AIR-PREV-017", + "AIR-PREV-002", + "AIR-DET-001" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Input Validation and Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0020", + "AML.M0021", + "AML.M0015" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN02.AR01", + "Description": "GenAI model output MUST be validated for format conformance, malicious patterns, sensitive data and inapropriate content before being passed to users, application or plugins.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN02 Model Output Filtering and Sanitisation", + "SubSection": "", + "SubSectionObjective": "Inspect and validate GenAI model output before passing it to users, applications or plugins in order to filter or sanitise insecure or unreliable output and prevent sensitive data leakage.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH03", + "CCC.GenAI.TH04", + "CCC.GenAI.TH05", + "CCC.GenAI.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-003", + "AIR-PREV-017", + "AIR-PREV-002", + "AIR-DET-001" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Output Validation and Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0020", + "AML.M0002" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN02.AR02", + "Description": "In the event of policy violations, the AI-generated content MUST be redacted, encoded or rejected.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN02 Model Output Filtering and Sanitisation", + "SubSection": "", + "SubSectionObjective": "Inspect and validate GenAI model output before passing it to users, applications or plugins in order to filter or sanitise insecure or unreliable output and prevent sensitive data leakage.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH03", + "CCC.GenAI.TH04", + "CCC.GenAI.TH05", + "CCC.GenAI.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-003", + "AIR-PREV-017", + "AIR-PREV-002", + "AIR-DET-001" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Output Validation and Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0020", + "AML.M0002" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN03.AR01", + "Description": "When data is designated for model training or RAG ingestion, then its source MUST be explicitly approved and its provenance documented.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN03 Data Provenance and Source Vetting", + "SubSection": "", + "SubSectionObjective": "Ensure that all data for training, fine-tuning or RAG comes from trusted, approved sources and is authorised for the intended purposes in order to prevent the initial introduction of malicious content or leaked sensitive data.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH02", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-006" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Training Data Management" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0025" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN03.AR02", + "Description": "Data from unvetted sources MUST NOT be used in production systems.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN03 Data Provenance and Source Vetting", + "SubSection": "", + "SubSectionObjective": "Ensure that all data for training, fine-tuning or RAG comes from trusted, approved sources and is authorised for the intended purposes in order to prevent the initial introduction of malicious content or leaked sensitive data.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH02", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-006" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Training Data Management" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0025" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN04.AR01", + "Description": "When data is ingested for training, fine-tuning or conversion to vector embeddings, it MUST be validated for sensitive information or malicious content.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN04 Sanitisation of Ingested Data", + "SubSection": "", + "SubSectionObjective": "Validate and sanitise all data ingested by GenAI systems from extenal sources or internal knowledge bases, whether for training, conversion to vector embeddings, or real-time retireval, in order to remove or redact poisoned or sensitive data before further processing.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH02", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-002" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Training Data Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0007" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN04.AR02", + "Description": "If sensitive data or malicious content is detected, it must be rejected, redacted or flagged for manual review.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN04 Sanitisation of Ingested Data", + "SubSection": "", + "SubSectionObjective": "Validate and sanitise all data ingested by GenAI systems from extenal sources or internal knowledge bases, whether for training, conversion to vector embeddings, or real-time retireval, in order to remove or redact poisoned or sensitive data before further processing.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH02", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-002" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Training Data Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0007" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN05.AR01", + "Description": "When a RAG-enabled system generates a response containing information retrieved from its knowledge base, then the response MUST include a verifiable citation that links back to the specific source document.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN05 Citations and Source Traceability", + "SubSection": "", + "SubSectionObjective": "Require the GenAI system to provide citations or direct links back to the source documents used to generate a response, in to enhance the transparency, trustworthiness, and verifiability of AI-generated content.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH09", + "CCC.GenAI.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-DET-013" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN06.AR01", + "Description": "When an LLM invokes an external tool (e.g., an API, a plugin), then the tool MUST operate with the least privileges required for performing its intended functionality.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.GenAI.CN06 Least Privilege for Plugins", + "SubSection": "", + "SubSectionObjective": "Restricts the permissions of any external tools the GenAI system can call to limit the potential damage if an agent is coerced to perform unintended actions or vulnerabilities in the tools are exploited.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH07", + "CCC.GenAI.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Agent Permissions" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN07.AR01", + "Description": "When an application makes an API call to a foundational model in a production environment, then it MUST specify an explicit version identifier.", + "Attributes": [ + { + "FamilyName": "Configuration Management", + "FamilyDescription": "The Configuration Management control family involves establishing, maintaining and monitoring the configuration of the service and related applications and infrastructure to ensure consistency, secure defaults and compliance.", + "Section": "CCC.GenAI.CN07 Model Version Pinning", + "SubSection": "", + "SubSectionObjective": "Mandate that applications are locked (\"pinned\") to a specific, tested version of a foundational model to prevent unexpected behaviour changes introduced by provider-side updates.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH10" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-010" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN08.AR01", + "Description": "When a new AI model is considered for production deployment, it MUST undergo a formal red teaming and quality assurance review.", + "Attributes": [ + { + "FamilyName": "Model Assurance and Evaluation", + "FamilyDescription": "The Model Assurance and Evaluation control family encompasses the proactiveand continuous processes of testing and validating the AI model's behavior to ensure it aligns with safety, ethical, and quality standards.", + "Section": "CCC.GenAI.CN08 Quality Control and Red Teaming", + "SubSection": "", + "SubSectionObjective": "Establish a formal program for quality evaluation and adversarial testing (red teaming) to ensure GenAI system meet all business, quality, security and compliance requirements before getting deployed into production environments.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH02", + "CCC.GenAI.TH04", + "CCC.GenAI.TH08", + "CCC.GenAI.TH10" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-005" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Adversarial Training and Testing", + "Red Teaming", + "Product Governance" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0008" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN08.AR02", + "Description": "If model quality review or red teaming identifies an issue that exceeds the organization's risk tolerance, the model MUST NOT be deployed until the issue is remediated.", + "Attributes": [ + { + "FamilyName": "Model Assurance and Evaluation", + "FamilyDescription": "The Model Assurance and Evaluation control family encompasses the proactiveand continuous processes of testing and validating the AI model's behavior to ensure it aligns with safety, ethical, and quality standards.", + "Section": "CCC.GenAI.CN08 Quality Control and Red Teaming", + "SubSection": "", + "SubSectionObjective": "Establish a formal program for quality evaluation and adversarial testing (red teaming) to ensure GenAI system meet all business, quality, security and compliance requirements before getting deployed into production environments.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH02", + "CCC.GenAI.TH04", + "CCC.GenAI.TH08", + "CCC.GenAI.TH10" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-005" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Adversarial Training and Testing", + "Red Teaming", + "Product Governance" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0008" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN01.AR01", + "Description": "Verify that only authorized users can access MLDE resources, and that access modes are properly defined and enforced.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN01 Define Access Mode for ML Development Environments", + "SubSection": "", + "SubSectionObjective": "Ensure that access to Machine Learning Development Environment (MLDE) resources is strictly defined and controlled. Only authorized users with appropriate permissions can access these environments, mitigating the risk of unauthorized access, data leakage, or service disruption.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01", + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.1.1", + "2013 A.9.2.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-02" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN03.AR01", + "Description": "Verify that root access is disabled on MLDE instances containing sensitive data.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN03 Disable Root Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent users from obtaining root access on MLDE instances to reduce the risk of unauthorized system modifications and potential security breaches.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08", + "IAM-12" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.2.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN03.AR02", + "Description": "For MLDE instances without sensitive data, ensure that root access is only enabled when necessary and properly authorized.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN03 Disable Root Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent users from obtaining root access on MLDE instances to reduce the risk of unauthorized system modifications and potential security breaches.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08", + "IAM-12" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.2.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN04.AR01", + "Description": "Verify that terminal access is disabled on MLDE instances containing sensitive data.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN04 Disable Terminal Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent users from accessing the terminal on MLDE instances to limit the risk of unauthorized commands and potential system compromise.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.2.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN04.AR02", + "Description": "For MLDE instances without sensitive data, ensure that terminal access is only enabled when necessary and properly authorized.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN04 Disable Terminal Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent users from accessing the terminal on MLDE instances to limit the risk of unauthorized commands and potential system compromise.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.2.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN02.AR01", + "Description": "Confirm that file download functionality is disabled on MLDE instances containing sensitive data.", + "Attributes": [ + { + "FamilyName": "Data Protection", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN02 Disable File Downloads on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent unauthorized file downloads from MLDE instances to protect sensitive data from being exfiltrated.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH02", + "CCC.Core.TH02" ] } ], @@ -4514,168 +7041,98 @@ { "ReferenceId": "CCM", "Identifiers": [ - "DSP-10", - "DSP-19" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-4" - ] - } - ] - } - ], - "Checks": [ - "storage_cross_tenant_replication_disabled", - "cosmosdb_account_firewall_use_selected_networks", - "cosmosdb_account_use_private_endpoints", - "containerregistry_uses_private_link", - "keyvault_private_endpoints", - "storage_ensure_private_endpoints_in_storage_accounts" - ] - }, - { - "Id": "CCC.Core.CN02.AR01", - "Description": "When data is stored, it MUST be encrypted using the latest industry-standard encryption methods.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN02 Encrypt Data for Storage", - "SubSection": "", - "SubSectionObjective": "Ensure that all data stored is encrypted at rest using strong encryption algorithms.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "UEM-08", - "DSP-17" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-13", - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "storage_infrastructure_encryption_is_enabled", - "storage_ensure_encryption_with_customer_managed_keys", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "vm_ensure_attached_disks_encrypted_with_cmk", - "vm_ensure_unattached_disks_encrypted_with_cmk", - "sqlserver_tde_encrypted_with_cmk", - "sqlserver_tde_encryption_enabled", - "databricks_workspace_cmk_encryption_enabled" - ] - }, - { - "Id": "CCC.Core.CN11.AR01", - "Description": "When encryption keys are used, the service MUST verify that all encryption keys use the latest industry-standard cryptographic algorithms.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH16" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" + "DSI-05", + "DSI-07" ] }, { "ReferenceId": "ISO_27001", "Identifiers": [ - "2013 A.10.1.2" + "2013 A.13.2.1" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "SC-12", - "SC-17" + "SC-7", + "SC-8" ] } ] } ], - "Checks": [ - "storage_ensure_encryption_with_customer_managed_keys", - "databricks_workspace_cmk_encryption_enabled", - "keyvault_key_rotation_enabled", - "keyvault_key_expiration_set_in_non_rbac", - "keyvault_rbac_key_expiration_set", - "keyvault_rbac_secret_expiration_set", - "keyvault_non_rbac_secret_expiration_set", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "storage_smb_channel_encryption_with_secure_algorithm", - "storage_secure_transfer_required_is_enabled", - "storage_ensure_minimum_tls_version_12" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN11.AR02", - "Description": "When encryption keys are used, the service MUST rotate active keys within 180 days of issuance.", + "Id": "CCC.MLDE.CN02.AR02", + "Description": "For MLDE instances without sensitive data, ensure that file downloads are monitored and logged.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", + "FamilyName": "Data Protection", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN02 Disable File Downloads on MLDE Instances", "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "SubSectionObjective": "Prevent unauthorized file downloads from MLDE instances to protect sensitive data from being exfiltrated.", "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH02", + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-5" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSI-05", + "DSI-07" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.13.2.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7", + "SC-8" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN05.AR01", + "Description": "Verify that only approved VM and container images can be selected when creating MLDE instances.", + "Attributes": [ + { + "FamilyName": "Configuration Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN05 Restrict Environment Options on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Limit the virtual machine and container image options available when creating new MLDE instances to approved and secure configurations.", + "Applicability": [ + "tlp-red", "tlp-amber" ], "Recommendation": "", @@ -4683,7 +7140,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH16" + "CCC.MLDE.TH04" ] } ], @@ -4691,51 +7148,43 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" + "PR.IP-1" ] }, { "ReferenceId": "CCM", "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" + "TVM-02" ] }, { "ReferenceId": "ISO_27001", "Identifiers": [ - "2013 A.10.1.2" + "2013 A.12.5.1" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "SC-12", - "SC-17" + "CM-2" ] } ] } ], - "Checks": [ - "keyvault_key_rotation_enabled", - "storage_key_rotation_90_days", - "databricks_workspace_cmk_encryption_enabled" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN11.AR03", - "Description": "When encrypting data, the service MUST verify that customer-managed encryption keys (CMEKs) are used.", + "Id": "CCC.MLDE.CN05.AR02", + "Description": "Attempt to create an MLDE instance with an unapproved image and confirm that it is denied.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "", - "SubSection": "CCC.Core.CN11 Protect Encryption Keys", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "FamilyName": "Configuration Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN05 Restrict Environment Options on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Limit the virtual machine and container image options available when creating new MLDE instances to approved and secure configurations.", "Applicability": [ - "tlp-amber", "tlp-red" ], "Recommendation": "", @@ -4743,7 +7192,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH16" + "CCC.MLDE.TH04" ] } ], @@ -4751,52 +7200,371 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" + "PR.IP-1" ] }, { "ReferenceId": "CCM", "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" + "TVM-02" ] }, { "ReferenceId": "ISO_27001", "Identifiers": [ - "2013 A.10.1.2" + "2013 A.12.5.1" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "SC-12", - "SC-17" + "CM-2" ] } ] } ], - "Checks": [ - "databricks_workspace_cmk_encryption_enabled", - "storage_ensure_encryption_with_customer_managed_keys", - "vm_ensure_attached_disks_encrypted_with_cmk", - "vm_ensure_unattached_disks_encrypted_with_cmk", - "sqlserver_tde_encrypted_with_cmk", - "monitor_storage_account_with_activity_logs_cmk_encrypted" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN11.AR04", - "Description": "When encryption keys are accessed, the service MUST verify that access to encryption keys is restricted to authorized personnel and services, following the principle of least privilege.", + "Id": "CCC.MLDE.CN06.AR01", + "Description": "Verify that automatic scheduled upgrades are enabled on user-managed MLDE instances containing sensitive data.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", + "FamilyName": "Vulnerability Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN06 Require Automatic Scheduled Upgrades on User-Managed MLDE Instances", "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "SubSectionObjective": "Ensure that MLDE instances are kept up-to-date with the latest security patches by enforcing automatic scheduled upgrades.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH04", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IP-12" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "TVM-01", + "TVM-02" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.12.6.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN06.AR02", + "Description": "Ensure that the upgrade schedule is appropriately configured and does not interfere with critical operations.", + "Attributes": [ + { + "FamilyName": "Vulnerability Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN06 Require Automatic Scheduled Upgrades on User-Managed MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Ensure that MLDE instances are kept up-to-date with the latest security patches by enforcing automatic scheduled upgrades.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH04", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IP-12" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "TVM-01", + "TVM-02" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.12.6.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN07.AR01", + "Description": "Verify that MLDE instances containing sensitive data cannot be accessed via public IP addresses.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN07 Restrict Public IP Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent public IP access to MLDE instances to reduce exposure to the internet and enhance security.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH02", + "CCC.VPC.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "SEF-05" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.13.1.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN07.AR02", + "Description": "For MLDE instances without sensitive data requiring public access, ensure that appropriate security controls are in place and access is approved.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN07 Restrict Public IP Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent public IP access to MLDE instances to reduce exposure to the internet and enhance security.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH02", + "CCC.VPC.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "SEF-05" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.13.1.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN08.AR01", + "Description": "Verify that MLDE instances containing sensitive data can only be deployed in approved virtual networks with appropriate security controls.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN08 Restrict Virtual Networks for MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Limit the virtual networks that can be used when creating new MLDE instances to ensure they are deployed within approved and secure network environments.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01", + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-12" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.1.2" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN08.AR02", + "Description": "Ensure that MLDE instances without sensitive data are deployed in networks that meet organizational security standards.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN08 Restrict Virtual Networks for MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Limit the virtual networks that can be used when creating new MLDE instances to ensure they are deployed within approved and secure network environments.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01", + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-12" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.1.2" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Message.CN01.AR01", + "Description": "Attempt to publish a message without using a customer-managed encryption key and verify that the message is rejected or not stored.", + "Attributes": [ + { + "FamilyName": "Encryption", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.Message.CN01 Use Customer-Managed Encryption Keys (CMEK) for Messages", + "SubSection": "", + "SubSectionObjective": "Ensure that messages are encrypted using customer-managed encryption keys (CMEK) to provide enhanced control over encryption processes and keys, meeting compliance and security requirements.", "Applicability": [ "tlp-clear", "tlp-green", @@ -4808,7 +7576,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH16" + "CCC.Core.TH01" ] } ], @@ -4819,546 +7587,82 @@ "PR.DS-1" ] }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.2" - ] - }, { "ReferenceId": "NIST_800_53", "Identifiers": [ "SC-12", - "SC-17" + "SC-13" ] } ] } ], - "Checks": [ - "databricks_workspace_cmk_encryption_enabled", - "keyvault_rbac_enabled", - "keyvault_key_rotation_enabled", - "keyvault_key_expiration_set_in_non_rbac", - "keyvault_rbac_key_expiration_set", - "keyvault_access_only_through_private_endpoints", - "keyvault_private_endpoints", - "keyvault_logging_enabled", - "keyvault_recoverable", - "storage_ensure_encryption_with_customer_managed_keys" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN11.AR05", - "Description": "When encryption keys are used, the service MUST rotate active keys within 365 days of issuance.", + "Id": "CCC.SvlsComp.CN01.AR01", + "Description": "Attempt to access the serverless function over the public internet and verify that access is denied.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.SvlsComp.CN01 Enforce Use of Private Endpoints for Serverless Function", "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "SubSectionObjective": "Ensure that the serverless function is accessible only through a private endpoint, allowing it to communicate securely within a virtual private network and preventing unauthorized external access.", "Applicability": [ - "tlp-clear", - "tlp-green" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH16" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-12", - "SC-17" - ] - } - ] - } - ], - "Checks": [ - "keyvault_key_rotation_enabled", - "storage_ensure_encryption_with_customer_managed_keys", - "databricks_workspace_cmk_encryption_enabled", - "storage_key_rotation_90_days", - "keyvault_rbac_key_expiration_set", - "keyvault_rbac_secret_expiration_set", - "keyvault_key_expiration_set_in_non_rbac" - ] - }, - { - "Id": "CCC.Core.CN11.AR06", - "Description": "When encryption keys are used, the service MUST rotate active keys within 90 days of issuance.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH16" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-12", - "SC-17" - ] - } - ] - } - ], - "Checks": [ - "keyvault_key_rotation_enabled" - ] - }, - { - "Id": "CCC.Core.CN14.AR01", - "Description": "When backups are created for disaster recovery purposes, the storage mechanism MUST NOT allow modification or deletion within 30 days of creation.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN14 Maintain Recent Backups", - "SubSection": "", - "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Use immutable storage solutions where possible. Implement backup retention policies that enforce a minimum retention period of 30 days. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "vm_backup_enabled", - "vm_sufficient_daily_backup_retention_period" - ] - }, - { - "Id": "CCC.Core.CN14.AR02", - "Description": "When backups are created for disaster recovery purposes, the most recent backup MUST have a creation date within the past 30 days.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN14 Maintain Recent Backups", - "SubSection": "", - "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", - "Applicability": [ - "tlp-clear", - "tlp-green", + "tlp-red", "tlp-amber" ], - "Recommendation": "Implement automated backup processes to ensure that backups are created regularly. Monitor backup schedules and verify that the most recent backup creation date is within the last 30 days. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06" + "CCC.Core.TH01" ] } ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "vm_backup_enabled", - "vm_sufficient_daily_backup_retention_period" - ] - }, - { - "Id": "CCC.Core.CN14.AR02", - "Description": "When backups are created for disaster recovery purposes, the most recent backup MUST have a creation date within the past 14 days.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN14 Maintain Recent Backups", - "SubSection": "", - "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "Implement automated backup processes to ensure that backups are created regularly. Monitor backup schedules and verify that the most recent backup creation date is within the last 14 days. ", - "SectionThreatMappings": [ + "SectionGuidelineMappings": [ { - "ReferenceId": "CCC", + "ReferenceId": "NIST-CSF", "Identifiers": [ - "CCC.TH06" + "PR.AC-5" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7", + "SC-8" ] } - ], - "SectionGuidelineMappings": [] + ] } ], "Checks": [ - "vm_backup_enabled", - "vm_sufficient_daily_backup_retention_period" + "app_function_not_publicly_accessible" ] }, { - "Id": "CCC.Core.CN03.AR01", - "Description": "When an entity attempts to modify the service through a user interface, the authentication process MUST require multiple identifying factors for authentication.", + "Id": "CCC.SvlsComp.CN02.AR01", + "Description": "Send requests to invoke the function up to the allowed threshold and confirm they are successful; then send additional requests exceeding the threshold from the same entity and verify that they are denied.", "Attributes": [ { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "FamilyName": "Availability", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.SvlsComp.CN02 Implement Function Invocation Rate Limits", "SubSection": "", - "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "SubSectionObjective": "Ensure that function invocation is limited to a specified threshold from any single entity, preventing resource exhaustion and denial of service attacks.", "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-red", + "tlp-amber" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-14" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-03", - "IAM-08" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "IA-2" - ] - } - ] - } - ], - "Checks": [ - "entra_non_privileged_user_has_mfa", - "entra_privileged_user_has_mfa", - "entra_conditional_access_policy_require_mfa_for_management_api", - "entra_security_defaults_enabled", - "entra_user_with_vm_access_has_mfa" - ] - }, - { - "Id": "CCC.Core.CN03.AR02", - "Description": "When an entity attempts to modify the service through an API endpoint, the authentication process MUST require a credential such as an API key or token AND originate from within the trust perimeter.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "", - "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-14" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-03", - "IAM-08" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "IA-2" - ] - } - ] - } - ], - "Checks": [ - "entra_conditional_access_policy_require_mfa_for_management_api", - "entra_non_privileged_user_has_mfa", - "entra_privileged_user_has_mfa", - "entra_security_defaults_enabled" - ] - }, - { - "Id": "CCC.Core.CN03.AR03", - "Description": "When an entity attempts to view information on the service through a user interface, the authentication process MUST require multiple identifying factors from the user.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "", - "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-14" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-03", - "IAM-08" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "IA-2" - ] - } - ] - } - ], - "Checks": [ - "entra_conditional_access_policy_require_mfa_for_management_api", - "entra_non_privileged_user_has_mfa", - "entra_privileged_user_has_mfa", - "entra_security_defaults_enabled" - ] - }, - { - "Id": "CCC.Core.CN03.AR04", - "Description": "When an entity attempts to view information on the service through an API endpoint, the authentication process MUST require a credential such as an API key or token AND originate from within the trust perimeter.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "", - "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-14" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-03", - "IAM-08" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "IA-2" - ] - } - ] - } - ], - "Checks": [ - "entra_conditional_access_policy_require_mfa_for_management_api", - "entra_non_privileged_user_has_mfa", - "entra_privileged_user_has_mfa", - "entra_user_with_vm_access_has_mfa" - ] - }, - { - "Id": "CCC.Core.CN05.AR01", - "Description": "When an attempt is made to modify data on the service or a child resource, the service MUST block requests from unauthorized entities.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH12" ] } ], @@ -5366,782 +7670,19 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" + "PR.DS-4" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AC-3" + "SC-5" ] } ] } ], - "Checks": [ - "aks_cluster_rbac_enabled", - "aks_network_policy_enabled", - "aks_clusters_public_access_disabled", - "app_client_certificates_on", - "app_ensure_auth_is_set_up", - "app_register_with_identity", - "app_function_identity_is_configured", - "app_function_identity_without_admin_privileges", - "app_function_not_publicly_accessible", - "app_function_access_keys_configured", - "iam_custom_role_has_permissions_to_administer_resource_locks", - "iam_subscription_roles_owner_custom_not_created", - "iam_role_user_access_admin_restricted", - "keyvault_rbac_enabled", - "keyvault_private_endpoints", - "keyvault_access_only_through_private_endpoints", - "entra_conditional_access_policy_require_mfa_for_management_api", - "entra_global_admin_in_less_than_five_users", - "entra_non_privileged_user_has_mfa", - "entra_security_defaults_enabled", - "entra_trusted_named_locations_exists", - "entra_user_with_vm_access_has_mfa", - "vm_jit_access_enabled", - "vm_linux_enforce_ssh_authentication" - ] - }, - { - "Id": "CCC.Core.CN05.AR02", - "Description": "When administrative access or configuration change is attempted on the service or a child resource, the service MUST refuse requests from unauthorized entities.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "aks_cluster_rbac_enabled", - "app_register_with_identity", - "app_function_identity_is_configured", - "app_function_identity_without_admin_privileges", - "iam_custom_role_has_permissions_to_administer_resource_locks", - "iam_subscription_roles_owner_custom_not_created", - "iam_role_user_access_admin_restricted", - "containerregistry_not_publicly_accessible", - "containerregistry_uses_private_link", - "containerregistry_admin_user_disabled", - "cosmosdb_account_use_private_endpoints", - "cosmosdb_account_firewall_use_selected_networks", - "cosmosdb_account_use_aad_and_rbac", - "keyvault_rbac_enabled", - "keyvault_private_endpoints", - "keyvault_access_only_through_private_endpoints", - "storage_account_key_access_disabled", - "storage_ensure_private_endpoints_in_storage_accounts", - "storage_ensure_azure_services_are_trusted_to_access_is_enabled", - "storage_default_to_entra_authorization_enabled", - "network_http_internet_access_restricted", - "network_rdp_internet_access_restricted", - "network_ssh_internet_access_restricted", - "network_udp_internet_access_restricted", - "aks_clusters_public_access_disabled", - "app_function_not_publicly_accessible", - "entra_global_admin_in_less_than_five_users", - "entra_conditional_access_policy_require_mfa_for_management_api", - "entra_non_privileged_user_has_mfa", - "entra_privileged_user_has_mfa", - "entra_trusted_named_locations_exists", - "entra_user_with_vm_access_has_mfa" - ] - }, - { - "Id": "CCC.Core.CN05.AR03", - "Description": "When administrative access or configuration change is attempted on the service or a child resource in a multi-tenant environment, the service MUST refuse requests across tenant boundaries unless the origin is explicitly included in a pre-approved allowlist.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "entra_conditional_access_policy_require_mfa_for_management_api", - "entra_global_admin_in_less_than_five_users", - "entra_non_privileged_user_has_mfa", - "iam_role_user_access_admin_restricted", - "entra_trusted_named_locations_exists", - "keyvault_private_endpoints", - "keyvault_access_only_through_private_endpoints", - "keyvault_rbac_enabled", - "cosmosdb_account_firewall_use_selected_networks", - "cosmosdb_account_use_private_endpoints", - "containerregistry_not_publicly_accessible", - "containerregistry_uses_private_link", - "containerregistry_admin_user_disabled", - "network_http_internet_access_restricted", - "network_rdp_internet_access_restricted", - "network_ssh_internet_access_restricted", - "network_udp_internet_access_restricted", - "app_function_not_publicly_accessible", - "storage_default_network_access_rule_is_denied", - "storage_ensure_private_endpoints_in_storage_accounts", - "storage_blob_public_access_level_is_disabled", - "storage_cross_tenant_replication_disabled" - ] - }, - { - "Id": "CCC.Core.CN05.AR04", - "Description": "When data is requested from outside the trust perimeter, the service MUST refuse requests from unauthorized entities.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "", - "SubSection": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "aks_cluster_rbac_enabled", - "aks_network_policy_enabled", - "aks_clusters_public_access_disabled", - "app_register_with_identity", - "app_function_access_keys_configured", - "app_function_identity_is_configured", - "app_function_identity_without_admin_privileges", - "app_ensure_auth_is_set_up", - "app_function_not_publicly_accessible", - "containerregistry_not_publicly_accessible", - "containerregistry_uses_private_link", - "containerregistry_admin_user_disabled", - "keyvault_private_endpoints", - "keyvault_access_only_through_private_endpoints", - "keyvault_rbac_enabled", - "storage_account_key_access_disabled", - "storage_default_to_entra_authorization_enabled", - "storage_ensure_azure_services_are_trusted_to_access_is_enabled", - "storage_default_network_access_rule_is_denied", - "storage_secure_transfer_required_is_enabled", - "iam_role_user_access_admin_restricted", - "iam_subscription_roles_owner_custom_not_created", - "cosmosdb_account_use_aad_and_rbac", - "sqlserver_azuread_administrator_enabled", - "sqlserver_unrestricted_inbound_access", - "network_http_internet_access_restricted", - "network_rdp_internet_access_restricted", - "network_ssh_internet_access_restricted", - "network_udp_internet_access_restricted", - "vm_jit_access_enabled", - "entra_conditional_access_policy_require_mfa_for_management_api", - "entra_global_admin_in_less_than_five_users", - "entra_non_privileged_user_has_mfa", - "entra_privileged_user_has_mfa", - "entra_trusted_named_locations_exists", - "entra_user_with_vm_access_has_mfa" - ] - }, - { - "Id": "CCC.Core.CN05.AR05", - "Description": "When any request is made from outside the trust perimeter, the service MUST NOT provide any response that may indicate the service exists.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "aks_clusters_created_with_private_nodes", - "aks_clusters_public_access_disabled", - "aks_network_policy_enabled", - "app_function_not_publicly_accessible", - "app_register_with_identity", - "app_function_identity_is_configured", - "app_function_identity_without_admin_privileges", - "app_client_certificates_on", - "app_ensure_auth_is_set_up", - "containerregistry_not_publicly_accessible", - "containerregistry_uses_private_link", - "containerregistry_admin_user_disabled", - "keyvault_private_endpoints", - "keyvault_access_only_through_private_endpoints", - "keyvault_rbac_enabled", - "keyvault_logging_enabled", - "storage_account_key_access_disabled", - "storage_default_to_entra_authorization_enabled", - "storage_ensure_private_endpoints_in_storage_accounts", - "storage_ensure_azure_services_are_trusted_to_access_is_enabled", - "iam_custom_role_has_permissions_to_administer_resource_locks", - "iam_role_user_access_admin_restricted", - "iam_subscription_roles_owner_custom_not_created", - "vm_jit_access_enabled", - "vm_linux_enforce_ssh_authentication", - "network_http_internet_access_restricted", - "network_rdp_internet_access_restricted", - "network_ssh_internet_access_restricted", - "network_udp_internet_access_restricted" - ] - }, - { - "Id": "CCC.Core.CN05.AR06", - "Description": "When any request is made to the service or a child resource, the service MUST refuse requests from unauthorized entities.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "aks_cluster_rbac_enabled", - "app_register_with_identity", - "app_function_identity_is_configured", - "app_function_identity_without_admin_privileges", - "app_function_access_keys_configured", - "app_function_not_publicly_accessible", - "iam_custom_role_has_permissions_to_administer_resource_locks", - "iam_role_user_access_admin_restricted", - "iam_subscription_roles_owner_custom_not_created", - "cosmosdb_account_use_aad_and_rbac", - "entra_global_admin_in_less_than_five_users", - "entra_non_privileged_user_has_mfa", - "entra_privileged_user_has_mfa", - "entra_conditional_access_policy_require_mfa_for_management_api", - "keyvault_rbac_enabled", - "vm_jit_access_enabled", - "vm_linux_enforce_ssh_authentication" - ] - }, - { - "Id": "CCC.Core.CN04.AR01", - "Description": "When administrative access or configuration change is attempted on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN04 Log All Access and Changes", - "SubSection": "", - "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-08" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3", - "AU-12" - ] - } - ] - } - ], - "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "keyvault_logging_enabled", - "network_flow_log_captured_sent", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "monitor_alert_create_update_sqlserver_fr", - "monitor_alert_delete_nsg", - "monitor_alert_delete_public_ip_address_rule", - "monitor_alert_delete_security_solution", - "monitor_alert_create_policy_assignment", - "monitor_alert_create_update_public_ip_address_rule", - "monitor_alert_create_update_security_solution", - "monitor_alert_service_health_exists" - ] - }, - { - "Id": "CCC.Core.CN04.AR02", - "Description": "When any attempt is made to modify data on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN04 Log All Access and Changes", - "SubSection": "", - "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-08" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3", - "AU-12" - ] - } - ] - } - ], - "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "keyvault_logging_enabled", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "network_flow_log_captured_sent" - ] - }, - { - "Id": "CCC.Core.CN04.AR03", - "Description": "When any attempt is made to read data on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN04 Log All Access and Changes", - "SubSection": "", - "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-08" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3", - "AU-12" - ] - } - ] - } - ], - "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "keyvault_logging_enabled", - "app_http_logs_enabled", - "network_flow_log_captured_sent", - "monitor_storage_account_with_activity_logs_is_private", - "monitor_storage_account_with_activity_logs_cmk_encrypted" - ] - }, - { - "Id": "CCC.Core.CN07.AR01", - "Description": "When enumeration activities are detected, the service MUST publish an event to a monitored channel which includes the client identity, time, and nature of the activity.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN07 Alert on Unusual Enumeration Activity", - "SubSection": "", - "SubSectionObjective": "Ensure that logs and associated alerts are generated when unusual enumeration activity is detected that may indicate reconnaissance activities.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Implement event publication mechanisms and alerts for patterns indicative of enumeration activities, such as repeated access attempts, requests, or liveness probes. Configure alerts to notify security teams of any activities that merit further investigation. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH15" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-05", - "SEF-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-6" - ] - } - ] - } - ], - "Checks": [ - "apim_threat_detection_llm_jacking" - ] - }, - { - "Id": "CCC.Core.CN07.AR02", - "Description": "When enumeration activities are detected, the service MUST log the client identity, time, and nature of the activity.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN07 Alert on Unusual Enumeration Activity", - "SubSection": "", - "SubSectionObjective": "Ensure that logs and associated alerts are generated when unusual enumeration activity is detected that may indicate reconnaissance activities.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Implement logging mechanisms to capture details of enumeration activities, including client identity, timestamps, and activity nature. Retain logs according to organizational policies, and occasionally review them for patterns that may indicate reconnaissance activities. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH15" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-05", - "SEF-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-6" - ] - } - ] - } - ], - "Checks": [ - "monitor_diagnostic_settings_exists", - "monitor_diagnostic_setting_with_appropriate_categories", - "monitor_alert_create_update_sqlserver_fr", - "monitor_alert_delete_nsg", - "monitor_alert_delete_public_ip_address_rule", - "monitor_alert_delete_security_solution", - "monitor_alert_create_policy_assignment", - "monitor_alert_create_update_nsg", - "monitor_alert_create_update_public_ip_address_rule", - "monitor_alert_create_update_security_solution", - "monitor_alert_service_health_exists", - "monitor_storage_account_with_activity_logs_cmk_encrypted", - "monitor_storage_account_with_activity_logs_is_private" - ] + "Checks": [] } ] } diff --git a/prowler/compliance/gcp/ccc_gcp.json b/prowler/compliance/gcp/ccc_gcp.json index e3060646c5..df6b15bc5c 100644 --- a/prowler/compliance/gcp/ccc_gcp.json +++ b/prowler/compliance/gcp/ccc_gcp.json @@ -1,10 +1,1630 @@ { "Framework": "CCC", - "Version": "", + "Version": "v2025.10", "Provider": "GCP", "Name": "Common Cloud Controls Catalog (CCC)", "Description": "Common Cloud Controls Catalog (CCC) for GCP", "Requirements": [ + { + "Id": "CCC.Core.CN01.AR01", + "Description": "When a port is exposed for non-SSH network traffic, all traffic MUST include a TLS handshake AND be encrypted using TLS 1.3 or higher.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Most cloud services enable TLS 1.3 by default. Where it is not already set, ensure that your services are configured or updated accordingly.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [ + "cloudsql_instance_ssl_connections" + ] + }, + { + "Id": "CCC.Core.CN01.AR02", + "Description": "When a port is exposed for SSH network traffic, all traffic MUST include a SSH handshake AND be encrypted using SSHv2 or higher.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Any time port 22 is exposed, ensure that it has a properly implemented SSH server with SSHv2 enabled and configured with strong ciphers.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [ + "compute_firewall_ssh_access_from_the_internet_allowed", + "compute_instance_block_project_wide_ssh_keys_disabled", + "compute_project_os_login_enabled", + "compute_project_os_login_2fa_enabled" + ] + }, + { + "Id": "CCC.Core.CN01.AR03", + "Description": "When the service receives unencrypted traffic, then it MUST either block the request or automatically redirect it to the secure equivalent.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Review firewall, load balancer, and application configurations to ensure insecure protocols such as HTTP, FTP, and Telnet are not exposed. Where possible, implement automatic redirection to secure protocols such as HTTPS, SFTP, SSH, and regularly scan for protocol drift.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [ + "cloudsql_instance_ssl_connections" + ] + }, + { + "Id": "CCC.Core.CN01.AR07", + "Description": "When a port is exposed, the service MUST ensure that the protocol and service officially assigned to that port number by the IANA Service Name and Transport Protocol Port Number Registry, and no other, is run on that port.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Reference the IANA Service Name and Transport Protocol Port Number Registry for more information about correct protocol-to-port assignments. Avoid running non-standard services on well-known ports.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN01.AR08", + "Description": "When a service transmits data using TLS, mutual TLS (mTLS) MUST be implemented to require both client and server certificate authentication for all connections.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "SubSection": "", + "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Configure mTLS for all endpoints that process or transmit sensitive data. Ensure both client and server certificates are validated and managed securely. Regularly review certificate authorities and automate certificate rotation where possible.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "IVS-03", + "IVS-07" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN13.AR01", + "Description": "When a port is exposed that uses certificate-based encryption, the service MUST only use valid, unexpired certificates issued by a trusted certificate authority.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH18" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN13.AR02", + "Description": "When a port is exposed that uses certificate-based encryption, the service MUST rotate active certificates within 180 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", + "Applicability": [ + "tlp-amber" + ], + "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH18" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN13.AR03", + "Description": "When a port is exposed that uses certificate-based encryption, the service MUST rotate active certificates within 90 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH18" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN06.AR01", + "Description": "When the service is running, its region and availability zone MUST be included in a list of explicitly trusted or approved locations within the trust perimeter.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN06 Restrict Deployments to Trust Perimeter", + "SubSection": "", + "SubSectionObjective": "Ensure that the service and its child resources are only deployed on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Maintain an up-to-date list of trusted and approved regions based on organizational policies. Validate the service's deployment location is included in this list.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-19" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN06.AR02", + "Description": "When a child resource is deployed, its region and availability zone MUST be included in a list of explicitly trusted or approved locations within the trust perimeter.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN06 Restrict Deployments to Trust Perimeter", + "SubSection": "", + "SubSectionObjective": "Ensure that the service and its child resources are only deployed on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Maintain an up-to-date list of trusted and approved regions based on organizational policies. Validate that child resources can only be deployed to locations included in this list.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-19" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN08.AR01", + "Description": "When data is created or modified, the data MUST have a complete and recoverable duplicate that is stored in a physically separate data center.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN08 Replicate Data to Multiple Locations", + "SubSection": "", + "SubSectionObjective": "Ensure that data is replicated across multiple physical locations to protect against data loss due to hardware failures, natural disasters, or other catastrophic events.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Implement automated data replication processes to ensure that data is consistently duplicated in another region or availability zone. Regularly test data recovery from the replicated location to ensure integrity and availability.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "BCR-08", + "BCR-10", + "BCR-11" + ] + } + ] + } + ], + "Checks": [ + "cloudsql_instance_automated_backups" + ] + }, + { + "Id": "CCC.Core.CN08.AR02", + "Description": "When data is replicated into a second location, the service MUST be able to accurately represent the replication locations, replication status, and data synchronization status.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN08 Replicate Data to Multiple Locations", + "SubSection": "", + "SubSectionObjective": "Ensure that data is replicated across multiple physical locations to protect against data loss due to hardware failures, natural disasters, or other catastrophic events.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "BCR-08", + "BCR-10", + "BCR-11" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN09.AR01", + "Description": "When the service is operational, its logs and any child resource logs MUST NOT be accessible from the resource they record access to.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH07", + "CCC.Core.TH09", + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-02", + "LOG-04", + "LOG-09" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_log_retention_policy_lock" + ] + }, + { + "Id": "CCC.Core.CN09.AR02", + "Description": "When the service is operational, disabling the logs for the service or its child resources MUST NOT be possible without also disabling the corresponding resource.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "No normal business operations should disable logs, as this could indicate an attempt to cover up unauthorized access. Ensure that logging mechanisms are tightly integrated with service operations, so that logging cannot be disabled without stopping the service itself.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH07", + "CCC.Core.TH09", + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-02", + "LOG-04", + "LOG-09" + ] + } + ] + } + ], + "Checks": [ + "iam_audit_logs_enabled", + "logging_sink_created", + "cloudstorage_bucket_log_retention_policy_lock" + ] + }, + { + "Id": "CCC.Core.CN09.AR03", + "Description": "When the service is operational, any attempt to redirect logs for the service or its child resources MUST NOT be possible without halting operation of the corresponding resource and publishing corresponding events to monitored channels.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "No normal business operations should result in the redirection of logs, as this could indicate an attempt to cover up unauthorized access. Ensure that logging configurations are immutable during service operation so that any changes require stopping the service and publishing corresponding events to monitored channels.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH07", + "CCC.Core.TH09", + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-02", + "LOG-04", + "LOG-09" + ] + } + ] + } + ], + "Checks": [ + "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled" + ] + }, + { + "Id": "CCC.Core.CN10.AR01", + "Description": "When data is replicated, the service MUST ensure that replication only occurs to destinations that are explicitly included within the defined trust perimeter.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN10 Restrict Data Replication to Trust Perimeter", + "SubSection": "", + "SubSectionObjective": "Ensure that data is only replicated on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-10", + "DSP-19" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN02.AR01", + "Description": "When data is stored, it MUST be encrypted using the latest industry-standard encryption methods.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN02 Encrypt Data for Storage", + "SubSection": "", + "SubSectionObjective": "Ensure that all data stored is encrypted at rest using strong encryption algorithms.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-03", + "CEK-04", + "UEM-08", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "compute_instance_encryption_with_csek_enabled", + "dataproc_encrypted_with_cmks_disabled", + "bigquery_dataset_cmk_encryption", + "bigquery_table_cmk_encryption" + ] + }, + { + "Id": "CCC.Core.CN11.AR01", + "Description": "When encryption keys are used, the service MUST verify that all encryption keys use the latest industry-standard cryptographic algorithms.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN11.AR02", + "Description": "When encryption keys are used, the service MUST rotate active keys within 180 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "kms_key_rotation_enabled" + ] + }, + { + "Id": "CCC.Core.CN11.AR03", + "Description": "When encrypting data, the service MUST verify that customer-managed encryption keys (CMEKs) are used.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "bigquery_dataset_cmk_encryption", + "bigquery_table_cmk_encryption", + "dataproc_encrypted_with_cmks_disabled", + "compute_instance_encryption_with_csek_enabled" + ] + }, + { + "Id": "CCC.Core.CN11.AR04", + "Description": "When encryption keys are accessed, the service MUST verify that access to encryption keys is restricted to authorized personnel and services, following the principle of least privilege.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "kms_key_not_publicly_accessible", + "iam_role_kms_enforce_separation_of_duties" + ] + }, + { + "Id": "CCC.Core.CN11.AR05", + "Description": "When encryption keys are used, the service MUST rotate active keys within 365 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-clear", + "tlp-green" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "kms_key_rotation_enabled" + ] + }, + { + "Id": "CCC.Core.CN11.AR06", + "Description": "When encryption keys are used, the service MUST rotate active keys within 90 days of issuance.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN11 Protect Encryption Keys", + "SubSection": "", + "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "CEK-08", + "CEK-10", + "CEK-12" + ] + } + ] + } + ], + "Checks": [ + "kms_key_rotation_enabled" + ] + }, + { + "Id": "CCC.Core.CN14.AR01", + "Description": "When backups are created for disaster recovery purposes, the storage mechanism MUST NOT allow modification or deletion within 30 days of creation.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN14 Maintain Recent Backups", + "SubSection": "", + "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Use immutable storage solutions where possible. Implement backup retention policies that enforce a minimum retention period of 30 days.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "cloudsql_instance_automated_backups", + "cloudstorage_bucket_log_retention_policy_lock", + "cloudstorage_bucket_sufficient_retention_period" + ] + }, + { + "Id": "CCC.Core.CN14.AR02", + "Description": "When backups are created for disaster recovery purposes, the most recent backup MUST have a creation date within the past 30 days.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN14 Maintain Recent Backups", + "SubSection": "", + "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber" + ], + "Recommendation": "Implement automated backup processes to ensure that backups are created regularly. Monitor backup schedules and verify that the most recent backup creation date is within the last 30 days.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "cloudsql_instance_automated_backups" + ] + }, + { + "Id": "CCC.Core.CN14.AR03", + "Description": "When backups are created for disaster recovery purposes, the most recent backup MUST have a creation date within the past 14 days.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.Core.CN14 Maintain Recent Backups", + "SubSection": "", + "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "Implement automated backup processes to ensure that backups are created regularly. Monitor backup schedules and verify that the most recent backup creation date is within the last 14 days.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [] + } + ], + "Checks": [ + "cloudsql_instance_automated_backups" + ] + }, + { + "Id": "CCC.Core.CN03.AR01", + "Description": "When an entity attempts to modify the service through a user interface, the authentication process MUST require multiple identifying factors for authentication.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "SubSection": "", + "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-14" + ] + } + ] + } + ], + "Checks": [ + "compute_project_os_login_2fa_enabled" + ] + }, + { + "Id": "CCC.Core.CN03.AR02", + "Description": "When an entity attempts to modify the service through an API endpoint, the authentication process MUST require a credential such as an API key or token AND originate from within the trust perimeter.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "SubSection": "", + "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-14" + ] + } + ] + } + ], + "Checks": [ + "apikeys_api_restrictions_configured", + "apikeys_key_exists", + "apikeys_key_rotated_in_90_days" + ] + }, + { + "Id": "CCC.Core.CN03.AR03", + "Description": "When an entity attempts to view information on the service through a user interface, the authentication process MUST require multiple identifying factors from the user.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "SubSection": "", + "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-14" + ] + } + ] + } + ], + "Checks": [ + "compute_project_os_login_2fa_enabled" + ] + }, + { + "Id": "CCC.Core.CN03.AR04", + "Description": "When an entity attempts to view information on the service through an API endpoint, the authentication process MUST require a credential such as an API key or token AND originate from within the trust perimeter.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "SubSection": "", + "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-14" + ] + } + ] + } + ], + "Checks": [ + "apikeys_api_restrictions_configured", + "apikeys_key_exists", + "apikeys_key_rotated_in_90_days" + ] + }, + { + "Id": "CCC.Core.CN05.AR01", + "Description": "When an attempt is made to modify data on the service or a child resource, the service MUST block requests from unauthorized entities.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_public_access", + "compute_instance_public_ip", + "cloudsql_instance_public_ip", + "compute_instance_default_service_account_in_use", + "compute_instance_default_service_account_in_use_with_full_api_access", + "gke_cluster_no_default_service_account", + "iam_no_service_roles_at_project_level", + "iam_role_kms_enforce_separation_of_duties", + "iam_role_sa_enforce_separation_of_duties", + "iam_sa_no_administrative_privileges" + ] + }, + { + "Id": "CCC.Core.CN05.AR02", + "Description": "When administrative access or configuration change is attempted on the service or a child resource, the service MUST refuse requests from unauthorized entities.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "iam_no_service_roles_at_project_level", + "iam_sa_no_administrative_privileges", + "iam_role_kms_enforce_separation_of_duties", + "iam_role_sa_enforce_separation_of_duties", + "iam_account_access_approval_enabled" + ] + }, + { + "Id": "CCC.Core.CN05.AR03", + "Description": "When administrative access or configuration change is attempted on the service or a child resource in a multi-tenant environment, the service MUST refuse requests across tenant boundaries unless the origin is explicitly included in a pre-approved allowlist.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_uses_vpc_service_controls" + ] + }, + { + "Id": "CCC.Core.CN05.AR04", + "Description": "When data is requested from outside the trust perimeter, the service MUST refuse requests from unauthorized entities.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_public_access", + "cloudstorage_uses_vpc_service_controls", + "cloudsql_instance_public_ip", + "cloudsql_instance_public_access", + "compute_instance_public_ip", + "kms_key_not_publicly_accessible", + "bigquery_dataset_public_access" + ] + }, + { + "Id": "CCC.Core.CN05.AR05", + "Description": "When any request is made from outside the trust perimeter, the service MUST NOT provide any response that may indicate the service exists.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "compute_instance_public_ip", + "cloudstorage_bucket_public_access", + "cloudsql_instance_public_ip", + "kms_key_not_publicly_accessible", + "compute_image_not_publicly_shared", + "bigquery_dataset_public_access" + ] + }, + { + "Id": "CCC.Core.CN05.AR06", + "Description": "When any request is made to the service or a child resource, the service MUST refuse requests from unauthorized entities.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", + "SubSection": "", + "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-01", + "DSP-07", + "DSP-08", + "DSP-10", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "iam_no_service_roles_at_project_level", + "iam_sa_no_administrative_privileges", + "iam_sa_no_user_managed_keys", + "iam_sa_user_managed_key_rotate_90_days", + "iam_sa_user_managed_key_unused", + "iam_service_account_unused", + "iam_role_kms_enforce_separation_of_duties", + "iam_role_sa_enforce_separation_of_duties" + ] + }, + { + "Id": "CCC.Core.CN04.AR01", + "Description": "When administrative access or configuration change is attempted on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN04 Log All Access and Changes", + "SubSection": "", + "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-08" + ] + } + ] + } + ], + "Checks": [ + "iam_audit_logs_enabled", + "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", + "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", + "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", + "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", + "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", + "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", + "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", + "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled" + ] + }, + { + "Id": "CCC.Core.CN04.AR02", + "Description": "When any attempt is made to modify data on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN04 Log All Access and Changes", + "SubSection": "", + "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-08" + ] + } + ] + } + ], + "Checks": [ + "iam_audit_logs_enabled", + "cloudstorage_audit_logs_enabled", + "cloudstorage_bucket_logging_enabled", + "logging_sink_created" + ] + }, + { + "Id": "CCC.Core.CN04.AR03", + "Description": "When any attempt is made to read data on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN04 Log All Access and Changes", + "SubSection": "", + "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-08" + ] + } + ] + } + ], + "Checks": [ + "iam_audit_logs_enabled", + "cloudstorage_audit_logs_enabled", + "cloudstorage_bucket_logging_enabled", + "logging_sink_created" + ] + }, + { + "Id": "CCC.Core.CN07.AR01", + "Description": "When enumeration activities are detected, the service MUST publish an event to a monitored channel which includes the client identity, time, and nature of the activity.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN07 Alert on Unusual Enumeration Activity", + "SubSection": "", + "SubSectionObjective": "Ensure that logs and associated alerts are generated when unusual enumeration activity is detected that may indicate reconnaissance activities.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Implement event publication mechanisms and alerts for patterns indicative of enumeration activities, such as repeated access attempts, requests, or liveness probes. Configure alerts to notify security teams of any activities that merit further investigation.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH15" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-05", + "SEF-05" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Core.CN07.AR02", + "Description": "When enumeration activities are detected, the service MUST log the client identity, time, and nature of the activity.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements.", + "Section": "CCC.Core.CN07 Alert on Unusual Enumeration Activity", + "SubSection": "", + "SubSectionObjective": "Ensure that logs and associated alerts are generated when unusual enumeration activity is detected that may indicate reconnaissance activities.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Implement logging mechanisms to capture details of enumeration activities, including client identity, timestamps, and activity nature. Retain logs according to organizational policies, and occasionally review them for patterns that may indicate reconnaissance activities.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH15" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "LOG-05", + "SEF-05" + ] + } + ] + } + ], + "Checks": [] + }, { "Id": "CCC.AuditLog.CN01.AR01", "Description": "When the signature validation process is performed, then it MUST detect any modification of data.", @@ -18,13 +1638,13 @@ "Applicability": [ "tlp-red" ], - "Recommendation": "Ensure hash of data is included in digital signature. ", + "Recommendation": "Ensure hash of data is included in digital signature.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06", - "CCC.TH07" + "CCC.Core.TH06", + "CCC.Core.TH07" ] } ], @@ -44,12 +1664,7 @@ ] } ], - "Checks": [ - "iam_audit_logs_enabled", - "cloudstorage_bucket_log_retention_policy_lock", - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled" - ] + "Checks": [] }, { "Id": "CCC.AuditLog.CN01.AR02", @@ -64,13 +1679,13 @@ "Applicability": [ "tlp-red" ], - "Recommendation": "Ensure verification process includes a chained hash function. ", + "Recommendation": "Ensure verification process includes a chained hash function.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06", - "CCC.TH07" + "CCC.Core.TH06", + "CCC.Core.TH07" ] } ], @@ -90,11 +1705,7 @@ ] } ], - "Checks": [ - "iam_audit_logs_enabled", - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled" - ] + "Checks": [] }, { "Id": "CCC.AuditLog.CN02.AR01", @@ -115,7 +1726,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06" + "CCC.Core.TH06" ] } ], @@ -139,15 +1750,7 @@ ], "Checks": [ "iam_audit_logs_enabled", - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled" + "logging_sink_created" ] }, { @@ -164,12 +1767,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Ensure alerting is correctly configured ", + "Recommendation": "Ensure alerting is correctly configured", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -191,17 +1794,7 @@ } ], "Checks": [ - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "cloudstorage_bucket_log_retention_policy_lock", - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", - "iam_audit_logs_enabled" + "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled" ] }, { @@ -218,12 +1811,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Ensure alerting is correctly configured ", + "Recommendation": "Ensure alerting is correctly configured", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -245,9 +1838,7 @@ } ], "Checks": [ - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "cloudstorage_bucket_log_retention_policy_lock", - "logging_sink_created" + "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled" ] }, { @@ -264,13 +1855,13 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Configure the audit log bucket to enable server access logging. Ensure the target logging bucket is configured for appropriate security, including restricted access and immutability. ", + "Recommendation": "Configure the audit log bucket to enable server access logging. Ensure the target logging bucket is configured for appropriate security, including restricted access and immutability.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01", - "CCC.TH09" + "CCC.Core.TH01", + "CCC.Core.TH09" ] } ], @@ -292,10 +1883,8 @@ } ], "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock", - "cloudstorage_bucket_public_access", - "cloudstorage_bucket_uniform_bucket_level_access", - "logging_sink_created" + "cloudstorage_bucket_logging_enabled", + "cloudstorage_audit_logs_enabled" ] }, { @@ -312,12 +1901,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Configure audit log exporting. ", + "Recommendation": "Configure audit log exporting.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -340,11 +1929,8 @@ } ], "Checks": [ - "logging_sink_created", "iam_audit_logs_enabled", - "cloudstorage_bucket_log_retention_policy_lock", - "cloudstorage_bucket_uniform_bucket_level_access", - "cloudstorage_bucket_public_access" + "logging_sink_created" ] }, { @@ -362,13 +1948,13 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Configure the audit log bucket's lifecycle rules or object retention settings to enforce the required data retention period. ", + "Recommendation": "Configure the audit log bucket's lifecycle rules or object retention settings to enforce the required data retention period.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06", - "CCC.TH07" + "CCC.Core.TH06", + "CCC.Core.TH07" ] } ], @@ -390,8 +1976,7 @@ } ], "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock", - "logging_sink_created" + "cloudstorage_bucket_log_retention_policy_lock" ] }, { @@ -409,13 +1994,13 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Enable MFA Delete (or equivalent multi-factor authentication for delete operations) on the audit log bucket. ", + "Recommendation": "Enable MFA Delete (or equivalent multi-factor authentication for delete operations) on the audit log bucket.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06", - "CCC.TH07" + "CCC.Core.TH06", + "CCC.Core.TH07" ] } ], @@ -436,11 +2021,7 @@ ] } ], - "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock", - "iam_audit_logs_enabled", - "logging_sink_created" - ] + "Checks": [] }, { "Id": "CCC.AuditLog.CN08.AR01", @@ -456,12 +2037,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Configure object lock policy. ", + "Recommendation": "Configure object lock policy.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -500,12 +2081,12 @@ "tlp-red", "tlp-amber" ], - "Recommendation": "Review field level access controls on audit data. ", + "Recommendation": "Review field level access controls on audit data.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH07" ] } ], @@ -547,59 +2128,12 @@ "tlp-amber", "tlp-green" ], - "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted. ", + "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AA-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "SC-7" - ] - } - ] - } - ], - "Checks": [ - "cloudstorage_bucket_public_access", - "cloudstorage_bucket_uniform_bucket_level_access", - "cloudstorage_bucket_log_retention_policy_lock" - ] - }, - { - "Id": "CCC.AuditLog.CN10.AR02", - "Description": "When the URL of a audit log storage bucket's object is accessed publicly then, it should be denied by bucket policy.", - "Attributes": [ - { - "FamilyName": "Confidentiality", - "FamilyDescription": "Controls designed to protected the confidentiality of Audit Log data.", - "Section": "CCC.AuditLog.CN10 Ensure Audit Bucket is Not Publicly Accessible", - "SubSection": "", - "SubSectionObjective": "Ensure that audit log storage buckets are not publicly accessible to prevent unauthorized exposure of sensitive log data.", - "Applicability": [ - "tlp-red", - "tlp-amber", - "tlp-green" - ], - "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -626,25 +2160,26 @@ ] }, { - "Id": "CCC.Build.CN01.AR01", - "Description": "Attempt to initiate a build using an unauthorized build agent and verify that the build is rejected.", + "Id": "CCC.AuditLog.CN10.AR02", + "Description": "When the URL of a audit log storage bucket's object is accessed publicly then, it should be denied by bucket policy.", "Attributes": [ { - "FamilyName": "Access Control", - "FamilyDescription": "TODO: Describe this control family", - "Section": "", - "SubSection": "CCC.Build.C01 Restrict Allowed Build Agents", - "SubSectionObjective": "Ensure that builds are executed only on authorized build agents to maintain control over the build environment and prevent unauthorized code execution.", + "FamilyName": "Confidentiality", + "FamilyDescription": "Controls designed to protected the confidentiality of Audit Log data.", + "Section": "CCC.AuditLog.CN10 Ensure Audit Bucket is Not Publicly Accessible", + "SubSection": "", + "SubSectionObjective": "Ensure that audit log storage buckets are not publicly accessible to prevent unauthorized exposure of sensitive log data.", "Applicability": [ "tlp-red", - "tlp-amber" + "tlp-amber", + "tlp-green" ], - "Recommendation": "", + "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -652,14 +2187,296 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.AC-4" + "PR.AA-05" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ "AC-3", - "AC-6" + "SC-7" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_public_access", + "cloudstorage_bucket_uniform_bucket_level_access" + ] + }, + { + "Id": "CCC.Logging.CN01.AR01", + "Description": "When a new cloud account is created, provider-level audit and network flow logging MUST be enabled by default and directed to the central sink.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", + "Section": "CCC.Logging.CN01 Centralized and Comprehensive Log Aggregation", + "SubSection": "", + "SubSectionObjective": "Ensure all operational and security logs from across the cloud environment, including applications, operating systems, network traffic, and cloud service activity, are captured automatically and streamed to a central, secure log management service.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Logging.TH07" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.PS-04" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-2", + "AU-3" + ] + } + ] + } + ], + "Checks": [ + "iam_audit_logs_enabled", + "compute_subnet_flow_logs_enabled", + "logging_sink_created" + ] + }, + { + "Id": "CCC.Logging.CN01.AR02", + "Description": "When a new cloud compute resource is deployed, it MUST be configured to forward all relevant logs (e.g., OS, application, service logs) to the central log sink.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", + "Section": "CCC.Logging.CN01 Centralized and Comprehensive Log Aggregation", + "SubSection": "", + "SubSectionObjective": "Ensure all operational and security logs from across the cloud environment, including applications, operating systems, network traffic, and cloud service activity, are captured automatically and streamed to a central, secure log management service.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Logging.TH07" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.PS-04" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-2", + "AU-3" + ] + } + ] + } + ], + "Checks": [ + "logging_sink_created", + "compute_subnet_flow_logs_enabled", + "compute_loadbalancer_logging_enabled", + "compute_network_dns_logging_enabled" + ] + }, + { + "Id": "CCC.Logging.CN02.AR01", + "Description": "When a new log bucket or stream is created, its retention policy MUST be configured in accordance with organisation's data retention policy.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", + "Section": "CCC.Logging.CN02 Enforce Data Retention Policy for Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that the retention period configured for logs aligns with the organization's data retention policy.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Logging.TH05" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "GV.PO-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-11" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_log_retention_policy_lock", + "cloudstorage_bucket_sufficient_retention_period" + ] + }, + { + "Id": "CCC.Logging.CN02.AR02", + "Description": "When a query is performed to retrieve log events older than the number of days defined in the organisation's data retention policy, it MUST return an empty result.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", + "Section": "CCC.Logging.CN02 Enforce Data Retention Policy for Logs", + "SubSection": "", + "SubSectionObjective": "Ensure that the retention period configured for logs aligns with the organization's data retention policy.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Logging.TH05" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "GV.PO-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-11" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_log_retention_policy_lock", + "cloudstorage_bucket_sufficient_retention_period" + ] + }, + { + "Id": "CCC.Logging.CN03.AR01", + "Description": "When an attempt is made to modify or delete data before the object lock period expires, then the action MUST be denied.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data.", + "Section": "CCC.Logging.CN03 Enable Object Lock On Log Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure log immutability by enabling Write Once, Read Many (WORM) protection using object lock on log storage buckets. This prevents logs from being modified or deleted during the defined retention period, supporting compliance and forensic integrity.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "Configure object lock policy.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH07" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.PS-04" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-9", + "AU-11" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_log_retention_policy_lock" + ] + }, + { + "Id": "CCC.Logging.CN04.AR01", + "Description": "When restricted fields are accessed by unauthorized users, then those fields MUST remain masked.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify logs.", + "Section": "CCC.Logging.CN04 Restrict Field And Log Type Access", + "SubSection": "", + "SubSectionObjective": "Configure access to logs to follow the principle of least privilege in particular where technically possible limit the log fields users have access to to prevent accidental exposure to sensitive information such as PII.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "Review field level access controls on log data.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Logging.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.PS-04" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6", + "AU-9", + "AC-3", + "PT-2", + "PT-3", + "PT-3" ] } ] @@ -668,25 +2485,26 @@ "Checks": [] }, { - "Id": "CCC.Build.CN02.AR01", - "Description": "Attempt to trigger a build from an unauthorized external service or repository and verify that the build does not start.", + "Id": "CCC.Logging.CN05.AR01", + "Description": "When a log storage bucket is created, the bucket's access control settings MUST explicitly deny public read and write access.", "Attributes": [ { - "FamilyName": "Access Control", - "FamilyDescription": "TODO: Describe this control family", - "Section": "", - "SubSection": "CCC.Build.C02 Restrict Allowed External Services for Build Triggers", - "SubSectionObjective": "Ensure that builds can only be triggered by authorized external services or repositories to prevent unauthorized code execution or tampering.", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify logs.", + "Section": "CCC.Logging.CN05 Ensure Log Bucket is Not Publicly Accessible", + "SubSection": "", + "SubSectionObjective": "Ensure that log storage buckets are not publicly accessible to prevent unauthorized access to sensitive log data. In addition, logs should be replicated to another cloud region to enhance availability, durability, and support disaster recovery requirements.", "Applicability": [ "tlp-red", - "tlp-amber" + "tlp-amber", + "tlp-green" ], - "Recommendation": "", + "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted.", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -694,14 +2512,108 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.AC-4" + "PR.AA-05" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ "AC-3", - "AC-6" + "SC-7" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_public_access", + "cloudstorage_bucket_uniform_bucket_level_access" + ] + }, + { + "Id": "CCC.Logging.CN05.AR02", + "Description": "When the URL of a log storage bucket's object is accessed publicly, the action MUST be denied by bucket policy.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify logs.", + "Section": "CCC.Logging.CN05 Ensure Log Bucket is Not Publicly Accessible", + "SubSection": "", + "SubSectionObjective": "Ensure that log storage buckets are not publicly accessible to prevent unauthorized access to sensitive log data. In addition, logs should be replicated to another cloud region to enhance availability, durability, and support disaster recovery requirements.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green" + ], + "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", + "SC-7" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_public_access", + "cloudstorage_bucket_uniform_bucket_level_access" + ] + }, + { + "Id": "CCC.Logging.CN06.AR01", + "Description": "When a single principal executes an anomalously high number of log queries, an alert MUST be generated.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain logging-related events.", + "Section": "CCC.Logging.CN06 Detect and Alert on Potential Log Exfiltration", + "SubSection": "", + "SubSectionObjective": "Identify and alert on anomalous data access patterns that may indicate an attempt to exfiltrate log data.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Logging.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-03", + "DE.CM-09" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4", + "CA-7", + "AU-6" ] } ] @@ -710,26 +2622,1273 @@ "Checks": [] }, { - "Id": "CCC.Build.CN03.AR01", - "Description": "Attempt to access the build environment from an external network and verify that access is denied.", + "Id": "CCC.Logging.CN07.AR01", + "Description": "When an audit log event is recorded that corresponds to a modification of the logging service configuration such as disabling a log trail, deleting a log sink, or altering a log forwarding rule, an alert MUST be generated.", "Attributes": [ { - "FamilyName": "Network Security", - "FamilyDescription": "TODO: Describe this control family", - "Section": "", - "SubSection": "CCC.Build.C03 Deny External Network Access for Build Environments", - "SubSectionObjective": "Ensure that build environments do not have external network access to prevent unauthorized external access and data exfiltration.", + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain logging-related events.", + "Section": "CCC.Logging.CN07 Detect and Alert on Log Service Tampering", + "SubSection": "", + "SubSectionObjective": "Alert when any component of the critical logging infrastructure is disabled, modified, or deleted, indicating a defense evasion attempt.", "Applicability": [ - "tlp-red", - "tlp-amber" + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH02", - "CCC.TH05" + "CCC.Core.TH16" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-03", + "DE.CM-09" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4", + "CA-7", + "AU-6" + ] + } + ] + } + ], + "Checks": [ + "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled" + ] + }, + { + "Id": "CCC.Monitor.CN01.AR01", + "Description": "When an External Monitoring system exceeds the anticipated rate of monitoring checks then Rate Limiting MUST be applied and an Audit Alert MUST be generated.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain events from other monitoring services.", + "Section": "CCC.Monitor.CN01 Rate Limiting on External Monitoring", + "SubSection": "", + "SubSectionObjective": "Prevent DoS attacks using External Monitoring tools.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Monitor.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IR-01", + "DE.CM-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-5", + "SC-7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Monitor.CN02.AR01", + "Description": "When an Custom or User-Defined Metric starts to flood a collector, then a rate limit MUST be applied to reduce the network impact of traffic and an alert must triggered.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain events from other monitoring services.", + "Section": "CCC.Monitor.CN02 Rate Limiting on Metric Generation", + "SubSection": "", + "SubSectionObjective": "Prevent Malicious Actor or misconfiguration from flooding services with metric data.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Monitor.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-5(2)", + "CA-7", + "SI-4" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Monitor.CN03.AR01", + "Description": "When external systems have approved access to internal systems not normally available for public access then they MUST be secured to prevent unauthorised access jumping through to the internal systems and only allow access to specific internal services.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", + "Section": "CCC.Monitor.CN03 Access External Monitoring", + "SubSection": "", + "SubSectionObjective": "Control access to Synthetic monitoring solutions using API keys or Certificate based authentication to ensure they don't become an attack path, preventing monitoring systems from forging network requests to gain access to internal systems.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Monitor.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-06", + "PR.IR-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3" + ] + } + ] + } + ], + "Checks": [ + "apikeys_api_restrictions_configured", + "apikeys_key_exists", + "apikeys_key_rotated_in_90_days" + ] + }, + { + "Id": "CCC.Monitor.CN04.AR01", + "Description": "When monitoring dashboards display degraded services which may become potential targets then the dashboard MUST be protected from unauthorised access.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", + "Section": "CCC.Monitor.CN04 Restrict access to Monitoring Dashboards", + "SubSection": "", + "SubSectionObjective": "Control access to Monitoring Dashboards and reports to ensure they don't highlight an attack path.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Monitor.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-09", + "DE.AE-03" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4", + "AC-3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Monitor.CN05.AR01", + "Description": "When monitoring services have generated an alert, the service MUST ensure only authorised responders silence or acknowledge the alert.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", + "Section": "CCC.Monitor.CN05 Restrict access to silence or acknowledge an alert", + "SubSection": "", + "SubSectionObjective": "Ensure only a subset of users can silence or acknowledge alerts to prevent attackers hiding their activity.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH10" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IR-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Monitor.CN06.AR01", + "Description": "When systems push metrics or traces they MUST be authenticated for that particular type of metric or trace", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", + "Section": "CCC.Monitor.CN06 Metrics pushed for authorised services only", + "SubSection": "", + "SubSectionObjective": "Use IAM to control which types of metrics or traces can be pushed by different system to avoid a compromised system pushing fabricated metrics about a different service", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Monitor.TH05" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-5" + ] + } + ] + } + ], + "Checks": [ + "iam_sa_no_administrative_privileges", + "iam_sa_no_user_managed_keys", + "compute_instance_default_service_account_in_use", + "compute_instance_default_service_account_in_use_with_full_api_access" + ] + }, + { + "Id": "CCC.ObjStor.CN01.AR01", + "Description": "When a request is made to read a bucket, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", + "SubSection": "", + "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption, or sensitive data decryption.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-03", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "kms_key_not_publicly_accessible" + ] + }, + { + "Id": "CCC.ObjStor.CN01.AR02", + "Description": "When a request is made to read an object, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", + "SubSection": "", + "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption, or sensitive data decryption.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-03", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "kms_key_not_publicly_accessible" + ] + }, + { + "Id": "CCC.ObjStor.CN01.AR03", + "Description": "When a request is made to write to a bucket, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", + "SubSection": "", + "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption, or sensitive data decryption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-03", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "kms_key_not_publicly_accessible" + ] + }, + { + "Id": "CCC.ObjStor.CN01.AR04", + "Description": "When a request is made to write to an object, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", + "SubSection": "", + "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption, or sensitive data decryption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-03", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "kms_key_not_publicly_accessible" + ] + }, + { + "Id": "CCC.ObjStor.CN03.AR01", + "Description": "When an object storage bucket deletion is attempted, the bucket MUST be fully recoverable for a set time-frame after deletion is requested.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN03 Prevent Bucket Deletion Through Irrevocable Bucket Retention Policy", + "SubSection": "", + "SubSectionObjective": "Ensure that object storage bucket is not deleted after creation, and that the preventative measure cannot be unset.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_soft_delete_enabled", + "cloudstorage_bucket_log_retention_policy_lock" + ] + }, + { + "Id": "CCC.ObjStor.CN03.AR02", + "Description": "When an attempt is made to modify the retention policy for an object storage bucket, the service MUST prevent the policy from being modified.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN03 Prevent Bucket Deletion Through Irrevocable Bucket Retention Policy", + "SubSection": "", + "SubSectionObjective": "Ensure that object storage bucket is not deleted after creation, and that the preventative measure cannot be unset.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_log_retention_policy_lock" + ] + }, + { + "Id": "CCC.ObjStor.CN04.AR01", + "Description": "When an object is uploaded to the object storage system, the object MUST automatically receive a default retention policy that prevents premature deletion or modification.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN04 Objects have an Effective Retention Policy by Default", + "SubSection": "", + "SubSectionObjective": "Ensure that all objects stored in the object storage system have a retention policy applied by default, preventing premature deletion or modification of objects.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + }, + { + "ReferenceId": "CCC.ObjStor", + "Identifiers": [ + "CCC.ObjStor.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_sufficient_retention_period", + "cloudstorage_bucket_log_retention_policy_lock" + ] + }, + { + "Id": "CCC.ObjStor.CN04.AR02", + "Description": "When an attempt is made to delete or modify an object that is subject to an active retention policy, the service MUST prevent the action from being completed.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN04 Objects have an Effective Retention Policy by Default", + "SubSection": "", + "SubSectionObjective": "Ensure that all objects stored in the object storage system have a retention policy applied by default, preventing premature deletion or modification of objects.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + }, + { + "ReferenceId": "CCC.ObjStor", + "Identifiers": [ + "CCC.ObjStor.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_log_retention_policy_lock" + ] + }, + { + "Id": "CCC.ObjStor.CN05.AR01", + "Description": "When an object is uploaded to the object storage bucket, the object MUST be stored with a unique identifier.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN05 Versioning is Enabled for All Objects in the Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_versioning_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN05.AR02", + "Description": "When an object is modified, the service MUST assign a new unique identifier to the modified object to differentiate it from the previous version.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN05 Versioning is Enabled for All Objects in the Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_versioning_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN05.AR03", + "Description": "When an object is modified, the service MUST allow for recovery of previous versions of the object.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN05 Versioning is Enabled for All Objects in the Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_versioning_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN05.AR04", + "Description": "When an object is deleted, the service MUST retain other versions of the object to allow for recovery of previous versions.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN05 Versioning is Enabled for All Objects in the Bucket", + "SubSection": "", + "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "DSP-17" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_versioning_enabled" + ] + }, + { + "Id": "CCC.ObjStor.CN07.AR01", + "Description": "The object storage service MUST support a configuration option that requires MFA to be successfully completed before any object deletion can be attempted, regardless of the request interface.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN07 Multi-Factor Authentication Is Required for Object Deletion", + "SubSection": "", + "SubSectionObjective": "Ensure that deletion of objects stored in the object storage system is protected by multi-factor authentication (MFA), reducing the risk of accidental, unauthorized, or compromised-credential–based data destruction.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06", + "CCC.Core.TH17" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "IAM-12" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.ObjStor.CN07.AR02", + "Description": "When MFA deletion protection is enabled on a bucket or object namespace, the service MUST deny any deletion request from an identity that has not satisfied the MFA requirement at the time of the request.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN07 Multi-Factor Authentication Is Required for Object Deletion", + "SubSection": "", + "SubSectionObjective": "Ensure that deletion of objects stored in the object storage system is protected by multi-factor authentication (MFA), reducing the risk of accidental, unauthorized, or compromised-credential–based data destruction.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06", + "CCC.Core.TH17" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "IAM-12" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.ObjStor.CN07.AR03", + "Description": "When an attempt is made to delete an object, the service's audit logs MUST clearly record each deletion attempt, including whether MFA was required and whether validation was met.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + "Section": "CCC.ObjStor.CN07 Multi-Factor Authentication Is Required for Object Deletion", + "SubSection": "", + "SubSectionObjective": "Ensure that deletion of objects stored in the object storage system is protected by multi-factor authentication (MFA), reducing the risk of accidental, unauthorized, or compromised-credential–based data destruction.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01", + "CCC.Core.TH06", + "CCC.Core.TH17" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "DSP-16", + "IAM-12" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.ObjStor.CN02.AR01", + "Description": "When a permission set is allowed for an object in a bucket, the service MUST allow the same permission set to access all objects in the same bucket.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources.", + "Section": "CCC.ObjStor.CN02 Enforce Uniform Bucket-level Access to Prevent Inconsistent Permissions", + "SubSection": "", + "SubSectionObjective": "Ensure that uniform bucket-level access is enforced across all object storage buckets. This prevents the use of ad-hoc or inconsistent object-level permissions, ensuring centralized, consistent, and secure access management in accordance with the principle of least privilege.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_uniform_bucket_level_access" + ] + }, + { + "Id": "CCC.ObjStor.CN02.AR02", + "Description": "When a permission set is denied for an object in a bucket, the service MUST deny the same permission set to access all objects in the same bucket.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources.", + "Section": "CCC.ObjStor.CN02 Enforce Uniform Bucket-level Access to Prevent Inconsistent Permissions", + "SubSection": "", + "SubSectionObjective": "Ensure that uniform bucket-level access is enforced across all object storage buckets. This prevents the use of ad-hoc or inconsistent object-level permissions, ensuring centralized, consistent, and secure access management in accordance with the principle of least privilege.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_uniform_bucket_level_access" + ] + }, + { + "Id": "CCC.LB.CN01.AR01", + "Description": "When a single client sends more than 2000 requests within any 5-minute sliding window, the load balancer MUST throttle all subsequent requests from that client for at least 60 seconds.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity.", + "Section": "CCC.LB.CN01 Enforce and Detect Rate Limiting", + "SubSection": "", + "SubSectionObjective": "Detect and throttle malicious or excessive requests to prevent downstream resource exhaustion and brute-force activity.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Implement per-IP token-bucket limits with and verify via synthetic traffic tests.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH01", + "CCC.LB.TH09" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-1", + "PR.AC-7", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-6", + "SC-5", + "AC-7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.LB.CN01.AR02", + "Description": "When throttling is invoked, the load balancer MUST record the event in the access log within 5 minutes for alerting and trend analysis.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity.", + "Section": "CCC.LB.CN01 Enforce and Detect Rate Limiting", + "SubSection": "", + "SubSectionObjective": "Detect and throttle malicious or excessive requests to prevent downstream resource exhaustion and brute-force activity.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Enable access logging and configure metric filters on HTTP 429 counts to trigger alerts.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH01", + "CCC.LB.TH09" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-1", + "PR.AC-7", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-6", + "SC-5", + "AC-7" + ] + } + ] + } + ], + "Checks": [ + "compute_loadbalancer_logging_enabled" + ] + }, + { + "Id": "CCC.LB.CN06.AR01", + "Description": "When more than 10 percent of targets change from healthy to unhealthy within five minutes, an alert MUST be issued.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity.", + "Section": "CCC.LB.CN06 Secure Health-Check Telemetry", + "SubSection": "", + "SubSectionObjective": "Monitor health-check endpoints for tampering and alert on abnormal status changes.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Instrument metrics for health check results and target removal events. Configure monitoring alarms to alert on abnormal spikes in unhealthy targets.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH05" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.AE-2" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4" + ] + } + ] + } + ], + "Checks": [ + "compute_loadbalancer_logging_enabled" + ] + }, + { + "Id": "CCC.LB.CN04.AR01", + "Description": "When routing weights change, the request MUST originate from an explicitly defined and trusted identity and MUST be logged.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can change or query load-balancer resources.", + "Section": "CCC.LB.CN04 Enforce Distribution Policies", + "SubSection": "", + "SubSectionObjective": "Ensure traffic-splitting weights and algorithms are modified only by trusted identities.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Define a list of trusted principals allowed to modify routing configurations. Enforce via conditional access policies, and log changes using audit logging.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3" + ] + } + ] + } + ], + "Checks": [ + "iam_audit_logs_enabled", + "compute_loadbalancer_logging_enabled" + ] + }, + { + "Id": "CCC.LB.CN05.AR01", + "Description": "When stickiness is enabled, session cookies MUST expire within 30 minutes of inactivity.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can change or query load-balancer resources.", + "Section": "CCC.LB.CN05 Validate Session Affinity", + "SubSection": "", + "SubSectionObjective": "Configure session persistence to minimise fixation and hijacking risks.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Audit CCC.LB.CP15 parameters via configuration scans.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-7" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-23" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.LB.CN09.AR01", + "Description": "When an API call originates outside the approved CIDR set, the request MUST be denied.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can change or query load-balancer resources.", + "Section": "CCC.LB.CN09 Restrict Management API Access", + "SubSection": "", + "SubSectionObjective": "Limit load-balancer API calls to authorised identities and trusted networks.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Combine VPC endpoints with IAM condition-key filters for protected APIs.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH08" ] } ], @@ -743,102 +3902,7 @@ { "ReferenceId": "NIST_800_53", "Identifiers": [ - "SC-7", - "SC-5" - ] - } - ] - } - ], - "Checks": [ - "compute_firewall_rdp_access_from_the_internet_allowed", - "compute_firewall_ssh_access_from_the_internet_allowed", - "compute_instance_public_ip", - "cloudstorage_bucket_public_access", - "cloudsql_instance_public_ip", - "cloudsql_instance_public_access" - ] - }, - { - "Id": "CCC.CntrReg.CN01.AR01", - "Description": "Attempt to push an artifact with known vulnerabilities to the registry and observe if it is flagged or rejected by the vulnerability scanning process.", - "Attributes": [ - { - "FamilyName": "Risk Management", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CntrReg.CN01 Implement Vulnerability Scanning for Artifacts", - "SubSection": "", - "SubSectionObjective": "Ensure that container images and artifacts stored in the container registry are scanned for vulnerabilities to identify and remediate security issues before deployment.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.CntrReg.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "ID.RA-1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "RA-5", - "SI-5" - ] - } - ] - } - ], - "Checks": [ - "artifacts_container_analysis_enabled", - "gcr_container_scanning_enabled" - ] - }, - { - "Id": "CCC.DataWar.CN01.AR01", - "Description": "Attempt to access underlying database tables directly without using managed views and verify that access is denied.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.DataWar.CN01 Enforce Use of Managed Views for Data Access", - "SubSection": "", - "SubSectionObjective": "Ensure that data access is provided through managed views, restricting users from accessing underlying tables directly and enforcing consistent security policies.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "AC-6" + "SC-7" ] } ] @@ -847,25 +3911,26 @@ "Checks": [] }, { - "Id": "CCC.DataWar.CN02.AR01", - "Description": "Attempt to query sensitive columns without the necessary permissions and verify that access is denied or data is masked.", + "Id": "CCC.LB.CN02.AR01", + "Description": "When concurrent connections reach 80 percent of capacity, the autoscaling group MUST add at least one instance within five minutes.", "Attributes": [ { "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.DataWar.CN02 Enforce Column-Level Security Policies", + "FamilyDescription": "Controls that preserve availability and confidentiality of traffic processed by the load balancer.", + "Section": "CCC.LB.CN02 Auto-Scale Load Balancer Capacity", "SubSection": "", - "SubSectionObjective": "Ensure that access to sensitive data columns is restricted based on user roles, preventing unauthorized access to sensitive information.", + "SubSectionObjective": "Expand load-balancer capacity to maintain availability during traffic spikes.", "Applicability": [ - "tlp-red", - "tlp-amber" + "tlp-green", + "tlp-amber", + "tlp-red" ], - "Recommendation": "", + "Recommendation": "Enable autoscaling policies.", "SectionThreatMappings": [ { - "ReferenceId": "CCC", + "ReferenceId": "LB", "Identifiers": [ - "CCC.TH01" + "CCC.LB.TH09" ] } ], @@ -873,14 +3938,13 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.AC-4" + "ID.BE-5" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AC-3", - "AC-6" + "CP-10" ] } ] @@ -889,25 +3953,26 @@ "Checks": [] }, { - "Id": "CCC.DataWar.CN03.AR01", - "Description": "Attempt to query data rows that the user should not have access to and verify that access is denied or data is not returned.", + "Id": "CCC.LB.CN07.AR01", + "Description": "When responses pass through the load balancer, the \"Server\" header MUST be replaced with \"lb\".", "Attributes": [ { "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.DataWar.CN03 Enforce Row-Level Security Policies", + "FamilyDescription": "Controls that preserve availability and confidentiality of traffic processed by the load balancer.", + "Section": "CCC.LB.CN07 Scrub Sensitive Headers", "SubSection": "", - "SubSectionObjective": "Ensure that access to data rows is restricted based on user roles or attributes, preventing unauthorized access to specific subsets of data.", + "SubSectionObjective": "Remove headers that disclose internal details or software versions from HTTP responses.", "Applicability": [ - "tlp-red", - "tlp-amber" + "tlp-green", + "tlp-amber", + "tlp-red" ], - "Recommendation": "", + "Recommendation": "Configure header-transformation rules.", "SectionThreatMappings": [ { - "ReferenceId": "CCC", + "ReferenceId": "LB", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH15" ] } ], @@ -915,50 +3980,286 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.AC-4" + "PR.DS-2" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AC-3", - "AC-6" + "SC-13" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.LB.CN08.AR01", + "Description": "When a certificate is within 30 days of expiry, automated renewal MUST complete and deploy a new certificate within 24 hours.", + "Attributes": [ + { + "FamilyName": "Encryption", + "FamilyDescription": "Controls that ensure trustworthy TLS certificates and ciphers.", + "Section": "CCC.LB.CN08 Automate Certificate Renewal", + "SubSection": "", + "SubSectionObjective": "Maintain valid TLS certificates by automating renewal and deployment before expiry.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "Use certificate-manager auto-renewal workflows.", + "SectionThreatMappings": [ + { + "ReferenceId": "LB", + "Identifiers": [ + "CCC.LB.TH07" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-17" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.VPC.CN01.AR01", + "Description": "When a subscription is created, the subscription MUST NOT contain default network resources.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.VPC.CN01 Restrict Default Network Creation", + "SubSection": "", + "SubSectionObjective": "Restrict the automatic creation of default virtual networks and related resources during subscription initialization to avoid insecure default configurations and enforce custom network policies.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.VPC.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-5" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.12.3.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7" + ] + } + ] + } + ], + "Checks": [ + "compute_network_default_in_use" + ] + }, + { + "Id": "CCC.VPC.CN02.AR01", + "Description": "When a resource is created in a public subnet, that resource MUST NOT be assigned an external IP address by default.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.VPC.CN02 Limit Resource Creation in Public Subnet", + "SubSection": "", + "SubSectionObjective": "Restrict the creation of resources in the public subnet with direct access to the internet to minimize attack surfaces.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.VPC.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "SEF-05" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.13.1.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-4" ] } ] } ], "Checks": [ - "iam_no_service_roles_at_project_level", - "iam_sa_no_administrative_privileges", - "iam_role_sa_enforce_separation_of_duties", - "iam_role_kms_enforce_separation_of_duties", - "iam_account_access_approval_enabled", - "iam_audit_logs_enabled", - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "cloudsql_instance_postgres_enable_pgaudit_flag", - "cloudsql_instance_postgres_log_connections_flag", - "cloudsql_instance_postgres_log_disconnections_flag", - "cloudsql_instance_postgres_log_min_messages_flag", - "cloudsql_instance_postgres_log_min_duration_statement_flag", - "cloudsql_instance_postgres_log_error_verbosity_flag", - "cloudsql_instance_postgres_log_min_error_statement_flag", - "cloudsql_instance_public_ip", - "cloudsql_instance_private_ip_assignment", - "compute_firewall_ssh_access_from_the_internet_allowed", - "compute_firewall_rdp_access_from_the_internet_allowed", - "compute_instance_default_service_account_in_use", - "compute_instance_default_service_account_in_use_with_full_api_access", "compute_instance_public_ip" ] }, + { + "Id": "CCC.VPC.CN03.AR01", + "Description": "When a VPC peering connection is requested, the service MUST prevent connections from VPCs that are not explicitly allowed.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.VPC.CN03 Restrict VPC Peering to Authorized Accounts", + "SubSection": "", + "SubSectionObjective": "Ensure VPC peering connections are only established with explicitly authorized destinations to limit network exposure and enforce boundary controls.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.VPC.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IVS-01" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.13.1.3" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-4" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.VPC.CN04.AR01", + "Description": "When any network traffic goes to or from an interface in the VPC, the service MUST capture and log all relevant information.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.VPC.CN04 Enforce VPC Flow Logs on VPCs", + "SubSection": "", + "SubSectionObjective": "Ensure VPCs are configured with flow logs enabled to capture traffic information.", + "Applicability": [ + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.VPC.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.PT-1" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.12.4.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AU-2" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IVS-06" + ] + } + ] + } + ], + "Checks": [ + "compute_subnet_flow_logs_enabled" + ] + }, { "Id": "CCC.KeyMgmt.CN01.AR01", "Description": "When a key version is scheduled for deletion or disabled, an alert MUST be generated within five minutes.", "Attributes": [ { - "FamilyName": "Logging and Metrics Publication", + "FamilyName": "Logging and Monitoring", "FamilyDescription": "Controls that collect, alert, and retain key-management events.", "Section": "CCC.KeyMgmt.CN01 Alert on Key-version Changes", "SubSection": "", @@ -1117,429 +4418,29 @@ ] } ], - "Checks": [ - "kms_key_not_publicly_accessible", - "kms_key_rotation_enabled" - ] - }, - { - "Id": "CCC.LB.CN01.AR01", - "Description": "When a single client sends more than 2000 requests within any 5-minute sliding window, the load balancer MUST throttle all subsequent requests from that client for at least 60 seconds.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity. ", - "Section": "CCC.LB.CN01 Enforce and Detect Rate Limiting", - "SubSection": "", - "SubSectionObjective": "Detect and throttle malicious or excessive requests to prevent downstream resource exhaustion and brute-force activity.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Implement per-IP token-bucket limits with and verify via synthetic traffic tests. ", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH01", - "CCC.LB.TH09" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-1", - "PR.AC-7", - "PR.PT-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-6", - "SC-5", - "AC-7" - ] - } - ] - } - ], - "Checks": [ - "compute_loadbalancer_logging_enabled", - "compute_subnet_flow_logs_enabled", - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled" - ] - }, - { - "Id": "CCC.LB.CN01.AR02", - "Description": "When throttling is invoked, the load balancer MUST record the event in the access log within 5 minutes for alerting and trend analysis.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity. ", - "Section": "CCC.LB.CN01 Enforce and Detect Rate Limiting", - "SubSection": "", - "SubSectionObjective": "Detect and throttle malicious or excessive requests to prevent downstream resource exhaustion and brute-force activity.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Enable access logging and configure metric filters on HTTP 429 counts to trigger alerts. ", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH01", - "CCC.LB.TH09" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-1", - "PR.AC-7", - "PR.PT-4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-6", - "SC-5", - "AC-7" - ] - } - ] - } - ], - "Checks": [ - "compute_loadbalancer_logging_enabled", - "logging_sink_created" - ] - }, - { - "Id": "CCC.LB.CN06.AR01", - "Description": "When more than 10 percent of targets change from healthy to unhealthy within five minutes, an alert MUST be issued.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that detect anomalous traffic and record load-balancer activity. ", - "Section": "CCC.LB.CN06 Secure Health-Check Telemetry", - "SubSection": "", - "SubSectionObjective": "Monitor health-check endpoints for tampering and alert on abnormal status changes.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Instrument metrics for health check results and target removal events. Configure monitoring alarms to alert on abnormal spikes in unhealthy targets. ", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH05" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SI-4" - ] - } - ] - } - ], - "Checks": [ - "compute_loadbalancer_logging_enabled", - "logging_sink_created", - "compute_subnet_flow_logs_enabled" - ] - }, - { - "Id": "CCC.LB.CN04.AR01", - "Description": "When routing weights change, the request MUST originate from an explicitly defined and trusted identity and MUST be logged.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can change or query load-balancer resources. ", - "Section": "CCC.LB.CN04 Enforce Distribution Policies", - "SubSection": "", - "SubSectionObjective": "Ensure traffic-splitting weights and algorithms are modified only by trusted identities.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Define a list of trusted principals allowed to modify routing configurations. Enforce via conditional access policies, and log changes using audit logging. ", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH03" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "iam_audit_logs_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "compute_loadbalancer_logging_enabled", - "logging_sink_created", - "iam_no_service_roles_at_project_level", - "iam_role_sa_enforce_separation_of_duties", - "iam_sa_no_administrative_privileges" - ] - }, - { - "Id": "CCC.LB.CN05.AR01", - "Description": "When stickiness is enabled, session cookies MUST expire within 30 minutes of inactivity.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can change or query load-balancer resources. ", - "Section": "CCC.LB.CN05 Validate Session Affinity", - "SubSection": "", - "SubSectionObjective": "Configure session persistence to minimise fixation and hijacking risks.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Audit CCC.LB.F15 parameters via configuration scans.", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH04" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-23" - ] - } - ] - } - ], - "Checks": [ - "iam_no_service_roles_at_project_level", - "iam_sa_no_administrative_privileges", - "iam_role_kms_enforce_separation_of_duties", - "iam_role_sa_enforce_separation_of_duties", - "iam_account_access_approval_enabled", - "iam_audit_logs_enabled", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled" - ] - }, - { - "Id": "CCC.LB.CN09.AR01", - "Description": "When an API call originates outside the approved CIDR set, the request MUST be denied.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can change or query load-balancer resources. ", - "Section": "CCC.LB.CN09 Restrict Management API Access", - "SubSection": "", - "SubSectionObjective": "Limit load-balancer API calls to authorised identities and trusted networks.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Combine VPC endpoints with IAM condition-key filters for protected APIs.", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH08" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-5" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-7" - ] - } - ] - } - ], - "Checks": [ - "iam_account_access_approval_enabled", - "iam_audit_logs_enabled", - "iam_no_service_roles_at_project_level", - "iam_role_kms_enforce_separation_of_duties", - "iam_role_sa_enforce_separation_of_duties", - "iam_sa_no_administrative_privileges", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled" - ] - }, - { - "Id": "CCC.LB.CN02.AR01", - "Description": "When concurrent connections reach 80 percent of capacity, the autoscaling group MUST add at least one instance within five minutes.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls that preserve availability and confidentiality of traffic processed by the load balancer. ", - "Section": "CCC.LB.CN02 Auto-Scale Load Balancer Capacity", - "SubSection": "", - "SubSectionObjective": "Expand load-balancer capacity to maintain availability during traffic spikes.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Enable autoscaling policies.", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH09" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "ID.BE-5" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "CP-10" - ] - } - ] - } - ], "Checks": [] }, { - "Id": "CCC.LB.CN07.AR01", - "Description": "When responses pass through the load balancer, the \"Server\" header MUST be replaced with \"lb\".", + "Id": "CCC.SecMgmt.CN01.AR01", + "Description": "Attempt to use an outdated version of a secret after its rotation period has passed and verify that access is denied.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "Controls that preserve availability and confidentiality of traffic processed by the load balancer. ", - "Section": "CCC.LB.CN07 Scrub Sensitive Headers", + "FamilyName": "Data Protection", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.SecMgmt.CN01 Enforce Automatic Secret Rotation", "SubSection": "", - "SubSectionObjective": "Remove headers that disclose internal details or software versions from HTTP responses.", + "SubSectionObjective": "Ensure that secrets are automatically rotated on a defined schedule to reduce the risk of secret compromise and unauthorized access.", "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-red", + "tlp-amber" ], - "Recommendation": "Configure header-transformation rules.", + "Recommendation": "", "SectionThreatMappings": [ { - "ReferenceId": "LB", + "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH15" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-13" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.LB.CN08.AR01", - "Description": "When a certificate is within 30 days of expiry, automated renewal MUST complete and deploy a new certificate within 24 hours.", - "Attributes": [ - { - "FamilyName": "Encryption", - "FamilyDescription": "Controls that ensure trustworthy TLS certificates and ciphers.", - "Section": "CCC.LB.CN08 Automate Certificate Renewal", - "SubSection": "", - "SubSectionObjective": "Maintain valid TLS certificates by automating renewal and deployment before expiry.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Use certificate-manager auto-renewal workflows.", - "SectionThreatMappings": [ - { - "ReferenceId": "LB", - "Identifiers": [ - "CCC.LB.TH07" + "CCC.Core.TH01", + "CCC.Core.TH14" ] } ], @@ -1553,7 +4454,8 @@ { "ReferenceId": "NIST_800_53", "Identifiers": [ - "SC-17" + "SC-12", + "SC-28" ] } ] @@ -1562,214 +4464,26 @@ "Checks": [] }, { - "Id": "CCC.Logging.CN01.AR01", - "Description": "When a new cloud account is created, provider-level audit and network flow logging MUST be enabled by default and directed to the central sink.", + "Id": "CCC.SecMgmt.CN02.AR01", + "Description": "Attempt to retrieve a secret from an unauthorized region and verify that access is denied.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", - "Section": "CCC.Logging.CN01 Centralized and Comprehensive Log Aggregation", + "FamilyName": "Data Protection", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.SecMgmt.CN02 Enforce Secret Replication Policies", "SubSection": "", - "SubSectionObjective": "Ensure all operational and security logs from across the cloud environment, including applications, operating systems, network traffic, and cloud service activity, are captured automatically and streamed to a central, secure log management service.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Logging.TH07" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.PS-04" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3" - ] - } - ] - } - ], - "Checks": [ - "iam_audit_logs_enabled", - "compute_subnet_flow_logs_enabled", - "logging_sink_created" - ] - }, - { - "Id": "CCC.Logging.CN01.AR02", - "Description": "When a new cloud compute resource is deployed, it MUST be configured to forward all relevant logs (e.g., OS, application, service logs) to the central log sink.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", - "Section": "CCC.Logging.CN01 Centralized and Comprehensive Log Aggregation", - "SubSection": "", - "SubSectionObjective": "Ensure all operational and security logs from across the cloud environment, including applications, operating systems, network traffic, and cloud service activity, are captured automatically and streamed to a central, secure log management service.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Logging.TH07" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.PS-04" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3" - ] - } - ] - } - ], - "Checks": [ - "logging_sink_created", - "cloudstorage_bucket_log_retention_policy_lock", - "iam_audit_logs_enabled", - "compute_subnet_flow_logs_enabled", - "compute_network_dns_logging_enabled", - "compute_loadbalancer_logging_enabled" - ] - }, - { - "Id": "CCC.Logging.CN02.AR01", - "Description": "When a new log bucket or stream is created, its retention policy MUST be configured in accordance with organisation's data retention policy.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", - "Section": "CCC.Logging.CN02 Enforce Data Retention Policy for Logs", - "SubSection": "", - "SubSectionObjective": "Ensure that the retention period configured for logs aligns with the organization's data retention policy.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Logging.TH05" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "GV.PO-01" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-11" - ] - } - ] - } - ], - "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock" - ] - }, - { - "Id": "CCC.Logging.CN02.AR02", - "Description": "When a query is performed to retrieve log events older than the number of days defined in the organisation's data retention policy, it MUST return an empty result.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", - "Section": "CCC.Logging.CN02 Enforce Data Retention Policy for Logs", - "SubSection": "", - "SubSectionObjective": "Ensure that the retention period configured for logs aligns with the organization's data retention policy.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Logging.TH05" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "GV.PO-01" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-11" - ] - } - ] - } - ], - "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock" - ] - }, - { - "Id": "CCC.AuditLog.CN08.AR01", - "Description": "When an attempt is made to modify or delete data before the object lock period expires, then the action MUST be denied.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "Controls related to the confidentiality, integrity and availability of log data. ", - "Section": "CCC.Logging.CN03 Enable Object Lock On Log Bucket", - "SubSection": "", - "SubSectionObjective": "Ensure log immutability by enabling Write Once, Read Many (WORM) protection using object lock on log storage buckets. This prevents logs from being modified or deleted during the defined retention period, supporting compliance and forensic integrity.", + "SubSectionObjective": "Ensure that secrets are replicated only to authorized locations as per organizational data residency and compliance requirements.", "Applicability": [ "tlp-red", "tlp-amber" ], - "Recommendation": "Configure object lock policy. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07" + "CCC.Core.TH03", + "CCC.Core.TH04" ] } ], @@ -1777,98 +4491,7 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.PS-04" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-9", - "AU-11" - ] - } - ] - } - ], - "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock" - ] - }, - { - "Id": "CCC.AuditLog.CN04.AR01", - "Description": "When restricted fields are accessed by unauthorized users, then those fields MUST remain masked.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can access and modify logs. ", - "Section": "CCC.Logging.CN04 Restrict Field And Log Type Access", - "SubSection": "", - "SubSectionObjective": "Configure access to logs to follow the principle of least privilege in particular where technically possible limit the log fields users have access to to prevent accidental exposure to sensitive information such as PII.", - "Applicability": [ - "tlp-red", - "tlp-amber" - ], - "Recommendation": "Review field level access controls on log data. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Logging.TH04" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.PS-04" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-6", - "AU-9", - "AC-3", - "PT-2", - "PT-3", - "PT-3" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.Logging.CN05.AR01", - "Description": "When a log storage bucket is created, the bucket's access control settings MUST explicitly deny public read and write access.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can access and modify logs. ", - "Section": "CCC.Logging.CN05 Ensure Log Bucket is Not Publicly Accessible", - "SubSection": "", - "SubSectionObjective": "Ensure that log storage buckets are not publicly accessible to prevent unauthorized access to sensitive log data. In addition, logs should be replicated to another cloud region to enhance availability, durability, and support disaster recovery requirements.", - "Applicability": [ - "tlp-red", - "tlp-amber", - "tlp-green" - ], - "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AA-05" + "PR.DS-5" ] }, { @@ -1881,946 +4504,28 @@ ] } ], - "Checks": [ - "cloudstorage_bucket_public_access", - "cloudstorage_bucket_uniform_bucket_level_access", - "cloudstorage_bucket_log_retention_policy_lock", - "logging_sink_created" - ] + "Checks": [] }, { - "Id": "CCC.Logging.CN05.AR02", - "Description": "When the URL of a log storage bucket's object is accessed publicly, the action MUST be denied by bucket policy.", + "Id": "CCC.DataWar.CN01.AR01", + "Description": "Attempt to access underlying database tables directly without using managed views and verify that access is denied.", "Attributes": [ { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls that restrict who can access and modify logs. ", - "Section": "CCC.Logging.CN05 Ensure Log Bucket is Not Publicly Accessible", + "FamilyName": "Data", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.DataWar.CN01 Enforce Use of Managed Views for Data Access", "SubSection": "", - "SubSectionObjective": "Ensure that log storage buckets are not publicly accessible to prevent unauthorized access to sensitive log data. In addition, logs should be replicated to another cloud region to enhance availability, durability, and support disaster recovery requirements.", + "SubSectionObjective": "Ensure that data access is provided through managed views, restricting users from accessing underlying tables directly and enforcing consistent security policies.", "Applicability": [ "tlp-red", - "tlp-amber", - "tlp-green" - ], - "Recommendation": "Configure bucket policies and access control lists (ACLs) to restrict public access. Regularly review bucket permissions to ensure no public access has been inadvertently granted. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AA-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3", - "SC-7" - ] - } - ] - } - ], - "Checks": [ - "cloudstorage_bucket_public_access", - "cloudstorage_bucket_uniform_bucket_level_access" - ] - }, - { - "Id": "CCC.Logging.CN06.AR01", - "Description": "When a single principal executes an anomalously high number of log queries, an alert MUST be generated.", - "Attributes": [ - { - "FamilyName": "Logging and Monitoring", - "FamilyDescription": "Controls that collect, alert, and retain logging-related events. ", - "Section": "CCC.Logging.CN06 Detect and Alert on Potential Log Exfiltration", - "SubSection": "", - "SubSectionObjective": "Identify and alert on anomalous data access patterns that may indicate an attempt to exfiltrate log data.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-amber" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.Logging.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-03", - "DE.CM-09" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SI-4", - "CA-7", - "AU-6" - ] - } - ] - } - ], - "Checks": [ - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", - "logging_sink_created" - ] - }, - { - "Id": "CCC.Logging.CN07.AR01", - "Description": "When an audit log event is recorded that corresponds to a modification of the logging service configuration such as disabling a log trail, deleting a log sink, or altering a log forwarding rule, an alert MUST be generated.", - "Attributes": [ - { - "FamilyName": "Logging and Monitoring", - "FamilyDescription": "Controls that collect, alert, and retain logging-related events. ", - "Section": "CCC.Logging.CN07 Detect and Alert on Log Service Tampering", - "SubSection": "", - "SubSectionObjective": "Alert when any component of the critical logging infrastructure is disabled, modified, or deleted, indicating a defense evasion attempt.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH16" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-03", - "DE.CM-09" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SI-4", - "CA-7", - "AU-6" - ] - } - ] - } - ], - "Checks": [ - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN01.AR01", - "Description": "When a request is made to read a protected bucket, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN01 Prevent Unencrypted Requests", - "SubSection": "CCC.ObjStor.C01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", - "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption that can impact data availability and integrity.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01", - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-04", - "DCS-06" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "kms_key_not_publicly_accessible", - "kms_key_rotation_enabled" - ] - }, - { - "Id": "CCC.ObjStor.CN01.AR02", - "Description": "When a request is made to read a protected object, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN01 Prevent Unencrypted Requests", - "SubSection": "CCC.ObjStor.C01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", - "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption that can impact data availability and integrity.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01", - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-04", - "DCS-06" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.ObjStor.CN01.AR03", - "Description": "When a request is made to write to a bucket, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN01 Prevent Unencrypted Requests", - "SubSection": "CCC.ObjStor.C01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", - "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption that can impact data availability and integrity.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01", - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-04", - "DCS-06" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "kms_key_not_publicly_accessible", - "kms_key_rotation_enabled", - "bigquery_dataset_cmk_encryption", - "bigquery_table_cmk_encryption", - "dataproc_encrypted_with_cmks_disabled" - ] - }, - { - "Id": "CCC.ObjStor.CN01.AR04", - "Description": "When a request is made to write to an object, the service MUST prevent any request using KMS keys not listed as trusted by the organization.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN01 Prevent Unencrypted Requests", - "SubSection": "CCC.ObjStor.C01 Prevent Requests to Buckets or Objects with Untrusted KMS Keys", - "SubSectionObjective": "Prevent any requests to object storage buckets or objects using untrusted KMS keys to protect against unauthorized data encryption that can impact data availability and integrity.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01", - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-04", - "DCS-06" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "kms_key_not_publicly_accessible", - "kms_key_rotation_enabled", - "dataproc_encrypted_with_cmks_disabled", - "compute_instance_encryption_with_csek_enabled", - "bigquery_dataset_cmk_encryption", - "bigquery_table_cmk_encryption" - ] - }, - { - "Id": "CCC.ObjStor.CN03.AR01", - "Description": "When an object storage bucket deletion is attempted, the bucket MUST be fully recoverable for a set time-frame after deletion is requested.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "CCC.ObjStor.C03 Prevent Bucket Deletion Through Irrevocable Bucket Retention Policy", - "SubSectionObjective": "Ensure that object storage bucket is not deleted after creation, and that the preventative measure cannot be unset.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock" - ] - }, - { - "Id": "CCC.ObjStor.CN03.AR02", - "Description": "When an attempt is made to modify the retention policy for an object storage bucket, the service MUST prevent the policy from being modified.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "CCC.ObjStor.C03 Prevent Bucket Deletion Through Irrevocable Bucket Retention Policy", - "SubSectionObjective": "Ensure that object storage bucket is not deleted after creation, and that the preventative measure cannot be unset.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock" - ] - }, - { - "Id": "CCC.ObjStor.CN04.AR01", - "Description": "When an object is uploaded to the object storage system, the object MUST automatically receive a default retention policy that prevents premature deletion or modification.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN04 Log All Access and Changes", - "SubSection": "CCC.ObjStor.C04 Objects have an Effective Retention Policy by Default", - "SubSectionObjective": "Ensure that all objects stored in the object storage system have a retention policy applied by default, preventing premature deletion or modification of objects and ensuring compliance with data retention regulations.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock" - ] - }, - { - "Id": "CCC.ObjStor.CN04.AR02", - "Description": "When an attempt is made to delete or modify an object that is subject to an active retention policy, the service MUST prevent the action from being completed.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN04 Log All Access and Changes", - "SubSection": "CCC.ObjStor.C04 Objects have an Effective Retention Policy by Default", - "SubSectionObjective": "Ensure that all objects stored in the object storage system have a retention policy applied by default, preventing premature deletion or modification of objects and ensuring compliance with data retention regulations.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock" - ] - }, - { - "Id": "CCC.ObjStor.CN05.AR01", - "Description": "When an object is uploaded to the object storage bucket, the object MUST be stored with a unique identifier.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN05 Prevent Access from Untrusted Entities", - "SubSection": "CCC.ObjStor.C05 Versioning is Enabled for All Objects in the Bucket", - "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.ObjStor.CN05.AR02", - "Description": "When an object is modified, the service MUST assign a new unique identifier to the modified object to differentiate it from the previous version.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN05 Prevent Access from Untrusted Entities", - "SubSection": "CCC.ObjStor.C05 Versioning is Enabled for All Objects in the Bucket", - "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.ObjStor.CN05.AR03", - "Description": "When an object is modified, the service MUST allow for recovery of previous versions of the object.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN05 Prevent Access from Untrusted Entities", - "SubSection": "CCC.ObjStor.C05 Versioning is Enabled for All Objects in the Bucket", - "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.ObjStor.CN05.AR04", - "Description": "When an object is deleted, the service MUST retain other versions of the object to allow for recovery of previous versions.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN05 Prevent Access from Untrusted Entities", - "SubSection": "CCC.ObjStor.C05 Versioning is Enabled for All Objects in the Bucket", - "SubSectionObjective": "Ensure that versioning is enabled for all objects stored in the object storage bucket to enable recovery of previous versions of objects in case of loss or corruption.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.1.4" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-28", - "CP-10" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-16" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.ObjStor.CN06.AR01", - "Description": "When an object storage bucket is accessed, the service MUST store access logs in a separate data store.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN06 Prevent Deployment in Restricted Regions", - "SubSection": "CCC.ObjStor.C06 Access Logs are Stored in a Separate Data Store", - "SubSectionObjective": "Ensure that access logs for object storage buckets are stored in a separate data store to protect against unauthorized access, tampering, or deletion of logs (Logbuckets are exempt from this requirement, but must be tlp-red).", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH07", - "CCC.TH09" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-6" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-07", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2022 A.8.15.0" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-9", - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock", - "logging_sink_created" - ] - }, - { - "Id": "CCC.ObjStor.CN02.AR01", - "Description": "When a permission set is allowed for an object in a bucket, the service MUST allow the same permission set to access all objects in the same bucket.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN02 Ensure Data Encryption at Rest for All Stored Data", - "SubSection": "CCC.ObjStor.C02 Enforce Uniform Bucket-level Access to Prevent Inconsistent Permissions", - "SubSectionObjective": "Ensure that uniform bucket-level access is enforced across all object storage buckets. This prevents the use of ad-hoc or inconsistent object-level permissions, ensuring centralized, consistent, and secure access management in accordance with the principle of least privilege.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -2831,54 +4536,38 @@ "PR.AC-4" ] }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.1" - ] - }, { "ReferenceId": "NIST_800_53", "Identifiers": [ "AC-3", "AC-6" ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-09" - ] } ] } ], - "Checks": [ - "cloudstorage_bucket_uniform_bucket_level_access" - ] + "Checks": [] }, { - "Id": "CCC.ObjStor.CN02.AR02", - "Description": "When a permission set is denied for an object in a bucket, the service MUST deny the same permission set to access all objects in the same bucket.", + "Id": "CCC.DataWar.CN02.AR01", + "Description": "Attempt to query sensitive columns without the necessary permissions and verify that access is denied or data is masked.", "Attributes": [ { - "FamilyName": "Identity and Access Management", + "FamilyName": "Data", "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.CN02 Ensure Data Encryption at Rest for All Stored Data", - "SubSection": "CCC.ObjStor.C02 Enforce Uniform Bucket-level Access to Prevent Inconsistent Permissions", - "SubSectionObjective": "Ensure that uniform bucket-level access is enforced across all object storage buckets. This prevents the use of ad-hoc or inconsistent object-level permissions, ensuring centralized, consistent, and secure access management in accordance with the principle of least privilege.", + "Section": "CCC.DataWar.CN02 Enforce Column-Level Security Policies", + "SubSection": "", + "SubSectionObjective": "Ensure that access to sensitive data columns is restricted based on user roles, preventing unauthorized access to sensitive information.", "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-red", + "tlp-amber" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -2890,9 +4579,45 @@ ] }, { - "ReferenceId": "ISO_27001", + "ReferenceId": "NIST_800_53", "Identifiers": [ - "2013 A.9.4.1" + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.DataWar.CN03.AR01", + "Description": "Attempt to query data rows that the user should not have access to and verify that access is denied or data is not returned.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.DataWar.CN03 Enforce Row-Level Security Policies", + "SubSection": "", + "SubSectionObjective": "Ensure that access to data rows is restricted based on user roles or attributes, preventing unauthorized access to specific subsets of data.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" ] }, { @@ -2901,502 +4626,12 @@ "AC-3", "AC-6" ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DCS-09" - ] - } - ] - } - ], - "Checks": [ - "cloudstorage_bucket_uniform_bucket_level_access" - ] - }, - { - "Id": "CCC.Monitor.CN01.AR01", - "Description": "When an External Monitoring system exceeds the anticipated rate of monitoring checks then Rate Limiting MUST be applied and an Audit Alert MUST be generated.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that collect, alert, and retain events from other monitoring services.", - "Section": "CCC.Monitor.CN01 Rate Limiting on External Monitoring", - "SubSection": "", - "SubSectionObjective": "Prevent DoS attacks using External Monitoring tools.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Monitor.TH03" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.IR-01", - "DE.CM-01" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-5", - "SC-7" - ] - } - ] - } - ], - "Checks": [ - "iam_audit_logs_enabled", - "logging_sink_created", - "cloudstorage_bucket_log_retention_policy_lock", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled" - ] - }, - { - "Id": "CCC.Monitor.CN02.AR01", - "Description": "When an Custom or User-Defined Metric starts to flood a collector, then a rate limit MUST be applied to reduce the network impact of traffic and an alert must triggered.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "Controls that collect, alert, and retain events from other monitoring services.", - "Section": "CCC.Monitor.CN02 Rate Limiting on Metric Generation", - "SubSection": "", - "SubSectionObjective": "Prevent Malicious Actor or misconfiguration from flooding services with metric data.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Monitor.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-01" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-5(2)", - "CA-7", - "SI-4" - ] - } - ] - } - ], - "Checks": [ - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "iam_audit_logs_enabled", - "compute_loadbalancer_logging_enabled", - "compute_network_dns_logging_enabled", - "compute_subnet_flow_logs_enabled" - ] - }, - { - "Id": "CCC.Monitor.CN03.AR01", - "Description": "When external systems have approved access to internal systems not normally available for public access then they MUST be secured to prevent unauthorised access jumping through to the internal systems and only allow access to specific internal services.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", - "Section": "CCC.Monitor.CN03 Access External Monitoring", - "SubSection": "", - "SubSectionObjective": "Control access to Synthetic monitoring solutions using API keys or Certificate based authentication to ensure they don't become an attack path, preventing monitoring systems from forging network requests to gain access to internal systems.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Monitor.TH04" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-06", - "PR.IR-01", - "PR.AA-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "apikeys_api_restrictions_configured", - "apikeys_key_exists", - "apikeys_key_rotated_in_90_days" - ] - }, - { - "Id": "CCC.Monitor.CN04.AR01", - "Description": "When monitoring dashboards display degraded services which may become potential targets then the dashboard MUST be protected from unauthorised access.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", - "Section": "CCC.Monitor.CN04 Restrict access to Monitoring Dashboards", - "SubSection": "", - "SubSectionObjective": "Control access to Monitoring Dashboards and reports to ensure they don't highlight an attack path.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Monitor.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.CM-09", - "DE.AE-03" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SI-4", - "AC-3" - ] } ] } ], "Checks": [] }, - { - "Id": "CCC.Monitor.CN05.AR01", - "Description": "When monitoring services have generated an alert, the service MUST ensure only authorised responders silence or acknowledge the alert.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", - "Section": "CCC.Monitor.CN05 Restrict access to silence or acknowledge an alert", - "SubSection": "", - "SubSectionObjective": "Ensure only a subset of users can silence or acknowledge alerts to prevent attackers hiding their activity.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH10" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.IR-01", - "PR.AA-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", - "logging_sink_created", - "iam_organization_essential_contacts_configured" - ] - }, - { - "Id": "CCC.Monitor.CN06.AR01", - "Description": "When systems push metrics or traces they MUST be authenticated for that particular type of metric or trace", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "Controls designed to prevent unauthorised access to monitoring features.", - "Section": "CCC.Monitor.CN06 Metrics pushed for authorised services only", - "SubSection": "", - "SubSectionObjective": "Use IAM to control which types of metrics or traces can be pushed by different system to avoid a compromised system pushing fabricated metrics about a different service", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.Monitor.TH05" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AA-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-5" - ] - } - ] - } - ], - "Checks": [ - "compute_instance_default_service_account_in_use", - "compute_instance_default_service_account_in_use_with_full_api_access", - "gke_cluster_no_default_service_account", - "iam_no_service_roles_at_project_level", - "iam_sa_no_administrative_privileges", - "iam_sa_no_user_managed_keys", - "iam_sa_user_managed_key_rotate_90_days", - "iam_sa_user_managed_key_unused", - "iam_service_account_unused" - ] - }, - { - "Id": "CCC.VPC.CN01.AR01", - "Description": "When a subscription is created, the subscription MUST NOT contain default network resources.", - "Attributes": [ - { - "FamilyName": "Network Security", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.VPC.CN01 Restrict Default Network Creation", - "SubSection": "", - "SubSectionObjective": "Restrict the automatic creation of default virtual networks and related resources during subscription initialization to avoid insecure default configurations and enforce custom network policies.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.VPC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-5" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "TVM-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.12.3.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-7" - ] - } - ] - } - ], - "Checks": [ - "compute_network_default_in_use" - ] - }, - { - "Id": "CCC.VPC.CN03.AR01", - "Description": "When a VPC peering connection is requested, the service MUST prevent connections from VPCs that are not explicitly allowed.", - "Attributes": [ - { - "FamilyName": "Network Security", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.VPC.CN03 Restrict VPC Peering to Authorized Accounts", - "SubSection": "", - "SubSectionObjective": "Ensure VPC peering connections are only established with explicitly authorized destinations to limit network exposure and enforce boundary controls.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.VPC.TH03" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IVS-01" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-4" - ] - } - ] - } - ], - "Checks": [] - }, - { - "Id": "CCC.VPC.CN04.AR01", - "Description": "When any network traffic goes to or from an interface in the VPC, the service MUST capture and log all relevant information.", - "Attributes": [ - { - "FamilyName": "Network Security", - "FamilyDescription": "TODO: Describe this control family", - "Section": "CCC.VPC.CN04 Enforce VPC Flow Logs on VPCs", - "SubSection": "", - "SubSectionObjective": "Ensure VPCs are configured with flow logs enabled to capture traffic information.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.VPC.TH04" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.PT-1" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.12.4.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IVS-06" - ] - } - ] - } - ], - "Checks": [ - "compute_subnet_flow_logs_enabled" - ] - }, { "Id": "CCC.Vector.CN01.AR01", "Description": "When a vector embedding is submitted for indexing, the system MUST validate that it matches expected schema, dimension, and format profiles.", @@ -3420,7 +4655,7 @@ "Identifiers": [ "CCC.Vector.TH02", "CCC.Vector.TH05", - "CCC.TH12" + "CCC.Core.TH12" ] } ], @@ -3459,7 +4694,7 @@ "Identifiers": [ "CCC.Vector.TH02", "CCC.Vector.TH04", - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -3474,11 +4709,7 @@ } ], "Checks": [ - "iam_role_kms_enforce_separation_of_duties", - "iam_role_sa_enforce_separation_of_duties", - "iam_sa_no_administrative_privileges", - "iam_no_service_roles_at_project_level", - "kms_key_not_publicly_accessible" + "iam_sa_no_administrative_privileges" ] }, { @@ -3501,7 +4732,7 @@ "ReferenceId": "CCC", "Identifiers": [ "CCC.Vector.TH03", - "CCC.TH01" + "CCC.Core.TH01" ] } ], @@ -3540,7 +4771,7 @@ "ReferenceId": "CCC", "Identifiers": [ "CCC.Vector.TH02", - "CCC.TH12" + "CCC.Core.TH12" ] } ], @@ -3576,8 +4807,8 @@ "ReferenceId": "CCC", "Identifiers": [ "CCC.Vector.TH04", - "CCC.TH09", - "CCC.TH04" + "CCC.Core.TH09", + "CCC.Core.TH04" ] } ], @@ -3591,18 +4822,7 @@ ] } ], - "Checks": [ - "iam_audit_logs_enabled", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled" - ] + "Checks": [] }, { "Id": "CCC.Vector.CN06.AR01", @@ -3626,7 +4846,7 @@ "ReferenceId": "CCC", "Identifiers": [ "CCC.Vector.TH05", - "CCC.TH06" + "CCC.Core.TH06" ] } ], @@ -3671,422 +4891,25 @@ "Checks": [] }, { - "Id": "CCC.Core.CN01.AR01", - "Description": "When a port is exposed for non-SSH network traffic, all traffic MUST include a TLS handshake AND be encrypted using TLS 1.3 or higher.", + "Id": "CCC.RDMS.CN01.AR02", + "Description": "When an attempt is made to authenticate to the database using known default credentials, the authentication attempt must fail and no access should be granted.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN01 Password Management", "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Most cloud services enable TLS 1.3 by default. Where it is not already set, ensure that your services are configured or updated accordingly. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "cloudsql_instance_ssl_connections" - ] - }, - { - "Id": "CCC.Core.CN01.AR02", - "Description": "When a port is exposed for SSH network traffic, all traffic MUST include a SSH handshake AND be encrypted using SSHv2 or higher.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", - "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Any time port 22 is exposed, ensure that it has a properly implemented SSH server with SSHv2 enabled and configured with strong ciphers. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "compute_firewall_ssh_access_from_the_internet_allowed" - ] - }, - { - "Id": "CCC.Core.CN01.AR03", - "Description": "When the service receives unencrypted traffic, then it MUST either block the request or automatically redirect it to the secure equivalent.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", - "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Review firewall, load balancer, and application configurations to ensure insecure protocols such as HTTP, FTP, and Telnet are not exposed. Where possible, implement automatic redirection to secure protocols such as HTTPS, SFTP, SSH, and regularly scan for protocol drift. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "cloudsql_instance_ssl_connections", - "cloudsql_instance_public_access", - "cloudsql_instance_public_ip", - "compute_firewall_rdp_access_from_the_internet_allowed", - "compute_firewall_ssh_access_from_the_internet_allowed", - "compute_instance_public_ip", - "cloudstorage_bucket_public_access" - ] - }, - { - "Id": "CCC.Core.CN01.AR07", - "Description": "When a port is exposed, the service MUST ensure that the protocol and service officially assigned to that port number by the IANA Service Name and Transport Protocol Port Number Registry, and no other, is run on that port.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", - "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Reference the IANA Service Name and Transport Protocol Port Number Registry for more information about correct protocol-to-port assignments. Avoid running non-standard services on well-known ports. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "compute_firewall_rdp_access_from_the_internet_allowed", - "compute_firewall_ssh_access_from_the_internet_allowed" - ] - }, - { - "Id": "CCC.Core.CN01.AR08", - "Description": "When a service transmits data using TLS, mutual TLS (mTLS) MUST be implemented to require both client and server certificate authentication for all connections.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN01 Encrypt Data for Transmission", - "SubSection": "", - "SubSectionObjective": "Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Configure mTLS for all endpoints that process or transmit sensitive data. Ensure both client and server certificates are validated and managed securely. Regularly review certificate authorities and automate certificate rotation where possible. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH02" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-03", - "CEK-04", - "IVS-03", - "IVS-07" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-02" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.1" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-8", - "SC-13" - ] - } - ] - } - ], - "Checks": [ - "cloudsql_instance_ssl_connections" - ] - }, - { - "Id": "CCC.Core.CN13.AR01", - "Description": "When a port is exposed that uses certificate-based encryption, the service MUST only use valid, unexpired certificates issued by a trusted certificate authority.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH18" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [] - }, - { - "Id": "CCC.Core.CN13.AR02", - "Description": "When a port is exposed that uses certificate-based encryption, the service MUST rotate active certificates within 180 days of issuance.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", + "SubSectionObjective": "Ensure default vendor-supplied DB administrator credentials are replaced with strong, unique passwords and that these credentials are properly managed using a secure password or secrets management solution.", "Applicability": [ + "tlp-red", "tlp-amber" ], - "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH18" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [] - }, - { - "Id": "CCC.Core.CN13.AR03", - "Description": "When a port is exposed that uses certificate-based encryption, the service MUST rotate active certificates within 90 days of issuance.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN13 Minimize Lifetime of Encryption and Authentication Certificates", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption and authentication certificates have a limited lifetime to reduce the risk of compromise and ensure the use of up-to-date security practices.", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "Track certificate expiration dates and automate certificate renewal where possible. Use certificate management tools to ensure only certificates from trusted authorities are deployed. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH18" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "apikeys_key_rotated_in_90_days", - "kms_key_rotation_enabled" - ] - }, - { - "Id": "CCC.Core.CN06.AR01", - "Description": "When the service is running, its region and availability zone MUST be included in a list of explicitly trusted or approved locations within the trust perimeter.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN06 Restrict Deployments to Trust Perimeter", - "SubSection": "", - "SubSectionObjective": "Ensure that the service and its child resources are only deployed on infrastructure in locations that are explicitly included within a defined trust perimeter.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Maintain an up-to-date list of trusted and approved regions based on organizational policies. Validate the service's deployment location is included in this list. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH03" + "CCC.RDMS.TH01" ] } ], @@ -4094,19 +4917,89 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" + "PR.AA-01" ] }, { - "ReferenceId": "CCM", + "ReferenceId": "NIST_800_53", "Identifiers": [ - "DSP-19" + "AC-2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.RDMS.CN02.AR01", + "Description": "When repeated failed login attempts are made in a short timeframe, the account must be locked out or rate-limited to prevent further login attempts.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN02 Account Lockout and Rate-Limiting", + "SubSection": "", + "SubSectionObjective": "Ensure the database enforces lockouts or rate-limiting after a specified number of failed authentication attempts. This prevents brute force or password-guessing attacks from succeeding.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.RDMS.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-1" ] }, { - "ReferenceId": "ISO_27001", + "ReferenceId": "NIST_800_53", "Identifiers": [ - "2013 A.11.1.1" + "AC-7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.RDMS.CN04.AR01", + "Description": "When there is an attempt to perform a backup or restore, then the attempt must fail with an access denied message if credentials or roles that are not explicitly authorized for backup/restore functions.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN04 Access Control for Backup and Restore Operations", + "SubSection": "", + "SubSectionObjective": "Restrict who can initiate, manage, and validate database backup or restore operations through strict role-based or least-privilege access. Prevents accidental or malicious restorations, protecting data integrity and availability.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.RDMS.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" ] }, { @@ -4118,30 +5011,30 @@ ] } ], - "Checks": [] + "Checks": [ + "iam_no_service_roles_at_project_level" + ] }, { - "Id": "CCC.Core.CN06.AR02", - "Description": "When a child resource is deployed, its region and availability zone MUST be included in a list of explicitly trusted or approved locations within the trust perimeter.", + "Id": "CCC.RDMS.CN05.AR01", + "Description": "When an attempt is made to share a snapshot with an unauthorized account, the sharing request must be denied.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN06 Restrict Deployments to Trust Perimeter", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN05 Restrict Snapshot Sharing to Authorized Accounts", "SubSection": "", - "SubSectionObjective": "Ensure that the service and its child resources are only deployed on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "SubSectionObjective": "Ensure database snapshots can only be shared with explicitly authorized accounts, thereby minimizing the risk of data exposure or exfiltration.", "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-red", + "tlp-amber" ], - "Recommendation": "Maintain an up-to-date list of trusted and approved regions based on organizational policies. Validate that child resources can only be deployed to locations included in this list. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH03" + "CCC.RDMS.TH05" ] } ], @@ -4149,24 +5042,99 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-19" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.11.1.1" + "PR.DS-10" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ + "AC-4" + ] + } + ] + } + ], + "Checks": [ + "compute_image_not_publicly_shared" + ] + }, + { + "Id": "CCC.RDMS.CN03.AR01", + "Description": "When backups are disabled, paused, or fail to run as scheduled, an alert must be triggered and logged.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.RDMS.CN03 Enforce and Monitor Automated Backups", + "SubSection": "", + "SubSectionObjective": "Ensure database backups are automatically scheduled, actively monitored, and promptly reported if any disruptions occur. This helps maintain data integrity, facilitates disaster recovery, and supports business continuity when a system failure or breach occurs.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.RDMS.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IP-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "CP-9" + ] + } + ] + } + ], + "Checks": [ + "cloudsql_instance_automated_backups" + ] + }, + { + "Id": "CCC.Build.CN01.AR01", + "Description": "Attempt to initiate a build using an unauthorized build agent and verify that the build is rejected.", + "Attributes": [ + { + "FamilyName": "Access Control", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.Build.CN01 Restrict Allowed Build Agents", + "SubSection": "", + "SubSectionObjective": "Ensure that builds are executed only on authorized build agents to maintain control over the build environment and prevent unauthorized code execution.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-3", "AC-6" ] } @@ -4176,80 +5144,25 @@ "Checks": [] }, { - "Id": "CCC.Core.CN08.AR01", - "Description": "When data is created or modified, the data MUST have a complete and recoverable duplicate that is stored in a physically separate data center.", + "Id": "CCC.Build.CN02.AR01", + "Description": "Attempt to trigger a build from an unauthorized external service or repository and verify that the build does not start.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN08 Replicate Data to Multiple Locations", + "FamilyName": "Access Control", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.Build.CN02 Restrict Allowed External Services for Build Triggers", "SubSection": "", - "SubSectionObjective": "Ensure that data is replicated across multiple physical locations to protect against data loss due to hardware failures, natural disasters, or other catastrophic events.", + "SubSectionObjective": "Ensure that builds can only be triggered by authorized external services or repositories to prevent unauthorized code execution or tampering.", "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Implement automated data replication processes to ensure that data is consistently duplicated in another region or availability zone. Regularly test data recovery from the replicated location to ensure integrity and availability. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.PT-5" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "BCR-08", - "BCR-10", - "BCR-11" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "CP-2", - "CP-10" - ] - } - ] - } - ], - "Checks": [ - "cloudsql_instance_automated_backups", - "cloudstorage_bucket_log_retention_policy_lock" - ] - }, - { - "Id": "CCC.Core.CN08.AR02", - "Description": "When data is replicated into a second location, the service MUST be able to accurately represent the replication locations, replication status, and data synchronization status.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN08 Replicate Data to Multiple Locations", - "SubSection": "", - "SubSectionObjective": "Ensure that data is replicated across multiple physical locations to protect against data loss due to hardware failures, natural disasters, or other catastrophic events.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-red", + "tlp-amber" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH06" + "CCC.Core.TH01" ] } ], @@ -4257,22 +5170,14 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.PT-5" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "BCR-08", - "BCR-10", - "BCR-11" + "PR.AC-4" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "CP-2", - "CP-10" + "AC-3", + "AC-6" ] } ] @@ -4281,15 +5186,144 @@ "Checks": [] }, { - "Id": "CCC.Core.CN09.AR01", - "Description": "When the service is operational, its logs and any child resource logs MUST NOT be accessible from the resource they record access to.", + "Id": "CCC.Build.CN03.AR01", + "Description": "Attempt to access the build environment from an external network and verify that access is denied.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.Build.CN03 Deny External Network Access for Build Environments", "SubSection": "", - "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "SubSectionObjective": "Ensure that build environments do not have external network access to prevent unauthorized external access and data exfiltration.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH02", + "CCC.Core.TH05" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-5" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7", + "SC-5" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.CntrReg.CN01.AR01", + "Description": "Attempt to push an artifact with known vulnerabilities to the registry and observe if it is flagged or rejected by the vulnerability scanning process.", + "Attributes": [ + { + "FamilyName": "Risk Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.CntrReg.CN01 Implement Vulnerability Scanning for Artifacts", + "SubSection": "", + "SubSectionObjective": "Ensure that container images and artifacts stored in the container registry are scanned for vulnerabilities to identify and remediate security issues before deployment.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.CntrReg.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "ID.RA-1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "RA-5", + "SI-5" + ] + } + ] + } + ], + "Checks": [ + "artifacts_container_analysis_enabled", + "gcr_container_scanning_enabled" + ] + }, + { + "Id": "CCC.CntrReg.CN02.AR01", + "Description": "Confirm that artifacts older than the specified retention period are automatically deleted from the registry.", + "Attributes": [ + { + "FamilyName": "Data Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.CntrReg.CN02 Implement Cleanup Policies for Artifacts", + "SubSection": "", + "SubSectionObjective": "Ensure that unused or outdated artifacts are cleaned up according to defined policies to manage storage effectively and reduce security risks associated with outdated versions.", + "Applicability": [ + "tlp-red", + "tlp-amber" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.Core.TH14" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IP-6" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-12" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.IAM.CN01.AR01", + "Description": "When an identity policy for a non-administrative principal is evaluated, it MUST NOT grant permissions for creating credentials or generating temporary session tokens.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN01 Restrict IAM User Credentials Creation", + "SubSection": "", + "SubSectionObjective": "Prevent non-administrative principals from creating new long-lived credentials like access keys or generating temporary session tokens. This blocks a common privilege escalation and persistence vector.", "Applicability": [ "tlp-clear", "tlp-green", @@ -4301,9 +5335,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07", - "CCC.TH09", - "CCC.TH04" + "CCC.IAM.TH03" ] } ], @@ -4311,56 +5343,48 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-6" + "PR.AA-05" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AU-9" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-02", - "LOG-04", - "LOG-09" + "AC-2", + "AC-3", + "AC-5", + "AC-6" ] } ] } ], "Checks": [ - "logging_sink_created", - "cloudstorage_bucket_log_retention_policy_lock", - "iam_audit_logs_enabled" + "iam_sa_no_user_managed_keys", + "iam_no_service_roles_at_project_level" ] }, { - "Id": "CCC.Core.CN09.AR02", - "Description": "When the service is operational, disabling the logs for the service or its child resources MUST NOT be possible without also disabling the corresponding resource.", + "Id": "CCC.IAM.CN01.AR02", + "Description": "When a non-administrative principal attempts to create new credentials or a temporary session token, the service MUST deny the action.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN01 Restrict IAM User Credentials Creation", "SubSection": "", - "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", + "SubSectionObjective": "Prevent non-administrative principals from creating new long-lived credentials like access keys or generating temporary session tokens. This blocks a common privilege escalation and persistence vector.", "Applicability": [ "tlp-clear", "tlp-green", "tlp-amber", "tlp-red" ], - "Recommendation": "No normal business operations should disable logs, as this could indicate an attempt to cover up unauthorized access. Ensure that logging mechanisms are tightly integrated with service operations, so that logging cannot be disabled without stopping the service itself. ", + "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH07", - "CCC.TH09", - "CCC.TH04" + "CCC.IAM.TH03" ] } ], @@ -4368,21 +5392,589 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-6" + "PR.AA-05" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AU-9" + "AC-2", + "AC-3", + "AC-5", + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_sa_no_user_managed_keys", + "iam_no_service_roles_at_project_level" + ] + }, + { + "Id": "CCC.IAM.CN02.AR01", + "Description": "When an identity policy for a non-administrative principal is evaluated, it MUST NOT grant permissions for creating, updating, or attaching policies.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN02 Restrict IAM Policies Modification", + "SubSection": "", + "SubSectionObjective": "Ensure that only designated administrative accounts have the ability to create, modify, or attach policies that define permissions for other identities.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-05" ] }, { - "ReferenceId": "CCM", + "ReferenceId": "NIST_800_53", "Identifiers": [ - "LOG-02", - "LOG-04", - "LOG-09" + "AC-2", + "AC-3", + "AC-5", + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_no_service_roles_at_project_level", + "iam_sa_no_administrative_privileges" + ] + }, + { + "Id": "CCC.IAM.CN02.AR02", + "Description": "When a non-administrative principal attempts to create, update, or attach policies, the service MUST deny the action.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN02 Restrict IAM Policies Modification", + "SubSection": "", + "SubSectionObjective": "Ensure that only designated administrative accounts have the ability to create, modify, or attach policies that define permissions for other identities.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3", + "AC-5", + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_no_service_roles_at_project_level", + "iam_sa_no_administrative_privileges" + ] + }, + { + "Id": "CCC.IAM.CN03.AR01", + "Description": "When a policy is created or updated that grants a principal permission to assume a role or impersonate a service identity, the principal MUST NOT contain a wildcard or be public/anonymous.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN03 Restrict Role Assumption / Delegation", + "SubSection": "", + "SubSectionObjective": "Limit which principals can assume a role or impersonate a service identity to only those required. This prevents unintended cross-account or public access by securing the \"who can act as this identity\" boundary.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_role_sa_enforce_separation_of_duties", + "iam_no_service_roles_at_project_level" + ] + }, + { + "Id": "CCC.IAM.CN03.AR02", + "Description": "When an external or unauthenticated principal tries to assume a role or impersonate a service identity, the service MUST deny the action.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN03 Restrict Role Assumption / Delegation", + "SubSection": "", + "SubSectionObjective": "Limit which principals can assume a role or impersonate a service identity to only those required. This prevents unintended cross-account or public access by securing the \"who can act as this identity\" boundary.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_role_sa_enforce_separation_of_duties", + "iam_no_service_roles_at_project_level" + ] + }, + { + "Id": "CCC.IAM.CN04.AR01", + "Description": "When an IAM policy is created or updated, it MUST NOT contain allow statements with wildcard permissions, unless the statement is restricted by a condition.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN04 Restrict Wildcard Usage in IAM Policies", + "SubSection": "", + "SubSectionObjective": "Limit the use of wildcard permissions in IAM policies to prevent overly broad access from being granted by default.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01", + "CCC.IAM.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-6" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3", + "AC-6" + ] + } + ] + } + ], + "Checks": [ + "iam_sa_no_administrative_privileges" + ] + }, + { + "Id": "CCC.IAM.CN05.AR01", + "Description": "When a new cloud account is provisioned, a password policy MUST be configured for IAM users following the minimum PCI DSS v4.0.1 configurations.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "Controls that restrict who can access and modify IAM resources.", + "Section": "CCC.IAM.CN05 Strong Password Policies for IAM Users", + "SubSection": "", + "SubSectionObjective": "Ensure that the password policies for IAM users have strong configurations.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "When a new cloud account is provisioned, a password policy must be configured for all IAM users to align with the minimum requirements defined in PCI DSS v4.0.1. This includes, at a minimum: strength: 0 # Not yet specified - reference-id: A password length of at least 12 characters. strength: 0 # Not yet specified - reference-id: A mix of upper- and lower-case letters, numbers, and special characters. strength: 0 # Not yet specified - reference-id: Prevention of the use of previously used passwords (password history). strength: 0 # Not yet specified - reference-id: Password expiration at a defined interval (e.g., every 90 days). strength: 0 # Not yet specified - reference-id: Account lockout after a defined number of failed login attempts.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "IA-5" + ] + }, + { + "ReferenceId": "PCI-DSS", + "Identifiers": [ + "8.3.9", + "8.6.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.IAM.CN06.AR01", + "Description": "When a static credential such as an access key has existed for 90 days or more, it MUST be rotated.", + "Attributes": [ + { + "FamilyName": "Identity Provisioning and Lifecycle", + "FamilyDescription": "Controls related to the provisioning and lifecycle of IAM identities.", + "Section": "CCC.IAM.CN06 Maximum Age for Long-Term Static Credentials", + "SubSection": "", + "SubSectionObjective": "Ensure that long-lived static credentials like access keys are programmatically rotated within a defined time period to limit the window of opportunity if compromised.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "When a static credential such as an access key has existed for 90 days or more, it must be automatically rotated to reduce the risk of compromise due to long-term exposure. Organizations should implement automated checks to identify aging credentials and enforce rotation policies. Additionally, access key usage should be regularly monitored, and credentials that are no longer in use should be deactivated or deleted promptly. Where possible, prefer temporary, short-lived credentials over long-lived static ones to further minimize risk.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH09", + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2" + ] + } + ] + } + ], + "Checks": [ + "iam_sa_user_managed_key_rotate_90_days" + ] + }, + { + "Id": "CCC.IAM.CN07.AR01", + "Description": "When a user account is disabled or deleted in the organization's IdP, the corresponding cloud identity and its access policies MUST be disabled or deleted within 24 hours.", + "Attributes": [ + { + "FamilyName": "Identity Provisioning and Lifecycle", + "FamilyDescription": "Controls related to the provisioning and lifecycle of IAM identities.", + "Section": "CCC.IAM.CN07 Automate Identity De-provisioning", + "SubSection": "", + "SubSectionObjective": "Ensure that when an identity is terminated in the central Identity Provider (IdP), ts corresponding access to cloud resources is revoked automatically.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH10", + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2" + ] + } + ] + } + ], + "Checks": [ + "iam_service_account_unused", + "iam_sa_user_managed_key_unused" + ] + }, + { + "Id": "CCC.IAM.CN08.AR01", + "Description": "When an IAM user has credentials, such as passwords or access keys, that have not been used for 90 days or more, the unused credentials MUST be removed or deactivated.", + "Attributes": [ + { + "FamilyName": "Identity Provisioning and Lifecycle", + "FamilyDescription": "Controls related to the provisioning and lifecycle of IAM identities.", + "Section": "CCC.IAM.CN08 Maximum Age for Unused Credentials", + "SubSection": "", + "SubSectionObjective": "Ensure that unused IAM credentals are removed to reduce exposure in the event of potential compromise.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "IAM user credentials (such as passwords or access keys) that have not been used for 90 days or more must be automatically removed or deactivated.", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH11", + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2" + ] + } + ] + } + ], + "Checks": [ + "iam_sa_user_managed_key_unused", + "iam_service_account_unused" + ] + }, + { + "Id": "CCC.IAM.CN09.AR01", + "Description": "When a human user accesses the cloud environment, they MUST authenticate through the organization's federated IdP via a standard protocol (e.g., SAML, OIDC).", + "Attributes": [ + { + "FamilyName": "Identity Provisioning and Lifecycle", + "FamilyDescription": "Controls related to the provisioning and lifecycle of IAM identities.", + "Section": "CCC.IAM.CN09 Enforce Federated Single Sign-On (SSO) for Human Users", + "SubSection": "", + "SubSectionObjective": "Ensure that all human users must authenticate through a central, federated Identity Provider (IdP) to access the cloud environment. This eliminates cloud-native user accounts with long-lived passwords, centralizes authentication controls, and simplifies lifecycle management.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01", + "CCC.IAM.TH09" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "IA-2" + ] + } + ] + } + ], + "Checks": [ + "compute_project_os_login_enabled" + ] + }, + { + "Id": "CCC.IAM.CN10.AR01", + "Description": "When suspicious API requests are detected, real time alerts MUST be generated to notify security personnel.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain IAM-related events.", + "Section": "CCC.IAM.CN10 Alert On Anomalous Behaviour", + "SubSection": "", + "SubSectionObjective": "Ensure that logs and associated alerts are generated when anomalous API requests are made by a single identity, such as API requests commonly associated with privilege escalation tactics, originating from an external or malicious IP address or performed by a previously dormant identity, which may indicate that credentals may be compromised, as well as for password brute-force attempts and account lockouts.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4", + "SI-5", + "AC-2" + ] + } + ] + } + ], + "Checks": [ + "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", + "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled" + ] + }, + { + "Id": "CCC.IAM.CN10.AR02", + "Description": "When suspicious API requests are detected, the associated events MUST be logged, including the source details, time, and nature of the activity.", + "Attributes": [ + { + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain IAM-related events.", + "Section": "CCC.IAM.CN10 Alert On Anomalous Behaviour", + "SubSection": "", + "SubSectionObjective": "Ensure that logs and associated alerts are generated when anomalous API requests are made by a single identity, such as API requests commonly associated with privilege escalation tactics, originating from an external or malicious IP address or performed by a previously dormant identity, which may indicate that credentals may be compromised, as well as for password brute-force attempts and account lockouts.", + "Applicability": [ + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.IAM.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-4", + "SI-5", + "AC-2" ] } ] @@ -4390,78 +5982,21 @@ ], "Checks": [ "iam_audit_logs_enabled", - "logging_sink_created", - "cloudstorage_bucket_log_retention_policy_lock", - "compute_subnet_flow_logs_enabled", - "compute_loadbalancer_logging_enabled", - "compute_network_dns_logging_enabled" + "logging_sink_created" ] }, { - "Id": "CCC.Core.CN09.AR03", - "Description": "When the service is operational, any attempt to redirect logs for the service or its child resources MUST NOT be possible without halting operation of the corresponding resource and publishing corresponding events to monitored channels.", + "Id": "CCC.IAM.CN11.AR01", + "Description": "When a cloud account or organization is provisioned, the native automated access and usage analysis services MUST be enabled to continuously monitor for external or public access to resources, and unused access.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN09 Ensure Integrity of Access Logs", + "FamilyName": "Logging and Monitoring", + "FamilyDescription": "Controls that collect, alert, and retain IAM-related events.", + "Section": "CCC.IAM.CN11 Enable Continuous IAM Access and Usage Analysis", "SubSection": "", - "SubSectionObjective": "Ensure that access logs are always recorded to an external location that cannot be manipulated from the context of the service(s) it contains logs for.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "No normal business operations should result in the redirection of logs, as this could indicate an attempt to cover up unauthorized access. Ensure that logging configurations are immutable during service operation so that any changes require stopping the service and publishing corresponding events to monitored channels. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH07", - "CCC.TH09", - "CCC.TH04" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-6" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-9" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-02", - "LOG-04", - "LOG-09" - ] - } - ] - } - ], - "Checks": [ - "logging_sink_created", - "cloudstorage_bucket_log_retention_policy_lock" - ] - }, - { - "Id": "CCC.Core.CN10.AR01", - "Description": "When data is replicated, the service MUST ensure that replication only occurs to destinations that are explicitly included within the defined trust perimeter.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN10 Restrict Data Replication to Trust Perimeter", - "SubSection": "", - "SubSectionObjective": "Ensure that data is only replicated on infrastructure in locations that are explicitly included within a defined trust perimeter.", + "SubSectionObjective": "Enable and configure the cloud provider's native access and usage analysis services to continuously monitor for external access paths and internal unused access.", "Applicability": [ + "tlp-clear", "tlp-green", "tlp-amber", "tlp-red" @@ -4471,7 +6006,978 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH04" + "CCC.IAM.TH02", + "CCC.IAM.TH10", + "CCC.IAM.TH11" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "ID.RA-01", + "ID.IM-01" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "CA-7", + "RA-5" + ] + } + ] + } + ], + "Checks": [ + "iam_account_access_approval_enabled", + "iam_cloud_asset_inventory_enabled" + ] + }, + { + "Id": "CCC.GenAI.CN01.AR01", + "Description": "Untrusted input such as user queries, RAG data or tool output MUST be validated before it is passed to a GenAI model.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN01 Model Input Filtering and Sanitisation", + "SubSection": "", + "SubSectionObjective": "Inspect and validate input before it is passed to a GenAI model in order to filter or sanitise adversarial queries and prevent sensitive data leakage.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-003", + "AIR-PREV-017", + "AIR-PREV-002", + "AIR-DET-001" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Input Validation and Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0020", + "AML.M0021", + "AML.M0015" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN01.AR02", + "Description": "If malicious patterns such as prompt injection or sensitive data are detected during input validation, the input MUST be blocked or sanitised.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN01 Model Input Filtering and Sanitisation", + "SubSection": "", + "SubSectionObjective": "Inspect and validate input before it is passed to a GenAI model in order to filter or sanitise adversarial queries and prevent sensitive data leakage.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-003", + "AIR-PREV-017", + "AIR-PREV-002", + "AIR-DET-001" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Input Validation and Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0020", + "AML.M0021", + "AML.M0015" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN02.AR01", + "Description": "GenAI model output MUST be validated for format conformance, malicious patterns, sensitive data and inapropriate content before being passed to users, application or plugins.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN02 Model Output Filtering and Sanitisation", + "SubSection": "", + "SubSectionObjective": "Inspect and validate GenAI model output before passing it to users, applications or plugins in order to filter or sanitise insecure or unreliable output and prevent sensitive data leakage.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH03", + "CCC.GenAI.TH04", + "CCC.GenAI.TH05", + "CCC.GenAI.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-003", + "AIR-PREV-017", + "AIR-PREV-002", + "AIR-DET-001" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Output Validation and Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0020", + "AML.M0002" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN02.AR02", + "Description": "In the event of policy violations, the AI-generated content MUST be redacted, encoded or rejected.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN02 Model Output Filtering and Sanitisation", + "SubSection": "", + "SubSectionObjective": "Inspect and validate GenAI model output before passing it to users, applications or plugins in order to filter or sanitise insecure or unreliable output and prevent sensitive data leakage.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH03", + "CCC.GenAI.TH04", + "CCC.GenAI.TH05", + "CCC.GenAI.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-003", + "AIR-PREV-017", + "AIR-PREV-002", + "AIR-DET-001" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Output Validation and Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0020", + "AML.M0002" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN03.AR01", + "Description": "When data is designated for model training or RAG ingestion, then its source MUST be explicitly approved and its provenance documented.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN03 Data Provenance and Source Vetting", + "SubSection": "", + "SubSectionObjective": "Ensure that all data for training, fine-tuning or RAG comes from trusted, approved sources and is authorised for the intended purposes in order to prevent the initial introduction of malicious content or leaked sensitive data.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH02", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-006" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Training Data Management" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0025" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN03.AR02", + "Description": "Data from unvetted sources MUST NOT be used in production systems.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN03 Data Provenance and Source Vetting", + "SubSection": "", + "SubSectionObjective": "Ensure that all data for training, fine-tuning or RAG comes from trusted, approved sources and is authorised for the intended purposes in order to prevent the initial introduction of malicious content or leaked sensitive data.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH02", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-006" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Training Data Management" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0025" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN04.AR01", + "Description": "When data is ingested for training, fine-tuning or conversion to vector embeddings, it MUST be validated for sensitive information or malicious content.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN04 Sanitisation of Ingested Data", + "SubSection": "", + "SubSectionObjective": "Validate and sanitise all data ingested by GenAI systems from extenal sources or internal knowledge bases, whether for training, conversion to vector embeddings, or real-time retireval, in order to remove or redact poisoned or sensitive data before further processing.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH02", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-002" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Training Data Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0007" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN04.AR02", + "Description": "If sensitive data or malicious content is detected, it must be rejected, redacted or flagged for manual review.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN04 Sanitisation of Ingested Data", + "SubSection": "", + "SubSectionObjective": "Validate and sanitise all data ingested by GenAI systems from extenal sources or internal knowledge bases, whether for training, conversion to vector embeddings, or real-time retireval, in order to remove or redact poisoned or sensitive data before further processing.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH02", + "CCC.GenAI.TH03" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-002" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Training Data Sanitization" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0007" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN05.AR01", + "Description": "When a RAG-enabled system generates a response containing information retrieved from its knowledge base, then the response MUST include a verifiable citation that links back to the specific source document.", + "Attributes": [ + { + "FamilyName": "Data", + "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters.", + "Section": "CCC.GenAI.CN05 Citations and Source Traceability", + "SubSection": "", + "SubSectionObjective": "Require the GenAI system to provide citations or direct links back to the source documents used to generate a response, in to enhance the transparency, trustworthiness, and verifiability of AI-generated content.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH09", + "CCC.GenAI.TH04" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-DET-013" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN06.AR01", + "Description": "When an LLM invokes an external tool (e.g., an API, a plugin), then the tool MUST operate with the least privileges required for performing its intended functionality.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration.", + "Section": "CCC.GenAI.CN06 Least Privilege for Plugins", + "SubSection": "", + "SubSectionObjective": "Restricts the permissions of any external tools the GenAI system can call to limit the potential damage if an agent is coerced to perform unintended actions or vulnerabilities in the tools are exploited.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH07", + "CCC.GenAI.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Agent Permissions" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN07.AR01", + "Description": "When an application makes an API call to a foundational model in a production environment, then it MUST specify an explicit version identifier.", + "Attributes": [ + { + "FamilyName": "Configuration Management", + "FamilyDescription": "The Configuration Management control family involves establishing, maintaining and monitoring the configuration of the service and related applications and infrastructure to ensure consistency, secure defaults and compliance.", + "Section": "CCC.GenAI.CN07 Model Version Pinning", + "SubSection": "", + "SubSectionObjective": "Mandate that applications are locked (\"pinned\") to a specific, tested version of a foundational model to prevent unexpected behaviour changes introduced by provider-side updates.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH10" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-010" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN08.AR01", + "Description": "When a new AI model is considered for production deployment, it MUST undergo a formal red teaming and quality assurance review.", + "Attributes": [ + { + "FamilyName": "Model Assurance and Evaluation", + "FamilyDescription": "The Model Assurance and Evaluation control family encompasses the proactiveand continuous processes of testing and validating the AI model's behavior to ensure it aligns with safety, ethical, and quality standards.", + "Section": "CCC.GenAI.CN08 Quality Control and Red Teaming", + "SubSection": "", + "SubSectionObjective": "Establish a formal program for quality evaluation and adversarial testing (red teaming) to ensure GenAI system meet all business, quality, security and compliance requirements before getting deployed into production environments.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH02", + "CCC.GenAI.TH04", + "CCC.GenAI.TH08", + "CCC.GenAI.TH10" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-005" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Adversarial Training and Testing", + "Red Teaming", + "Product Governance" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0008" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.GenAI.CN08.AR02", + "Description": "If model quality review or red teaming identifies an issue that exceeds the organization's risk tolerance, the model MUST NOT be deployed until the issue is remediated.", + "Attributes": [ + { + "FamilyName": "Model Assurance and Evaluation", + "FamilyDescription": "The Model Assurance and Evaluation control family encompasses the proactiveand continuous processes of testing and validating the AI model's behavior to ensure it aligns with safety, ethical, and quality standards.", + "Section": "CCC.GenAI.CN08 Quality Control and Red Teaming", + "SubSection": "", + "SubSectionObjective": "Establish a formal program for quality evaluation and adversarial testing (red teaming) to ensure GenAI system meet all business, quality, security and compliance requirements before getting deployed into production environments.", + "Applicability": [ + "tlp-clear", + "tlp-green", + "tlp-amber", + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.GenAI.TH01", + "CCC.GenAI.TH02", + "CCC.GenAI.TH04", + "CCC.GenAI.TH08", + "CCC.GenAI.TH10" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "FINOS-AIGF", + "Identifiers": [ + "AIR-PREV-005" + ] + }, + { + "ReferenceId": "SAIF", + "Identifiers": [ + "Adversarial Training and Testing", + "Red Teaming", + "Product Governance" + ] + }, + { + "ReferenceId": "MITRE-ATLAS", + "Identifiers": [ + "AML.M0008" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN01.AR01", + "Description": "Verify that only authorized users can access MLDE resources, and that access modes are properly defined and enforced.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN01 Define Access Mode for ML Development Environments", + "SubSection": "", + "SubSectionObjective": "Ensure that access to Machine Learning Development Environment (MLDE) resources is strictly defined and controlled. Only authorized users with appropriate permissions can access these environments, mitigating the risk of unauthorized access, data leakage, or service disruption.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01", + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.1.1", + "2013 A.9.2.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-2", + "AC-3" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-01", + "IAM-02" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN03.AR01", + "Description": "Verify that root access is disabled on MLDE instances containing sensitive data.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN03 Disable Root Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent users from obtaining root access on MLDE instances to reduce the risk of unauthorized system modifications and potential security breaches.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08", + "IAM-12" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.2.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN03.AR02", + "Description": "For MLDE instances without sensitive data, ensure that root access is only enabled when necessary and properly authorized.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN03 Disable Root Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent users from obtaining root access on MLDE instances to reduce the risk of unauthorized system modifications and potential security breaches.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08", + "IAM-12" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.2.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN04.AR01", + "Description": "Verify that terminal access is disabled on MLDE instances containing sensitive data.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN04 Disable Terminal Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent users from accessing the terminal on MLDE instances to limit the risk of unauthorized commands and potential system compromise.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.2.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN04.AR02", + "Description": "For MLDE instances without sensitive data, ensure that terminal access is only enabled when necessary and properly authorized.", + "Attributes": [ + { + "FamilyName": "Identity and Access Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN04 Disable Terminal Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent users from accessing the terminal on MLDE instances to limit the risk of unauthorized commands and potential system compromise.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-08" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.2.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN02.AR01", + "Description": "Confirm that file download functionality is disabled on MLDE instances containing sensitive data.", + "Attributes": [ + { + "FamilyName": "Data Protection", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN02 Disable File Downloads on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent unauthorized file downloads from MLDE instances to protect sensitive data from being exfiltrated.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH02", + "CCC.Core.TH02" ] } ], @@ -4485,14 +6991,21 @@ { "ReferenceId": "CCM", "Identifiers": [ - "DSP-10", - "DSP-19" + "DSI-05", + "DSI-07" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.13.2.1" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "AC-4" + "SC-7", + "SC-8" ] } ] @@ -4501,26 +7014,28 @@ "Checks": [] }, { - "Id": "CCC.Core.CN02.AR01", - "Description": "When data is stored, it MUST be encrypted using the latest industry-standard encryption methods.", + "Id": "CCC.MLDE.CN02.AR02", + "Description": "For MLDE instances without sensitive data, ensure that file downloads are monitored and logged.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN02 Encrypt Data for Storage", + "FamilyName": "Data Protection", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN02 Disable File Downloads on MLDE Instances", "SubSection": "", - "SubSectionObjective": "Ensure that all data stored is encrypted at rest using strong encryption algorithms.", + "SubSectionObjective": "Prevent unauthorized file downloads from MLDE instances to protect sensitive data from being exfiltrated.", "Applicability": [ + "tlp-red", + "tlp-amber", "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-clear" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.MLDE.TH02", + "CCC.Core.TH02" ] } ], @@ -4528,110 +7043,46 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" + "PR.DS-5" ] }, { "ReferenceId": "CCM", "Identifiers": [ - "CEK-03", - "CEK-04", - "UEM-08", - "DSP-17" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-13", - "SC-28" - ] - } - ] - } - ], - "Checks": [ - "compute_instance_encryption_with_csek_enabled", - "dataproc_encrypted_with_cmks_disabled", - "bigquery_dataset_cmk_encryption", - "bigquery_table_cmk_encryption", - "kms_key_not_publicly_accessible", - "kms_key_rotation_enabled" - ] - }, - { - "Id": "CCC.Core.CN11.AR01", - "Description": "When encryption keys are used, the service MUST verify that all encryption keys use the latest industry-standard cryptographic algorithms.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH16" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" + "DSI-05", + "DSI-07" ] }, { "ReferenceId": "ISO_27001", "Identifiers": [ - "2013 A.10.1.2" + "2013 A.13.2.1" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "SC-12", - "SC-17" + "SC-7", + "SC-8" ] } ] } ], - "Checks": [ - "kms_key_rotation_enabled", - "dataproc_encrypted_with_cmks_disabled", - "bigquery_dataset_cmk_encryption", - "bigquery_table_cmk_encryption", - "kms_key_not_publicly_accessible" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN11.AR02", - "Description": "When encryption keys are used, the service MUST rotate active keys within 180 days of issuance.", + "Id": "CCC.MLDE.CN05.AR01", + "Description": "Verify that only approved VM and container images can be selected when creating MLDE instances.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", + "FamilyName": "Configuration Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN05 Restrict Environment Options on MLDE Instances", "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "SubSectionObjective": "Limit the virtual machine and container image options available when creating new MLDE instances to approved and secure configurations.", "Applicability": [ + "tlp-red", "tlp-amber" ], "Recommendation": "", @@ -4639,7 +7090,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH16" + "CCC.MLDE.TH04" ] } ], @@ -4647,53 +7098,43 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" + "PR.IP-1" ] }, { "ReferenceId": "CCM", "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" + "TVM-02" ] }, { "ReferenceId": "ISO_27001", "Identifiers": [ - "2013 A.10.1.2" + "2013 A.12.5.1" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "SC-12", - "SC-17" + "CM-2" ] } ] } ], - "Checks": [ - "kms_key_rotation_enabled", - "kms_key_not_publicly_accessible", - "dataproc_encrypted_with_cmks_disabled", - "bigquery_dataset_cmk_encryption", - "bigquery_table_cmk_encryption" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN11.AR03", - "Description": "When encrypting data, the service MUST verify that customer-managed encryption keys (CMEKs) are used.", + "Id": "CCC.MLDE.CN05.AR02", + "Description": "Attempt to create an MLDE instance with an unapproved image and confirm that it is denied.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "", - "SubSection": "CCC.Core.CN11 Protect Encryption Keys", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "FamilyName": "Configuration Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN05 Restrict Environment Options on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Limit the virtual machine and container image options available when creating new MLDE instances to approved and secure configurations.", "Applicability": [ - "tlp-amber", "tlp-red" ], "Recommendation": "", @@ -4701,7 +7142,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH16" + "CCC.MLDE.TH04" ] } ], @@ -4709,51 +7150,371 @@ { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.DS-1" + "PR.IP-1" ] }, { "ReferenceId": "CCM", "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" + "TVM-02" ] }, { "ReferenceId": "ISO_27001", "Identifiers": [ - "2013 A.10.1.2" + "2013 A.12.5.1" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "SC-12", - "SC-17" + "CM-2" ] } ] } ], - "Checks": [ - "bigquery_dataset_cmk_encryption", - "bigquery_table_cmk_encryption", - "dataproc_encrypted_with_cmks_disabled", - "kms_key_not_publicly_accessible", - "kms_key_rotation_enabled" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN11.AR04", - "Description": "When encryption keys are accessed, the service MUST verify that access to encryption keys is restricted to authorized personnel and services, following the principle of least privilege.", + "Id": "CCC.MLDE.CN06.AR01", + "Description": "Verify that automatic scheduled upgrades are enabled on user-managed MLDE instances containing sensitive data.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", + "FamilyName": "Vulnerability Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN06 Require Automatic Scheduled Upgrades on User-Managed MLDE Instances", "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "SubSectionObjective": "Ensure that MLDE instances are kept up-to-date with the latest security patches by enforcing automatic scheduled upgrades.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH04", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IP-12" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "TVM-01", + "TVM-02" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.12.6.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN06.AR02", + "Description": "Ensure that the upgrade schedule is appropriately configured and does not interfere with critical operations.", + "Attributes": [ + { + "FamilyName": "Vulnerability Management", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN06 Require Automatic Scheduled Upgrades on User-Managed MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Ensure that MLDE instances are kept up-to-date with the latest security patches by enforcing automatic scheduled upgrades.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH04", + "CCC.Core.TH06" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.IP-12" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "TVM-01", + "TVM-02" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.12.6.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SI-2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN07.AR01", + "Description": "Verify that MLDE instances containing sensitive data cannot be accessed via public IP addresses.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN07 Restrict Public IP Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent public IP access to MLDE instances to reduce exposure to the internet and enhance security.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH02", + "CCC.VPC.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "SEF-05" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.13.1.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN07.AR02", + "Description": "For MLDE instances without sensitive data requiring public access, ensure that appropriate security controls are in place and access is approved.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN07 Restrict Public IP Access on MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Prevent public IP access to MLDE instances to reduce exposure to the internet and enhance security.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH02", + "CCC.VPC.TH02" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-3" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "SEF-05" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.13.1.1" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "SC-7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN08.AR01", + "Description": "Verify that MLDE instances containing sensitive data can only be deployed in approved virtual networks with appropriate security controls.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN08 Restrict Virtual Networks for MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Limit the virtual networks that can be used when creating new MLDE instances to ensure they are deployed within approved and secure network environments.", + "Applicability": [ + "tlp-red" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01", + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-12" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.1.2" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.MLDE.CN08.AR02", + "Description": "Ensure that MLDE instances without sensitive data are deployed in networks that meet organizational security standards.", + "Attributes": [ + { + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.MLDE.CN08 Restrict Virtual Networks for MLDE Instances", + "SubSection": "", + "SubSectionObjective": "Limit the virtual networks that can be used when creating new MLDE instances to ensure they are deployed within approved and secure network environments.", + "Applicability": [ + "tlp-red", + "tlp-amber", + "tlp-green", + "tlp-clear" + ], + "Recommendation": "", + "SectionThreatMappings": [ + { + "ReferenceId": "CCC", + "Identifiers": [ + "CCC.MLDE.TH01", + "CCC.Core.TH01" + ] + } + ], + "SectionGuidelineMappings": [ + { + "ReferenceId": "NIST-CSF", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "CCM", + "Identifiers": [ + "IAM-12" + ] + }, + { + "ReferenceId": "ISO_27001", + "Identifiers": [ + "2013 A.9.1.2" + ] + }, + { + "ReferenceId": "NIST_800_53", + "Identifiers": [ + "AC-6" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC.Message.CN01.AR01", + "Description": "Attempt to publish a message without using a customer-managed encryption key and verify that the message is rejected or not stored.", + "Attributes": [ + { + "FamilyName": "Encryption", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.Message.CN01 Use Customer-Managed Encryption Keys (CMEK) for Messages", + "SubSection": "", + "SubSectionObjective": "Ensure that messages are encrypted using customer-managed encryption keys (CMEK) to provide enhanced control over encryption processes and keys, meeting compliance and security requirements.", "Applicability": [ "tlp-clear", "tlp-green", @@ -4765,7 +7526,7 @@ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH16" + "CCC.Core.TH01" ] } ], @@ -4776,310 +7537,53 @@ "PR.DS-1" ] }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.2" - ] - }, { "ReferenceId": "NIST_800_53", "Identifiers": [ "SC-12", - "SC-17" + "SC-13" ] } ] } ], - "Checks": [ - "kms_key_not_publicly_accessible", - "kms_key_rotation_enabled", - "bigquery_dataset_cmk_encryption", - "bigquery_table_cmk_encryption", - "dataproc_encrypted_with_cmks_disabled" - ] + "Checks": [] }, { - "Id": "CCC.Core.CN11.AR05", - "Description": "When encryption keys are used, the service MUST rotate active keys within 365 days of issuance.", + "Id": "CCC.SvlsComp.CN01.AR01", + "Description": "Attempt to access the serverless function over the public internet and verify that access is denied.", "Attributes": [ { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", + "FamilyName": "Network Security", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.SvlsComp.CN01 Enforce Use of Private Endpoints for Serverless Function", "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", + "SubSectionObjective": "Ensure that the serverless function is accessible only through a private endpoint, allowing it to communicate securely within a virtual private network and preventing unauthorized external access.", "Applicability": [ - "tlp-clear", - "tlp-green" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH16" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-12", - "SC-17" - ] - } - ] - } - ], - "Checks": [ - "kms_key_rotation_enabled", - "kms_key_not_publicly_accessible", - "dataproc_encrypted_with_cmks_disabled", - "bigquery_dataset_cmk_encryption", - "bigquery_table_cmk_encryption" - ] - }, - { - "Id": "CCC.Core.CN11.AR06", - "Description": "When encryption keys are used, the service MUST rotate active keys within 90 days of issuance.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN11 Protect Encryption Keys", - "SubSection": "", - "SubSectionObjective": "Ensure that encryption keys are managed securely by enforcing the use of approved algorithms, regular key rotation, and customer-managed encryption keys (CMEKs).", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH16" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.DS-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "CEK-08", - "CEK-10", - "CEK-12" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.10.1.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "SC-12", - "SC-17" - ] - } - ] - } - ], - "Checks": [ - "kms_key_rotation_enabled", - "kms_key_not_publicly_accessible", - "dataproc_encrypted_with_cmks_disabled", - "bigquery_dataset_cmk_encryption", - "bigquery_table_cmk_encryption" - ] - }, - { - "Id": "CCC.Core.CN14.AR01", - "Description": "When backups are created for disaster recovery purposes, the storage mechanism MUST NOT allow modification or deletion within 30 days of creation.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN14 Maintain Recent Backups", - "SubSection": "", - "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Use immutable storage solutions where possible. Implement backup retention policies that enforce a minimum retention period of 30 days. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock", - "cloudsql_instance_automated_backups" - ] - }, - { - "Id": "CCC.Core.CN14.AR02", - "Description": "When backups are created for disaster recovery purposes, the most recent backup MUST have a creation date within the past 30 days.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN14 Maintain Recent Backups", - "SubSection": "", - "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", - "Applicability": [ - "tlp-clear", - "tlp-green", + "tlp-red", "tlp-amber" ], - "Recommendation": "Implement automated backup processes to ensure that backups are created regularly. Monitor backup schedules and verify that the most recent backup creation date is within the last 30 days. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "cloudsql_instance_automated_backups" - ] - }, - { - "Id": "CCC.Core.CN14.AR02", - "Description": "When backups are created for disaster recovery purposes, the most recent backup MUST have a creation date within the past 14 days.", - "Attributes": [ - { - "FamilyName": "Data", - "FamilyDescription": "The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle. These controls govern how data is transmitted, stored, replicated, and protected from unauthorized access, tampering, or exposure beyond defined trust perimeters. ", - "Section": "CCC.Core.CN14 Maintain Recent Backups", - "SubSection": "", - "SubSectionObjective": "Ensure that all backups used for disaster recovery are recent and subject to a retention policy that limits deletion.", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "Implement automated backup processes to ensure that backups are created regularly. Monitor backup schedules and verify that the most recent backup creation date is within the last 14 days. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH06" - ] - } - ], - "SectionGuidelineMappings": [] - } - ], - "Checks": [ - "cloudsql_instance_automated_backups", - "cloudstorage_bucket_log_retention_policy_lock" - ] - }, - { - "Id": "CCC.Core.CN03.AR01", - "Description": "When an entity attempts to modify the service through a user interface, the authentication process MUST require multiple identifying factors for authentication.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "", - "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH01" ] } ], "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-14" - ] - }, { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-03", - "IAM-08" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.2" + "PR.AC-5" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "IA-2" + "SC-7", + "SC-8" ] } ] @@ -5088,990 +7592,45 @@ "Checks": [] }, { - "Id": "CCC.Core.CN03.AR02", - "Description": "When an entity attempts to modify the service through an API endpoint, the authentication process MUST require a credential such as an API key or token AND originate from within the trust perimeter.", + "Id": "CCC.SvlsComp.CN02.AR01", + "Description": "Send requests to invoke the function up to the allowed threshold and confirm they are successful; then send additional requests exceeding the threshold from the same entity and verify that they are denied.", "Attributes": [ { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", + "FamilyName": "Availability", + "FamilyDescription": "TODO: Describe this control family", + "Section": "CCC.SvlsComp.CN02 Implement Function Invocation Rate Limits", "SubSection": "", - "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", + "SubSectionObjective": "Ensure that function invocation is limited to a specified threshold from any single entity, preventing resource exhaustion and denial of service attacks.", "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" + "tlp-red", + "tlp-amber" ], "Recommendation": "", "SectionThreatMappings": [ { "ReferenceId": "CCC", "Identifiers": [ - "CCC.TH01" + "CCC.Core.TH12" ] } ], "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-14" - ] - }, { "ReferenceId": "NIST-CSF", "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-03", - "IAM-08" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.2" + "PR.DS-4" ] }, { "ReferenceId": "NIST_800_53", "Identifiers": [ - "IA-2" - ] - } - ] - } - ], - "Checks": [ - "apikeys_api_restrictions_configured", - "apikeys_key_exists", - "apikeys_key_rotated_in_90_days", - "compute_firewall_rdp_access_from_the_internet_allowed", - "compute_firewall_ssh_access_from_the_internet_allowed", - "compute_instance_public_ip", - "cloudsql_instance_public_ip", - "cloudstorage_bucket_public_access" - ] - }, - { - "Id": "CCC.Core.CN03.AR03", - "Description": "When an entity attempts to view information on the service through a user interface, the authentication process MUST require multiple identifying factors from the user.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "", - "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-14" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-03", - "IAM-08" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "IA-2" + "SC-5" ] } ] } ], "Checks": [] - }, - { - "Id": "CCC.Core.CN03.AR04", - "Description": "When an entity attempts to view information on the service through an API endpoint, the authentication process MUST require a credential such as an API key or token AND originate from within the trust perimeter.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN03 Implement Multi-factor Authentication (MFA) for Access", - "SubSection": "", - "SubSectionObjective": "Ensure that all sensitive activities require two or more identity factors during authentication to prevent unauthorized access.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-14" - ] - }, - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-7" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "IAM-03", - "IAM-08" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.9.4.2" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "IA-2" - ] - } - ] - } - ], - "Checks": [ - "compute_firewall_rdp_access_from_the_internet_allowed", - "compute_firewall_ssh_access_from_the_internet_allowed", - "cloudsql_instance_public_ip", - "cloudsql_instance_public_access", - "cloudstorage_bucket_public_access", - "apikeys_api_restrictions_configured", - "iam_account_access_approval_enabled", - "iam_audit_logs_enabled", - "compute_project_os_login_enabled" - ] - }, - { - "Id": "CCC.Core.CN05.AR01", - "Description": "When an attempt is made to modify data on the service or a child resource, the service MUST block requests from unauthorized entities.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "compute_firewall_rdp_access_from_the_internet_allowed", - "compute_firewall_ssh_access_from_the_internet_allowed", - "cloudstorage_bucket_public_access", - "compute_instance_public_ip", - "cloudsql_instance_public_ip", - "compute_instance_ip_forwarding_is_enabled", - "compute_instance_default_service_account_in_use", - "compute_instance_default_service_account_in_use_with_full_api_access", - "gke_cluster_no_default_service_account", - "iam_no_service_roles_at_project_level", - "iam_role_kms_enforce_separation_of_duties", - "iam_role_sa_enforce_separation_of_duties", - "iam_sa_no_administrative_privileges", - "iam_sa_no_user_managed_keys", - "iam_sa_user_managed_key_rotate_90_days", - "iam_sa_user_managed_key_unused", - "iam_service_account_unused", - "iam_audit_logs_enabled", - "iam_account_access_approval_enabled" - ] - }, - { - "Id": "CCC.Core.CN05.AR02", - "Description": "When administrative access or configuration change is attempted on the service or a child resource, the service MUST refuse requests from unauthorized entities.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "iam_account_access_approval_enabled", - "iam_audit_logs_enabled", - "iam_no_service_roles_at_project_level", - "iam_sa_no_administrative_privileges", - "iam_role_kms_enforce_separation_of_duties", - "iam_role_sa_enforce_separation_of_duties", - "iam_sa_no_user_managed_keys", - "iam_sa_user_managed_key_rotate_90_days", - "iam_sa_user_managed_key_unused", - "iam_service_account_unused", - "compute_project_os_login_enabled", - "compute_instance_default_service_account_in_use", - "compute_instance_default_service_account_in_use_with_full_api_access", - "compute_instance_public_ip", - "compute_firewall_rdp_access_from_the_internet_allowed", - "compute_firewall_ssh_access_from_the_internet_allowed", - "cloudsql_instance_public_ip", - "cloudstorage_bucket_public_access", - "apikeys_api_restrictions_configured", - "apikeys_key_exists", - "apikeys_key_rotated_in_90_days", - "iam_cloud_asset_inventory_enabled" - ] - }, - { - "Id": "CCC.Core.CN05.AR03", - "Description": "When administrative access or configuration change is attempted on the service or a child resource in a multi-tenant environment, the service MUST refuse requests across tenant boundaries unless the origin is explicitly included in a pre-approved allowlist.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "iam_no_service_roles_at_project_level", - "iam_account_access_approval_enabled", - "iam_audit_logs_enabled", - "iam_role_kms_enforce_separation_of_duties", - "iam_role_sa_enforce_separation_of_duties", - "iam_sa_no_administrative_privileges", - "iam_sa_no_user_managed_keys", - "iam_sa_user_managed_key_rotate_90_days", - "iam_sa_user_managed_key_unused", - "iam_service_account_unused", - "compute_firewall_rdp_access_from_the_internet_allowed", - "compute_firewall_ssh_access_from_the_internet_allowed", - "compute_instance_public_ip", - "cloudsql_instance_public_ip", - "cloudsql_instance_public_access", - "cloudstorage_bucket_public_access", - "compute_instance_default_service_account_in_use", - "compute_instance_default_service_account_in_use_with_full_api_access", - "gke_cluster_no_default_service_account" - ] - }, - { - "Id": "CCC.Core.CN05.AR04", - "Description": "When data is requested from outside the trust perimeter, the service MUST refuse requests from unauthorized entities.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "", - "SubSection": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "iam_no_service_roles_at_project_level", - "iam_account_access_approval_enabled", - "iam_audit_logs_enabled", - "iam_role_kms_enforce_separation_of_duties", - "iam_role_sa_enforce_separation_of_duties", - "iam_sa_no_administrative_privileges", - "iam_sa_no_user_managed_keys", - "iam_sa_user_managed_key_rotate_90_days", - "iam_sa_user_managed_key_unused", - "iam_service_account_unused", - "gke_cluster_no_default_service_account", - "compute_instance_default_service_account_in_use", - "compute_instance_default_service_account_in_use_with_full_api_access", - "compute_instance_block_project_wide_ssh_keys_disabled", - "compute_instance_public_ip", - "compute_firewall_ssh_access_from_the_internet_allowed", - "compute_firewall_rdp_access_from_the_internet_allowed", - "cloudsql_instance_public_ip", - "cloudsql_instance_public_access", - "cloudstorage_bucket_public_access", - "kms_key_not_publicly_accessible", - "kms_key_rotation_enabled", - "apikeys_api_restrictions_configured", - "apikeys_key_exists", - "apikeys_key_rotated_in_90_days" - ] - }, - { - "Id": "CCC.Core.CN05.AR05", - "Description": "When any request is made from outside the trust perimeter, the service MUST NOT provide any response that may indicate the service exists.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "compute_firewall_rdp_access_from_the_internet_allowed", - "compute_firewall_ssh_access_from_the_internet_allowed", - "compute_instance_public_ip", - "cloudsql_instance_public_access", - "cloudstorage_bucket_public_access", - "compute_instance_default_service_account_in_use", - "compute_instance_default_service_account_in_use_with_full_api_access", - "gke_cluster_no_default_service_account", - "iam_no_service_roles_at_project_level", - "iam_account_access_approval_enabled", - "iam_audit_logs_enabled", - "iam_sa_no_administrative_privileges", - "iam_sa_no_user_managed_keys", - "iam_role_kms_enforce_separation_of_duties", - "iam_role_sa_enforce_separation_of_duties", - "iam_sa_user_managed_key_rotate_90_days", - "iam_sa_user_managed_key_unused", - "iam_service_account_unused", - "apikeys_api_restrictions_configured", - "kms_key_not_publicly_accessible", - "compute_instance_ip_forwarding_is_enabled" - ] - }, - { - "Id": "CCC.Core.CN05.AR06", - "Description": "When any request is made to the service or a child resource, the service MUST refuse requests from unauthorized entities.", - "Attributes": [ - { - "FamilyName": "Identity and Access Management", - "FamilyDescription": "The Identity and Access Management control family ensures that only trusted and authenticated entities can access resources. These controls establish strong authentication, enforce multi-factor verification, and restrict access to approved sources to prevent unauthorized use or data exfiltration. ", - "Section": "CCC.Core.CN05 Prevent Access from Untrusted Entities", - "SubSection": "", - "SubSectionObjective": "Ensure that secure access controls enforce the principle of least privilege to restrict access to authorized entities from explicitly trusted sources only.", - "Applicability": [ - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "PR.AC-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "DSP-01", - "DSP-07", - "DSP-08", - "DSP-10", - "DSP-17" - ] - }, - { - "ReferenceId": "ISO_27001", - "Identifiers": [ - "2013 A.13.1.3" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AC-3" - ] - } - ] - } - ], - "Checks": [ - "compute_firewall_rdp_access_from_the_internet_allowed", - "compute_firewall_ssh_access_from_the_internet_allowed", - "compute_instance_public_ip", - "compute_instance_default_service_account_in_use", - "compute_instance_default_service_account_in_use_with_full_api_access", - "gke_cluster_no_default_service_account", - "cloudstorage_bucket_public_access", - "cloudsql_instance_public_access", - "cloudsql_instance_public_ip", - "cloudsql_instance_private_ip_assignment", - "apikeys_api_restrictions_configured", - "apikeys_key_exists", - "apikeys_key_rotated_in_90_days", - "kms_key_not_publicly_accessible", - "kms_key_rotation_enabled", - "iam_no_service_roles_at_project_level", - "iam_account_access_approval_enabled", - "iam_audit_logs_enabled", - "iam_role_kms_enforce_separation_of_duties", - "iam_role_sa_enforce_separation_of_duties", - "iam_sa_no_administrative_privileges", - "iam_sa_no_user_managed_keys", - "iam_sa_user_managed_key_rotate_90_days", - "iam_sa_user_managed_key_unused", - "iam_service_account_unused" - ] - }, - { - "Id": "CCC.Core.CN04.AR01", - "Description": "When administrative access or configuration change is attempted on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN04 Log All Access and Changes", - "SubSection": "", - "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-08" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3", - "AU-12" - ] - } - ] - } - ], - "Checks": [ - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "iam_audit_logs_enabled", - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled" - ] - }, - { - "Id": "CCC.Core.CN04.AR02", - "Description": "When any attempt is made to modify data on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN04 Log All Access and Changes", - "SubSection": "", - "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-08" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3", - "AU-12" - ] - } - ] - } - ], - "Checks": [ - "iam_audit_logs_enabled", - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled" - ] - }, - { - "Id": "CCC.Core.CN04.AR03", - "Description": "When any attempt is made to read data on the service or a child resource, the service MUST log the client identity, time, and result of the attempt.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN04 Log All Access and Changes", - "SubSection": "", - "SubSectionObjective": "Ensure that all access attempts are logged to maintain a detailed audit trail for security and compliance purposes.", - "Applicability": [ - "tlp-red" - ], - "Recommendation": "", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH01" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-3" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-08" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-2", - "AU-3", - "AU-12" - ] - } - ] - } - ], - "Checks": [ - "iam_audit_logs_enabled", - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled" - ] - }, - { - "Id": "CCC.Core.CN07.AR01", - "Description": "When enumeration activities are detected, the service MUST publish an event to a monitored channel which includes the client identity, time, and nature of the activity.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN07 Alert on Unusual Enumeration Activity", - "SubSection": "", - "SubSectionObjective": "Ensure that logs and associated alerts are generated when unusual enumeration activity is detected that may indicate reconnaissance activities.", - "Applicability": [ - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Implement event publication mechanisms and alerts for patterns indicative of enumeration activities, such as repeated access attempts, requests, or liveness probes. Configure alerts to notify security teams of any activities that merit further investigation. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH15" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-05", - "SEF-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-6" - ] - } - ] - } - ], - "Checks": [ - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "logging_sink_created" - ] - }, - { - "Id": "CCC.Core.CN07.AR02", - "Description": "When enumeration activities are detected, the service MUST log the client identity, time, and nature of the activity.", - "Attributes": [ - { - "FamilyName": "Logging & Monitoring", - "FamilyDescription": "The Logging & Monitoring control family ensures that access, changes, and security-relevant events are captured, monitored, and alerted on in order to provide visibility, support incident response, and meet compliance requirements. ", - "Section": "CCC.Core.CN07 Alert on Unusual Enumeration Activity", - "SubSection": "", - "SubSectionObjective": "Ensure that logs and associated alerts are generated when unusual enumeration activity is detected that may indicate reconnaissance activities.", - "Applicability": [ - "tlp-clear", - "tlp-green", - "tlp-amber", - "tlp-red" - ], - "Recommendation": "Implement logging mechanisms to capture details of enumeration activities, including client identity, timestamps, and activity nature. Retain logs according to organizational policies, and occasionally review them for patterns that may indicate reconnaissance activities. ", - "SectionThreatMappings": [ - { - "ReferenceId": "CCC", - "Identifiers": [ - "CCC.TH15" - ] - } - ], - "SectionGuidelineMappings": [ - { - "ReferenceId": "NIST-CSF", - "Identifiers": [ - "DE.AE-1" - ] - }, - { - "ReferenceId": "CCM", - "Identifiers": [ - "LOG-05", - "SEF-05" - ] - }, - { - "ReferenceId": "NIST_800_53", - "Identifiers": [ - "AU-6" - ] - } - ] - } - ], - "Checks": [ - "iam_audit_logs_enabled", - "logging_sink_created", - "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", - "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled" - ] } ] } diff --git a/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json b/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json index 5d99d82c73..53342033cc 100644 --- a/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json +++ b/prowler/compliance/googleworkspace/cis_1.3_googleworkspace.json @@ -230,7 +230,9 @@ { "Id": "3.1.2.1.1.1", "Description": "Ensure users are warned when they share a file outside their domain", - "Checks": [], + "Checks": [ + "drive_external_sharing_warn_users" + ], "Attributes": [ { "Section": "3 Apps", @@ -251,7 +253,9 @@ { "Id": "3.1.2.1.1.2", "Description": "Ensure users cannot publish files to the web or make visible to the world as public or unlisted", - "Checks": [], + "Checks": [ + "drive_publishing_files_disabled" + ], "Attributes": [ { "Section": "3 Apps", @@ -272,7 +276,9 @@ { "Id": "3.1.2.1.1.3", "Description": "Ensure document sharing is being controlled by domain with allowlists", - "Checks": [], + "Checks": [ + "drive_sharing_allowlisted_domains" + ], "Attributes": [ { "Section": "3 Apps", @@ -293,7 +299,9 @@ { "Id": "3.1.2.1.1.4", "Description": "Ensure users are warned when they share a file with users in an allowlisted domain", - "Checks": [], + "Checks": [ + "drive_warn_sharing_with_allowlisted_domains" + ], "Attributes": [ { "Section": "3 Apps", @@ -314,7 +322,9 @@ { "Id": "3.1.2.1.1.5", "Description": "Ensure Access Checker is configured to limit file access", - "Checks": [], + "Checks": [ + "drive_access_checker_recipients_only" + ], "Attributes": [ { "Section": "3 Apps", @@ -335,7 +345,9 @@ { "Id": "3.1.2.1.1.6", "Description": "Ensure only users inside your organization can distribute content externally", - "Checks": [], + "Checks": [ + "drive_internal_users_distribute_content" + ], "Attributes": [ { "Section": "3 Apps", @@ -356,7 +368,9 @@ { "Id": "3.1.2.1.2.1", "Description": "Ensure users can create new shared drives", - "Checks": [], + "Checks": [ + "drive_shared_drive_creation_allowed" + ], "Attributes": [ { "Section": "3 Apps", @@ -377,7 +391,9 @@ { "Id": "3.1.2.1.2.2", "Description": "Ensure manager access members cannot modify shared drive settings", - "Checks": [], + "Checks": [ + "drive_shared_drive_managers_cannot_override" + ], "Attributes": [ { "Section": "3 Apps", @@ -398,7 +414,9 @@ { "Id": "3.1.2.1.2.3", "Description": "Ensure shared drive file access is restricted to members only", - "Checks": [], + "Checks": [ + "drive_shared_drive_members_only_access" + ], "Attributes": [ { "Section": "3 Apps", @@ -419,7 +437,9 @@ { "Id": "3.1.2.1.2.4", "Description": "Ensure viewers and commenters ability to download, print, and copy files is disabled", - "Checks": [], + "Checks": [ + "drive_shared_drive_disable_download_print_copy" + ], "Attributes": [ { "Section": "3 Apps", @@ -461,7 +481,9 @@ { "Id": "3.1.2.2.2", "Description": "Ensure desktop access to Drive is disabled", - "Checks": [], + "Checks": [ + "drive_desktop_access_disabled" + ], "Attributes": [ { "Section": "3 Apps", diff --git a/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json b/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json index e81f4c70a3..b6e5a06491 100644 --- a/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json +++ b/prowler/compliance/googleworkspace/cisa_scuba_0.6_googleworkspace.json @@ -1089,7 +1089,9 @@ { "Id": "GWS.DRIVEDOCS.1.1", "Description": "External sharing SHALL be restricted to allowlisted domains", - "Checks": [], + "Checks": [ + "drive_sharing_allowlisted_domains" + ], "Attributes": [ { "Section": "Drive and Docs", @@ -1115,7 +1117,9 @@ { "Id": "GWS.DRIVEDOCS.1.3", "Description": "Warnings SHALL be enabled when a user is attempting to share with someone not in allowlisted domains", - "Checks": [], + "Checks": [ + "drive_warn_sharing_with_allowlisted_domains" + ], "Attributes": [ { "Section": "Drive and Docs", @@ -1141,7 +1145,9 @@ { "Id": "GWS.DRIVEDOCS.1.5", "Description": "Any OUs that do allow external sharing SHOULD disable making content available to anyone with the link", - "Checks": [], + "Checks": [ + "drive_publishing_files_disabled" + ], "Attributes": [ { "Section": "Drive and Docs", @@ -1154,7 +1160,9 @@ { "Id": "GWS.DRIVEDOCS.1.6", "Description": "Agencies SHALL set access checking to recipients only", - "Checks": [], + "Checks": [ + "drive_access_checker_recipients_only" + ], "Attributes": [ { "Section": "Drive and Docs", @@ -1193,7 +1201,9 @@ { "Id": "GWS.DRIVEDOCS.1.9", "Description": "Out-of-Domain file-level warnings SHALL be enabled", - "Checks": [], + "Checks": [ + "drive_external_sharing_warn_users" + ], "Attributes": [ { "Section": "Drive and Docs", @@ -1232,7 +1242,9 @@ { "Id": "GWS.DRIVEDOCS.2.1", "Description": "Agencies SHOULD NOT allow members with manager access to override shared Google Drive creation settings", - "Checks": [], + "Checks": [ + "drive_shared_drive_managers_cannot_override" + ], "Attributes": [ { "Section": "Drive and Docs", diff --git a/prowler/compliance/m365/cis_6.0_m365.json b/prowler/compliance/m365/cis_6.0_m365.json index f13c323df4..d0dcfb2e6d 100644 --- a/prowler/compliance/m365/cis_6.0_m365.json +++ b/prowler/compliance/m365/cis_6.0_m365.json @@ -873,7 +873,9 @@ { "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": [], + "Checks": [ + "intune_device_compliance_policy_unassigned_devices_not_compliant_by_default" + ], "Attributes": [ { "Section": "4 Microsoft Intune admin center", diff --git a/prowler/compliance/m365/iso27001_2022_m365.json b/prowler/compliance/m365/iso27001_2022_m365.json index ede6727166..1a3419a12e 100644 --- a/prowler/compliance/m365/iso27001_2022_m365.json +++ b/prowler/compliance/m365/iso27001_2022_m365.json @@ -206,7 +206,11 @@ "admincenter_users_admins_reduced_license_footprint", "entra_admin_portals_access_restriction", "entra_admin_users_phishing_resistant_mfa_enabled", + "entra_conditional_access_policy_mfa_enforced_for_guest_users", "entra_conditional_access_policy_block_o365_elevated_insider_risk", + "entra_conditional_access_policy_block_unknown_device_platforms", + "entra_conditional_access_policy_directory_sync_account_excluded", + "entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced", "entra_policy_guest_users_access_restrictions", "entra_seamless_sso_disabled" ] @@ -245,11 +249,14 @@ "entra_admin_users_mfa_enabled", "entra_admin_users_sign_in_frequency_enabled", "entra_break_glass_account_fido2_security_key_registered", + "entra_conditional_access_policy_mfa_enforced_for_guest_users", "entra_default_app_management_policy_enabled", "entra_all_apps_conditional_access_coverage", "entra_conditional_access_policy_device_registration_mfa_required", "entra_intune_enrollment_sign_in_frequency_every_time", + "entra_conditional_access_policy_block_unknown_device_platforms", "entra_conditional_access_policy_device_code_flow_blocked", + "entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced", "entra_legacy_authentication_blocked", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", @@ -425,6 +432,7 @@ ], "Checks": [ "admincenter_groups_not_public_visibility", + "exchange_organization_delicensing_resiliency_enabled", "teams_meeting_recording_disabled" ] }, @@ -626,6 +634,7 @@ "entra_admin_users_phishing_resistant_mfa_enabled", "entra_conditional_access_policy_approved_client_app_required_for_mobile", "entra_conditional_access_policy_app_enforced_restrictions", + "entra_conditional_access_policy_block_unknown_device_platforms", "entra_conditional_access_policy_device_registration_mfa_required", "entra_intune_enrollment_sign_in_frequency_every_time", "entra_managed_device_required_for_authentication", @@ -685,7 +694,10 @@ "entra_conditional_access_policy_approved_client_app_required_for_mobile", "entra_conditional_access_policy_app_enforced_restrictions", "entra_conditional_access_policy_block_elevated_insider_risk", + "entra_conditional_access_policy_directory_sync_account_excluded", "entra_conditional_access_policy_block_o365_elevated_insider_risk", + "entra_conditional_access_policy_block_unknown_device_platforms", + "entra_conditional_access_policy_mfa_enforced_for_guest_users", "entra_policy_guest_users_access_restrictions", "sharepoint_external_sharing_restricted" ] @@ -707,10 +719,14 @@ "entra_admin_users_sign_in_frequency_enabled", "entra_all_apps_conditional_access_coverage", "entra_conditional_access_policy_device_registration_mfa_required", + "entra_conditional_access_policy_mfa_enforced_for_guest_users", "entra_intune_enrollment_sign_in_frequency_every_time", "entra_break_glass_account_fido2_security_key_registered", "entra_conditional_access_policy_approved_client_app_required_for_mobile", + "entra_conditional_access_policy_block_unknown_device_platforms", "entra_conditional_access_policy_device_code_flow_blocked", + "entra_conditional_access_policy_directory_sync_account_excluded", + "entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced", "entra_identity_protection_sign_in_risk_enabled", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", diff --git a/prowler/config/config.py b/prowler/config/config.py index 3339a0d1ea..debea713aa 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -38,7 +38,7 @@ class _MutableTimestamp: timestamp = _MutableTimestamp(datetime.today()) timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc)) -prowler_version = "5.23.0" +prowler_version = "5.24.0" html_logo_url = "https://github.com/prowler-cloud/prowler/" square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png" aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png" diff --git a/prowler/lib/outputs/compliance/ccc/ccc.py b/prowler/lib/outputs/compliance/ccc/ccc.py new file mode 100644 index 0000000000..99a6c91cd9 --- /dev/null +++ b/prowler/lib/outputs/compliance/ccc/ccc.py @@ -0,0 +1,98 @@ +from colorama import Fore, Style +from tabulate import tabulate + +from prowler.config.config import orange_color + + +def get_ccc_table( + findings: list, + bulk_checks_metadata: dict, + compliance_framework: str, + output_filename: str, + output_directory: str, + compliance_overview: bool, +): + section_table = { + "Provider": [], + "Section": [], + "Status": [], + "Muted": [], + } + pass_count = [] + fail_count = [] + muted_count = [] + sections = {} + for index, finding in enumerate(findings): + check = bulk_checks_metadata[finding.check_metadata.CheckID] + check_compliances = check.Compliance + for compliance in check_compliances: + if compliance.Framework == "CCC": + for requirement in compliance.Requirements: + for attribute in requirement.Attributes: + section = attribute.Section + + if section not in sections: + sections[section] = {"FAIL": 0, "PASS": 0, "Muted": 0} + + if finding.muted: + if index not in muted_count: + muted_count.append(index) + sections[section]["Muted"] += 1 + else: + if finding.status == "FAIL" and index not in fail_count: + fail_count.append(index) + sections[section]["FAIL"] += 1 + elif finding.status == "PASS" and index not in pass_count: + pass_count.append(index) + sections[section]["PASS"] += 1 + + sections = dict(sorted(sections.items())) + for section in sections: + section_table["Provider"].append(compliance.Provider) + section_table["Section"].append(section) + if sections[section]["FAIL"] > 0: + section_table["Status"].append( + f"{Fore.RED}FAIL({sections[section]['FAIL']}){Style.RESET_ALL}" + ) + else: + if sections[section]["PASS"] > 0: + section_table["Status"].append( + f"{Fore.GREEN}PASS({sections[section]['PASS']}){Style.RESET_ALL}" + ) + else: + section_table["Status"].append(f"{Fore.GREEN}PASS{Style.RESET_ALL}") + section_table["Muted"].append( + f"{orange_color}{sections[section]['Muted']}{Style.RESET_ALL}" + ) + + if ( + len(fail_count) + len(pass_count) + len(muted_count) > 1 + ): # If there are no resources, don't print the compliance table + print( + f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:" + ) + total_findings_count = len(fail_count) + len(pass_count) + len(muted_count) + overview_table = [ + [ + f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}", + f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}", + f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", + ] + ] + print(tabulate(overview_table, tablefmt="rounded_grid")) + if not compliance_overview: + if len(fail_count) > 0 and len(section_table["Section"]) > 0: + print( + f"\nFramework {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Results:" + ) + print( + tabulate( + section_table, + tablefmt="rounded_grid", + headers="keys", + ) + ) + print(f"\nDetailed results of {compliance_framework.upper()} are in:") + print( + f" - CSV: {output_directory}/compliance/{output_filename}_{compliance_framework}.csv\n" + ) diff --git a/prowler/lib/outputs/compliance/compliance.py b/prowler/lib/outputs/compliance/compliance.py index bb7fbf1146..d399900837 100644 --- a/prowler/lib/outputs/compliance/compliance.py +++ b/prowler/lib/outputs/compliance/compliance.py @@ -3,6 +3,7 @@ import sys from prowler.lib.check.models import Check_Report from prowler.lib.logger import logger from prowler.lib.outputs.compliance.c5.c5 import get_c5_table +from prowler.lib.outputs.compliance.ccc.ccc import get_ccc_table from prowler.lib.outputs.compliance.cis.cis import get_cis_table from prowler.lib.outputs.compliance.csa.csa import get_csa_table from prowler.lib.outputs.compliance.ens.ens import get_ens_table @@ -104,6 +105,15 @@ def display_compliance_table( output_directory, compliance_overview, ) + elif compliance_framework.startswith("ccc_"): + get_ccc_table( + findings, + bulk_checks_metadata, + compliance_framework, + output_filename, + output_directory, + compliance_overview, + ) else: get_generic_compliance_table( findings, diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index 297921370d..f525ce2155 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -1572,6 +1572,7 @@ "eu-west-1", "eu-west-2", "eu-west-3", + "sa-east-1", "us-east-1", "us-east-2", "us-west-2" @@ -2192,6 +2193,7 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-5", + "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", @@ -2256,6 +2258,7 @@ "ca-central-1", "ca-west-1", "eu-central-1", + "eu-central-2", "eu-north-1", "eu-south-1", "eu-south-2", @@ -3145,6 +3148,17 @@ "aws-us-gov": [] } }, + "connecthealth": { + "regions": { + "aws": [ + "us-east-1", + "us-west-2" + ], + "aws-cn": [], + "aws-eusc": [], + "aws-us-gov": [] + } + }, "connectparticipant": { "regions": { "aws": [ @@ -4561,6 +4575,14 @@ ] } }, + "elementalinference": { + "regions": { + "aws": [], + "aws-cn": [], + "aws-eusc": [], + "aws-us-gov": [] + } + }, "emr": { "regions": { "aws": [ @@ -5386,6 +5408,7 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", + "ap-southeast-4", "ap-southeast-5", "ap-southeast-7", "ca-central-1", @@ -5559,6 +5582,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -5870,7 +5894,9 @@ "cn-north-1", "cn-northwest-1" ], - "aws-eusc": [], + "aws-eusc": [ + "eusc-de-east-1" + ], "aws-us-gov": [ "us-gov-east-1", "us-gov-west-1" @@ -6146,10 +6172,12 @@ "ca-central-1", "eu-central-1", "eu-north-1", + "eu-south-1", "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", + "il-central-1", "me-central-1", "me-south-1", "sa-east-1", @@ -6182,10 +6210,12 @@ "ca-central-1", "eu-central-1", "eu-north-1", + "eu-south-1", "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", + "il-central-1", "me-central-1", "me-south-1", "sa-east-1", @@ -6314,10 +6344,12 @@ "ca-central-1", "eu-central-1", "eu-north-1", + "eu-south-1", "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", + "il-central-1", "me-central-1", "me-south-1", "sa-east-1", @@ -6412,10 +6444,12 @@ "ca-central-1", "eu-central-1", "eu-north-1", + "eu-south-1", "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", + "il-central-1", "me-central-1", "me-south-1", "sa-east-1", @@ -7267,6 +7301,7 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", + "ap-southeast-5", "ca-central-1", "eu-central-1", "eu-north-1", @@ -8250,6 +8285,7 @@ "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-5", "ap-southeast-7", "ca-central-1", "eu-central-1", @@ -8524,6 +8560,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", @@ -8553,7 +8590,6 @@ "notificationscontacts": { "regions": { "aws": [ - "ap-southeast-5", "us-east-1" ], "aws-cn": [], @@ -8669,10 +8705,14 @@ "regions": { "aws": [ "ap-northeast-1", + "ap-northeast-2", + "ap-south-1", + "ap-south-2", "ap-southeast-2", "ca-central-1", "eu-central-1", "eu-west-1", + "eu-west-2", "us-east-1", "us-east-2", "us-west-2" @@ -9053,6 +9093,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -10620,19 +10661,36 @@ "s3vectors": { "regions": { "aws": [ + "af-south-1", + "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", + "ap-northeast-3", "ap-south-1", + "ap-south-2", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-3", + "ap-southeast-4", + "ap-southeast-5", + "ap-southeast-6", + "ap-southeast-7", "ca-central-1", + "ca-west-1", "eu-central-1", + "eu-central-2", "eu-north-1", + "eu-south-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", + "mx-central-1", + "sa-east-1", "us-east-1", "us-east-2", + "us-west-1", "us-west-2" ], "aws-cn": [], @@ -11778,26 +11836,38 @@ "regions": { "aws": [ "af-south-1", + "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", + "ap-northeast-3", "ap-south-1", "ap-south-2", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-3", + "ap-southeast-4", + "ap-southeast-5", "ap-southeast-6", + "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", + "eu-central-2", "eu-north-1", + "eu-south-1", "eu-south-2", "eu-west-1", "eu-west-2", + "eu-west-3", + "il-central-1", "me-central-1", "me-south-1", "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", + "us-west-1", "us-west-2" ], "aws-cn": [], @@ -12061,7 +12131,9 @@ "cn-north-1", "cn-northwest-1" ], - "aws-eusc": [], + "aws-eusc": [ + "eusc-de-east-1" + ], "aws-us-gov": [ "us-gov-east-1", "us-gov-west-1" @@ -12284,6 +12356,17 @@ "aws-us-gov": [] } }, + "sustainability": { + "regions": { + "aws": [ + "us-east-1", + "us-west-2" + ], + "aws-cn": [], + "aws-eusc": [], + "aws-us-gov": [] + } + }, "swf": { "regions": { "aws": [ @@ -12461,6 +12544,7 @@ "regions": { "aws": [ "ap-northeast-1", + "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2", @@ -12474,6 +12558,7 @@ "eu-west-2", "eu-west-3", "me-central-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -12759,6 +12844,16 @@ ] } }, + "uxc": { + "regions": { + "aws": [ + "us-east-1" + ], + "aws-cn": [], + "aws-eusc": [], + "aws-us-gov": [] + } + }, "verified-access": { "regions": { "aws": [ diff --git a/prowler/providers/aws/services/bedrock/bedrock_full_access_policy_attached/__init__.py b/prowler/providers/aws/services/bedrock/bedrock_full_access_policy_attached/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/bedrock/bedrock_full_access_policy_attached/bedrock_full_access_policy_attached.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_full_access_policy_attached/bedrock_full_access_policy_attached.metadata.json new file mode 100644 index 0000000000..5fff8ebb2d --- /dev/null +++ b/prowler/providers/aws/services/bedrock/bedrock_full_access_policy_attached/bedrock_full_access_policy_attached.metadata.json @@ -0,0 +1,43 @@ +{ + "Provider": "aws", + "CheckID": "bedrock_full_access_policy_attached", + "CheckTitle": "IAM role does not have AmazonBedrockFullAccess managed policy attached", + "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": "", + "Severity": "high", + "ResourceType": "AwsIamRole", + "ResourceGroup": "IAM", + "Description": "**IAM roles** (excluding service roles) are evaluated for attachment of the AWS-managed `AmazonBedrockFullAccess` policy.\n\nThis policy grants unrestricted access to all Amazon Bedrock actions and resources.", + "Risk": "The `AmazonBedrockFullAccess` policy grants broad permissions across all Bedrock resources. If a role with this policy is compromised, an attacker could:\n- Invoke any model to exfiltrate data or generate harmful content\n- Modify guardrails, logging, and security configurations\n- Incur significant costs through unrestricted model invocations", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html", + "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" + ], + "Remediation": { + "Code": { + "CLI": "aws iam detach-role-policy --role-name --policy-arn arn:aws:iam::aws:policy/AmazonBedrockFullAccess", + "NativeIaC": "```yaml\n# CloudFormation: IAM Role without AmazonBedrockFullAccess\nResources:\n :\n Type: AWS::IAM::Role\n Properties:\n AssumeRolePolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Principal:\n Service: ec2.amazonaws.com\n Action: sts:AssumeRole\n ManagedPolicyArns: [] # Critical: ensure AmazonBedrockFullAccess is NOT attached\n```", + "Other": "1. Open the AWS Console and go to IAM > Roles\n2. Select the role flagged by the check\n3. On the Permissions tab, find \"AmazonBedrockFullAccess\" under Attached policies\n4. Click Detach next to \"AmazonBedrockFullAccess\"\n5. Confirm the detach\n6. Attach a scoped policy granting only required Bedrock actions", + "Terraform": "```hcl\n# IAM Role without AmazonBedrockFullAccess\nresource \"aws_iam_role\" \"\" {\n assume_role_policy = < list[Check_Report_AWS]: + """Execute the check logic. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + if iam_client.roles: + for role in iam_client.roles: + if not role.is_service_role: + report = Check_Report_AWS(metadata=self.metadata(), resource=role) + report.region = iam_client.region + report.status = "PASS" + report.status_extended = f"IAM Role {role.name} does not have AmazonBedrockFullAccess policy attached." + for policy in role.attached_policies: + if ( + policy["PolicyArn"] + == "arn:aws:iam::aws:policy/AmazonBedrockFullAccess" + ): + report.status = "FAIL" + report.status_extended = f"IAM Role {role.name} has AmazonBedrockFullAccess policy attached." + break + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/bedrock/bedrock_vpc_endpoints_configured/__init__.py b/prowler/providers/aws/services/bedrock/bedrock_vpc_endpoints_configured/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/bedrock/bedrock_vpc_endpoints_configured/bedrock_vpc_endpoints_configured.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_vpc_endpoints_configured/bedrock_vpc_endpoints_configured.metadata.json new file mode 100644 index 0000000000..0e2ac8fef5 --- /dev/null +++ b/prowler/providers/aws/services/bedrock/bedrock_vpc_endpoints_configured/bedrock_vpc_endpoints_configured.metadata.json @@ -0,0 +1,44 @@ +{ + "Provider": "aws", + "CheckID": "bedrock_vpc_endpoints_configured", + "CheckTitle": "VPC endpoints ensure private connectivity for all Bedrock APIs", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "bedrock", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AwsEc2VpcEndpointService", + "ResourceGroup": "network", + "Description": "**Amazon VPCs** are evaluated for **interface VPC endpoints** to all Bedrock services: `bedrock`, `bedrock-runtime`, `bedrock-agent`, `bedrock-agent-runtime`, and `bedrock-mantle` (OpenAI-compatible API). Only endpoints in `available` state are considered. Their presence indicates private Bedrock API connectivity over **AWS PrivateLink** within the VPC.", + "Risk": "Without private Bedrock endpoints, control plane and runtime API traffic exits the VPC via IGW or NAT Gateway. This expands exposure to network path threats (e.g., DNS hijack, MITM), weakens egress isolation, and adds an internet dependency for Bedrock API access, reducing availability if NAT or edge paths fail.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/usingVPC.html", + "https://docs.aws.amazon.com/bedrock/latest/userguide/vpc-interface-endpoints.html", + "https://docs.aws.amazon.com/vpc/latest/privatelink/create-interface-endpoint.html" + ], + "Remediation": { + "Code": { + "CLI": "aws ec2 create-vpc-endpoint --vpc-id --service-name com.amazonaws..bedrock-runtime --vpc-endpoint-type Interface --subnet-ids && aws ec2 create-vpc-endpoint --vpc-id --service-name com.amazonaws..bedrock --vpc-endpoint-type Interface --subnet-ids && aws ec2 create-vpc-endpoint --vpc-id --service-name com.amazonaws..bedrock-agent --vpc-endpoint-type Interface --subnet-ids && aws ec2 create-vpc-endpoint --vpc-id --service-name com.amazonaws..bedrock-agent-runtime --vpc-endpoint-type Interface --subnet-ids && aws ec2 create-vpc-endpoint --vpc-id --service-name com.amazonaws..bedrock-mantle --vpc-endpoint-type Interface --subnet-ids ", + "NativeIaC": "```yaml\n# CloudFormation: create Bedrock interface VPC endpoints in the VPC\nResources:\n BedrockEndpoint:\n Type: AWS::EC2::VPCEndpoint\n Properties:\n VpcId: \"\" # CRITICAL: target VPC\n ServiceName: !Sub \"com.amazonaws.${AWS::Region}.bedrock\" # CRITICAL: Bedrock control plane endpoint\n VpcEndpointType: Interface # CRITICAL: interface endpoint\n SubnetIds:\n - \"\"\n BedrockRuntimeEndpoint:\n Type: AWS::EC2::VPCEndpoint\n Properties:\n VpcId: \"\" # CRITICAL: target VPC\n ServiceName: !Sub \"com.amazonaws.${AWS::Region}.bedrock-runtime\" # CRITICAL: Bedrock runtime endpoint\n VpcEndpointType: Interface # CRITICAL: interface endpoint\n SubnetIds:\n - \"\"\n BedrockAgentEndpoint:\n Type: AWS::EC2::VPCEndpoint\n Properties:\n VpcId: \"\" # CRITICAL: target VPC\n ServiceName: !Sub \"com.amazonaws.${AWS::Region}.bedrock-agent\" # CRITICAL: Bedrock agent control plane endpoint\n VpcEndpointType: Interface # CRITICAL: interface endpoint\n SubnetIds:\n - \"\"\n BedrockAgentRuntimeEndpoint:\n Type: AWS::EC2::VPCEndpoint\n Properties:\n VpcId: \"\" # CRITICAL: target VPC\n ServiceName: !Sub \"com.amazonaws.${AWS::Region}.bedrock-agent-runtime\" # CRITICAL: Bedrock agent runtime endpoint\n VpcEndpointType: Interface # CRITICAL: interface endpoint\n SubnetIds:\n - \"\"\n BedrockMantleEndpoint:\n Type: AWS::EC2::VPCEndpoint\n Properties:\n VpcId: \"\" # CRITICAL: target VPC\n ServiceName: !Sub \"com.amazonaws.${AWS::Region}.bedrock-mantle\" # CRITICAL: Bedrock Mantle OpenAI-compatible endpoint\n VpcEndpointType: Interface # CRITICAL: interface endpoint\n SubnetIds:\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 com.amazonaws..bedrock\n4. Select your VPC and at least one subnet\n5. Click Create endpoint\n6. Repeat steps 2-5 for com.amazonaws..bedrock-runtime, com.amazonaws..bedrock-agent, com.amazonaws..bedrock-agent-runtime, and com.amazonaws..bedrock-mantle", + "Terraform": "```hcl\n# Create Bedrock control plane interface VPC endpoint\nresource \"aws_vpc_endpoint\" \"bedrock\" {\n vpc_id = \"\" # CRITICAL: target VPC\n service_name = \"com.amazonaws..bedrock\" # CRITICAL: Bedrock control plane endpoint\n vpc_endpoint_type = \"Interface\" # CRITICAL: interface endpoint\n subnet_ids = [\"\"] # CRITICAL: subnet(s) for endpoint ENIs\n}\n\n# Create Bedrock runtime interface VPC endpoint\nresource \"aws_vpc_endpoint\" \"bedrock_runtime\" {\n vpc_id = \"\" # CRITICAL: target VPC\n service_name = \"com.amazonaws..bedrock-runtime\" # CRITICAL: Bedrock runtime endpoint\n vpc_endpoint_type = \"Interface\" # CRITICAL: interface endpoint\n subnet_ids = [\"\"] # CRITICAL: subnet(s) for endpoint ENIs\n}\n\n# Create Bedrock agent control plane interface VPC endpoint\nresource \"aws_vpc_endpoint\" \"bedrock_agent\" {\n vpc_id = \"\" # CRITICAL: target VPC\n service_name = \"com.amazonaws..bedrock-agent\" # CRITICAL: Bedrock agent control plane endpoint\n vpc_endpoint_type = \"Interface\" # CRITICAL: interface endpoint\n subnet_ids = [\"\"] # CRITICAL: subnet(s) for endpoint ENIs\n}\n\n# Create Bedrock agent runtime interface VPC endpoint\nresource \"aws_vpc_endpoint\" \"bedrock_agent_runtime\" {\n vpc_id = \"\" # CRITICAL: target VPC\n service_name = \"com.amazonaws..bedrock-agent-runtime\" # CRITICAL: Bedrock agent runtime endpoint\n vpc_endpoint_type = \"Interface\" # CRITICAL: interface endpoint\n subnet_ids = [\"\"] # CRITICAL: subnet(s) for endpoint ENIs\n}\n\n# Create Bedrock Mantle (OpenAI-compatible) interface VPC endpoint\nresource \"aws_vpc_endpoint\" \"bedrock_mantle\" {\n vpc_id = \"\" # CRITICAL: target VPC\n service_name = \"com.amazonaws..bedrock-mantle\" # CRITICAL: Bedrock Mantle OpenAI-compatible endpoint\n vpc_endpoint_type = \"Interface\" # CRITICAL: interface endpoint\n subnet_ids = [\"\"] # CRITICAL: subnet(s) for endpoint ENIs\n}\n```" + }, + "Recommendation": { + "Text": "Use **interface VPC endpoints** for all Bedrock services (`bedrock`, `bedrock-runtime`, `bedrock-agent`, `bedrock-agent-runtime`, `bedrock-mantle`) in each VPC that requires Bedrock 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/bedrock_vpc_endpoints_configured" + } + }, + "Categories": [ + "internet-exposed", + "trust-boundaries", + "gen-ai" + ], + "DependsOn": [], + "RelatedTo": [ + "vpc_endpoint_for_ec2_enabled" + ], + "Notes": "This check only evaluates VPCs in regions where Bedrock activity is detected (guardrails, agents, or model invocation logging enabled). Only VPC endpoints in 'available' state are considered." +} diff --git a/prowler/providers/aws/services/bedrock/bedrock_vpc_endpoints_configured/bedrock_vpc_endpoints_configured.py b/prowler/providers/aws/services/bedrock/bedrock_vpc_endpoints_configured/bedrock_vpc_endpoints_configured.py new file mode 100644 index 0000000000..92e2de3464 --- /dev/null +++ b/prowler/providers/aws/services/bedrock/bedrock_vpc_endpoints_configured/bedrock_vpc_endpoints_configured.py @@ -0,0 +1,88 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.bedrock.bedrock_agent_client import ( + bedrock_agent_client, +) +from prowler.providers.aws.services.bedrock.bedrock_client import bedrock_client +from prowler.providers.aws.services.vpc.vpc_client import vpc_client + +BEDROCK_ENDPOINT_SERVICES = { + "bedrock": "Bedrock control plane", + "bedrock-runtime": "Bedrock runtime", + "bedrock-agent": "Bedrock agent control plane", + "bedrock-agent-runtime": "Bedrock agent runtime", + "bedrock-mantle": "Bedrock Mantle (OpenAI-compatible API)", +} + + +class bedrock_vpc_endpoints_configured(Check): + """Ensure VPC endpoints are configured for Bedrock services. + + This check verifies that each VPC in regions with Bedrock activity has + interface VPC endpoints for all Amazon Bedrock services (control plane, + runtime, agent, agent runtime, and Mantle OpenAI-compatible API), + ensuring that traffic to these services remains within the AWS network. + - PASS: The VPC has VPC endpoints for all Bedrock services. + - FAIL: The VPC is missing one or more Bedrock VPC endpoints. + VPCs in regions without Bedrock activity are skipped. + """ + + def execute(self) -> list[Check_Report_AWS]: + """Execute the check logic. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + bedrock_regions = self._get_bedrock_active_regions() + + for vpc_id, vpc in vpc_client.vpcs.items(): + if not (vpc_client.provider.scan_unused_services or vpc.in_use): + continue + + if vpc.region not in bedrock_regions: + continue + + report = Check_Report_AWS(metadata=self.metadata(), resource=vpc) + report.status = "FAIL" + + found_services = set() + + for endpoint in vpc_client.vpc_endpoints: + if endpoint.vpc_id == vpc_id and endpoint.state == "available": + for svc_suffix in BEDROCK_ENDPOINT_SERVICES: + if endpoint.service_name.endswith(f".{svc_suffix}"): + found_services.add(svc_suffix) + + missing_services = set(BEDROCK_ENDPOINT_SERVICES) - found_services + + if not missing_services: + report.status = "PASS" + report.status_extended = ( + f"VPC {vpc.id} has VPC endpoints for all Bedrock services." + ) + else: + missing_labels = [ + BEDROCK_ENDPOINT_SERVICES[svc] for svc in sorted(missing_services) + ] + report.status_extended = f"VPC {vpc.id} does not have VPC endpoints for the following Bedrock services: {', '.join(missing_labels)}." + + findings.append(report) + + return findings + + @staticmethod + def _get_bedrock_active_regions() -> set[str]: + """Return regions where Bedrock resources or logging are configured.""" + active_regions = set() + + for region, config in bedrock_client.logging_configurations.items(): + if config.enabled: + active_regions.add(region) + + for guardrail in bedrock_client.guardrails.values(): + active_regions.add(guardrail.region) + + for agent in bedrock_agent_client.agents.values(): + active_regions.add(agent.region) + + return active_regions diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/__init__.py b/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe.metadata.json b/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe.metadata.json new file mode 100644 index 0000000000..692390afce --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "aws", + "CheckID": "iam_inline_policy_no_wildcard_marketplace_subscribe", + "CheckTitle": "Inline IAM policy does not allow 'aws-marketplace:Subscribe' on all resources", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AwsIamPolicy", + "ResourceGroup": "IAM", + "Description": "**IAM inline policies** are analyzed to identify statements that grant `aws-marketplace:Subscribe` on all resources (`*`). This action controls the ability to subscribe to AWS Marketplace products, including **Amazon Bedrock foundation models**, and should be scoped to specific product ARNs to enforce least privilege.", + "Risk": "Granting `aws-marketplace:Subscribe` on all resources via inline policies allows subscribing to any Marketplace product, including expensive Bedrock foundation models, leading to uncontrolled costs, shadow AI usage, and compliance violations.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html", + "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "```yaml\nResources:\n ExampleRole:\n Type: AWS::IAM::Role\n Properties:\n Policies:\n - PolicyName: scoped-marketplace-subscribe\n PolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Action:\n - aws-marketplace:Subscribe # FIX: scope to specific product ARNs\n Resource:\n - arn:aws:aws-marketplace::123456789012:product/approved-product-id\n```", + "Other": "1. In the AWS Console, open IAM and go to Users, Roles, or Groups where the inline policy is attached\n2. Select the entity, go to the Permissions tab, and open the inline policy\n3. Click Edit policy and switch to the JSON editor\n4. Replace \"Resource\": \"*\" with specific, approved AWS Marketplace product ARNs\n5. Save changes and re-run the check to confirm it passes", + "Terraform": "```hcl\nresource \"aws_iam_role_policy\" \"scoped_marketplace_subscribe\" {\n name = \"scoped-marketplace-subscribe\"\n role = aws_iam_role.example.id\n\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [{\n Effect = \"Allow\"\n Action = [\"aws-marketplace:Subscribe\"]\n Resource = [\"arn:aws:aws-marketplace::123456789012:product/approved-product-id\"] # FIX: scope to specific products\n }]\n })\n}\n```" + }, + "Recommendation": { + "Text": "Replace `Resource: \"*\"` with specific, approved AWS Marketplace product ARNs. Prefer managed policies over inline and apply the principle of least privilege to `aws-marketplace:Subscribe` permissions to prevent unauthorized subscriptions to costly Bedrock models and other Marketplace products.", + "Url": "https://hub.prowler.com/check/iam_inline_policy_no_wildcard_marketplace_subscribe" + } + }, + "Categories": [ + "gen-ai", + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [ + "iam_policy_no_wildcard_marketplace_subscribe" + ], + "Notes": "This check only evaluates IAM inline policies. See iam_policy_no_wildcard_marketplace_subscribe for the customer-managed policy variant." +} diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe.py b/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe.py new file mode 100644 index 0000000000..38abdf3984 --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe.py @@ -0,0 +1,33 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.iam_client import iam_client +from prowler.providers.aws.services.iam.lib.policy import ( + policy_allows_marketplace_subscribe_on_all_resources, +) + + +class iam_inline_policy_no_wildcard_marketplace_subscribe(Check): + def execute(self) -> list[Check_Report_AWS]: + findings = [] + for policy in iam_client.policies.values(): + if policy.type == "Inline": + report = Check_Report_AWS(metadata=self.metadata(), resource=policy) + report.region = iam_client.region + report.resource_id = f"{policy.entity}/{policy.name}" + report.status = "PASS" + + resource_type_str = report.resource_arn.split(":")[-1].split("/")[0] + resource_attached = report.resource_arn.split("/")[-1] + + report.status_extended = f"Inline policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} does not allow 'aws-marketplace:Subscribe' on all resources." + + if ( + policy.document + and policy_allows_marketplace_subscribe_on_all_resources( + policy.document + ) + ): + report.status = "FAIL" + report.status_extended = f"Inline policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} allows 'aws-marketplace:Subscribe' on all resources." + + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/__init__.py b/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.metadata.json b/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.metadata.json new file mode 100644 index 0000000000..22c7a44cfe --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "aws", + "CheckID": "iam_policy_no_wildcard_marketplace_subscribe", + "CheckTitle": "Custom IAM policy does not allow 'aws-marketplace:Subscribe' on all resources", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AwsIamPolicy", + "ResourceGroup": "IAM", + "Description": "**Customer-managed IAM policies** are examined for statements that grant `aws-marketplace:Subscribe` on all resources (`*`). This action controls the ability to subscribe to AWS Marketplace products, including **Amazon Bedrock foundation models**, and should be scoped to specific product ARNs to enforce least privilege.", + "Risk": "Granting `aws-marketplace:Subscribe` on all resources allows subscribing to any Marketplace product, including expensive Bedrock foundation models, leading to uncontrolled costs, shadow AI usage, and compliance violations from unapproved deployments.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html", + "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" + ], + "Remediation": { + "Code": { + "CLI": "aws iam create-policy-version --policy-arn --policy-document '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"aws-marketplace:Subscribe\"],\"Resource\":\"arn:aws:aws-marketplace:::product/\"}]}' --set-as-default", + "NativeIaC": "```yaml\nResources:\n ScopedMarketplacePolicy:\n Type: AWS::IAM::ManagedPolicy\n Properties:\n PolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Action:\n - aws-marketplace:Subscribe # FIX: scope to specific product ARNs\n Resource:\n - arn:aws:aws-marketplace::123456789012:product/approved-product-id\n```", + "Other": "1. In the AWS Console, open IAM > Policies\n2. Find the custom policy that allows aws-marketplace:Subscribe on Resource: *\n3. Click Edit and switch to the JSON editor\n4. Replace \"Resource\": \"*\" with specific, approved AWS Marketplace product ARNs\n5. Save changes and re-run the check to confirm it passes", + "Terraform": "```hcl\nresource \"aws_iam_policy\" \"scoped_marketplace_subscribe\" {\n name = \"scoped-marketplace-subscribe\"\n\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [{\n Effect = \"Allow\"\n Action = [\"aws-marketplace:Subscribe\"]\n Resource = [\"arn:aws:aws-marketplace::123456789012:product/approved-product-id\"] # FIX: scope to specific products\n }]\n })\n}\n```" + }, + "Recommendation": { + "Text": "Replace `Resource: \"*\"` with specific, approved AWS Marketplace product ARNs. Apply the principle of least privilege to `aws-marketplace:Subscribe` permissions to prevent unauthorized subscriptions to costly Bedrock models and other Marketplace products.", + "Url": "https://hub.prowler.com/check/iam_policy_no_wildcard_marketplace_subscribe" + } + }, + "Categories": [ + "gen-ai", + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe" + ], + "Notes": "This check only evaluates customer-managed IAM policies. AWS managed policies are maintained by AWS and cannot be modified. See iam_inline_policy_no_wildcard_marketplace_subscribe for the inline policy variant." +} diff --git a/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.py b/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.py new file mode 100644 index 0000000000..fe0e740a5e --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.py @@ -0,0 +1,28 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.iam_client import iam_client +from prowler.providers.aws.services.iam.lib.policy import ( + policy_allows_marketplace_subscribe_on_all_resources, +) + + +class iam_policy_no_wildcard_marketplace_subscribe(Check): + def execute(self) -> list[Check_Report_AWS]: + findings = [] + for policy in iam_client.policies.values(): + if policy.type == "Custom": + report = Check_Report_AWS(metadata=self.metadata(), resource=policy) + report.region = iam_client.region + report.status = "PASS" + report.status_extended = f"Custom Policy {policy.name} does not allow 'aws-marketplace:Subscribe' on all resources." + + if ( + policy.document + and policy_allows_marketplace_subscribe_on_all_resources( + policy.document + ) + ): + report.status = "FAIL" + report.status_extended = f"Custom Policy {policy.name} allows 'aws-marketplace:Subscribe' on all resources." + + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/__init__.py b/prowler/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock.metadata.json b/prowler/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock.metadata.json new file mode 100644 index 0000000000..0038a055f4 --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock.metadata.json @@ -0,0 +1,44 @@ +{ + "Provider": "aws", + "CheckID": "iam_role_access_not_stale_to_bedrock", + "CheckTitle": "Regular Bedrock access ensures IAM roles retain only actively used permissions", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:iam::account-id:role/resource-id", + "Severity": "medium", + "ResourceType": "AwsIamRole", + "ResourceGroup": "IAM", + "Description": "IAM roles granted **Bedrock** permissions are evaluated for recent service usage.\n\nRoles whose last Bedrock access exceeds the configured threshold (default **60 days**) or that have **never** accessed Bedrock are flagged, indicating stale permissions that should be reviewed.", + "Risk": "Stale Bedrock permissions widen the **blast radius** of a credential compromise.\n\nAn attacker who assumes a role with unused Bedrock permissions can invoke foundation models, exfiltrate data through model responses, or incur significant costs — all without triggering expected usage patterns.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_access-advisor.html", + "https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html", + "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#remove-credentials" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Open the IAM console and select the role\n2. Review the **Access Advisor** tab to confirm Bedrock has not been accessed recently\n3. Remove or detach any policies granting Bedrock permissions that are no longer needed\n4. If the role still requires Bedrock access, verify usage and reduce scope to least privilege", + "Terraform": "" + }, + "Recommendation": { + "Text": "Apply the **principle of least privilege** by regularly reviewing IAM Access Advisor data and revoking Bedrock permissions that are no longer actively used.\n\nEstablish a periodic access review process and automate alerts for stale permissions to maintain a minimal attack surface.", + "Url": "https://hub.prowler.com/check/iam_role_access_not_stale_to_bedrock" + } + }, + "Categories": [ + "identity-access", + "gen-ai" + ], + "DependsOn": [], + "RelatedTo": [ + "bedrock_api_key_no_administrative_privileges", + "bedrock_api_key_no_long_term_credentials" + ], + "Notes": "The staleness threshold is configurable via the `max_unused_bedrock_access_days` audit config key (default: 60 days)." +} diff --git a/prowler/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock.py b/prowler/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock.py new file mode 100644 index 0000000000..d0287ab970 --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock.py @@ -0,0 +1,65 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.iam_client import iam_client +from prowler.providers.aws.services.iam.lib.policy import ( + evaluate_bedrock_staleness, + find_bedrock_service, +) + + +class iam_role_access_not_stale_to_bedrock(Check): + """Detect IAM roles with stale Bedrock permissions. + + This check evaluates whether IAM roles with Bedrock service permissions + have actively used those permissions within the configured threshold + (default 60 days). + + - PASS: The role has accessed Bedrock within the allowed period. + - FAIL: The role has Bedrock permissions but has not used them within + the allowed period or has never used them. + """ + + def execute(self) -> list[Check_Report_AWS]: + """Execute the Bedrock access staleness check for IAM roles. + + Iterates over IAM roles, inspecting service last accessed data for + the ``bedrock`` namespace. Roles whose last Bedrock access exceeds + the configured threshold are reported as non-compliant. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + max_unused_bedrock_days = iam_client.audit_config.get( + "max_unused_bedrock_access_days", 60 + ) + + for ( + role_data, + last_accessed_services, + ) in iam_client.role_last_accessed_services.items(): + role_name = role_data[0] + role_arn = role_data[1] + + bedrock_service = find_bedrock_service(last_accessed_services) + if bedrock_service is None: + continue + + report = Check_Report_AWS( + metadata=self.metadata(), + resource={"name": role_name, "arn": role_arn}, + ) + report.resource_id = role_name + report.resource_arn = role_arn + report.region = iam_client.region + if iam_client.roles is not None: + for iam_role in iam_client.roles: + if iam_role.arn == role_arn: + report.resource_tags = iam_role.tags + break + + evaluate_bedrock_staleness( + report, bedrock_service, max_unused_bedrock_days, role_name, "Role" + ) + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/iam/iam_service.py b/prowler/providers/aws/services/iam/iam_service.py index a8469d3b9d..d6cb150178 100644 --- a/prowler/providers/aws/services/iam/iam_service.py +++ b/prowler/providers/aws/services/iam/iam_service.py @@ -1,5 +1,6 @@ import csv from datetime import datetime +from time import sleep from typing import Optional from botocore.client import ClientError @@ -92,6 +93,12 @@ class IAM(AWSService): self._get_access_keys_metadata() self.last_accessed_services = {} self._get_last_accessed_services() + self.role_last_accessed_services = {} + if ( + "iam_role_access_not_stale_to_bedrock" + in provider.audit_metadata.expected_checks + ): + self._get_role_last_accessed_services() self.user_temporary_credentials_usage = {} self._get_user_temporary_credentials_usage() self.organization_features = [] @@ -901,6 +908,74 @@ class IAM(AWSService): f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _get_role_last_accessed_services(self): + """Retrieve service last accessed details for all IAM roles. + + Uses a fire-all-then-collect pattern: all generate calls are + submitted first so the jobs run server-side in parallel, then + results are collected in a second pass. + """ + logger.info("IAM - Getting Role Last Accessed Services ...") + try: + if self.roles is None: + return + + # Phase 1: fire all generate requests + pending_jobs = [] + for role in self.roles: + try: + details = self.client.generate_service_last_accessed_details( + Arn=role.arn + ) + pending_jobs.append((role.name, role.arn, details["JobId"])) + except ClientError as error: + if error.response["Error"]["Code"] == "NoSuchEntity": + logger.warning( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + else: + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + except Exception as error: + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + # Phase 2: collect results + max_retries = 60 + for role_name, role_arn, job_id in pending_jobs: + try: + retries = 0 + response = self.client.get_service_last_accessed_details( + JobId=job_id + ) + while response["JobStatus"] == "IN_PROGRESS": + retries += 1 + if retries > max_retries: + logger.warning( + f"{self.region} -- Timeout waiting for service last accessed details for role {role_name}" + ) + break + sleep(1) + response = self.client.get_service_last_accessed_details( + JobId=job_id + ) + if response["JobStatus"] == "COMPLETED": + self.role_last_accessed_services[(role_name, role_arn)] = ( + response.get("ServicesLastAccessed", []) + ) + + except Exception as error: + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + except Exception as error: + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + def _get_access_keys_metadata(self): logger.info("IAM - Getting Access Keys Metadata ...") try: diff --git a/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/__init__.py b/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock.metadata.json b/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock.metadata.json new file mode 100644 index 0000000000..04547bfbc5 --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock.metadata.json @@ -0,0 +1,44 @@ +{ + "Provider": "aws", + "CheckID": "iam_user_access_not_stale_to_bedrock", + "CheckTitle": "Regular Bedrock access ensures IAM users retain only actively used permissions", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:iam::account-id:user/resource-id", + "Severity": "medium", + "ResourceType": "AwsIamUser", + "ResourceGroup": "IAM", + "Description": "IAM users granted **Bedrock** permissions are evaluated for recent service usage.\n\nUsers whose last Bedrock access exceeds the configured threshold (default **60 days**) or that have **never** accessed Bedrock are flagged, indicating stale permissions that should be reviewed.", + "Risk": "Stale Bedrock permissions widen the **blast radius** of a credential compromise.\n\nAn attacker who gains access to a user with unused Bedrock permissions can invoke foundation models, exfiltrate data through model responses, or incur significant costs — all without triggering expected usage patterns.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_access-advisor.html", + "https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html", + "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#remove-credentials" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Open the IAM console and select the user\n2. Review the **Access Advisor** tab to confirm Bedrock has not been accessed recently\n3. Remove or detach any policies granting Bedrock permissions that are no longer needed\n4. If the user still requires Bedrock access, verify usage and reduce scope to least privilege", + "Terraform": "" + }, + "Recommendation": { + "Text": "Apply the **principle of least privilege** by regularly reviewing IAM Access Advisor data and revoking Bedrock permissions that are no longer actively used.\n\nEstablish a periodic access review process and automate alerts for stale permissions to maintain a minimal attack surface.", + "Url": "https://hub.prowler.com/check/iam_user_access_not_stale_to_bedrock" + } + }, + "Categories": [ + "identity-access", + "gen-ai" + ], + "DependsOn": [], + "RelatedTo": [ + "bedrock_api_key_no_administrative_privileges", + "bedrock_api_key_no_long_term_credentials" + ], + "Notes": "The staleness threshold is configurable via the `max_unused_bedrock_access_days` audit config key (default: 60 days)." +} diff --git a/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock.py b/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock.py new file mode 100644 index 0000000000..09fb215782 --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock.py @@ -0,0 +1,64 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.iam_client import iam_client +from prowler.providers.aws.services.iam.lib.policy import ( + evaluate_bedrock_staleness, + find_bedrock_service, +) + + +class iam_user_access_not_stale_to_bedrock(Check): + """Detect IAM users with stale Bedrock permissions. + + This check evaluates whether IAM users with Bedrock service permissions + have actively used those permissions within the configured threshold + (default 60 days). + + - PASS: The user has accessed Bedrock within the allowed period. + - FAIL: The user has Bedrock permissions but has not used them within + the allowed period or has never used them. + """ + + def execute(self) -> list[Check_Report_AWS]: + """Execute the Bedrock access staleness check for IAM users. + + Iterates over IAM users, inspecting service last accessed data for + the ``bedrock`` namespace. Users whose last Bedrock access exceeds + the configured threshold are reported as non-compliant. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + max_unused_bedrock_days = iam_client.audit_config.get( + "max_unused_bedrock_access_days", 60 + ) + + for ( + user_data, + last_accessed_services, + ) in iam_client.last_accessed_services.items(): + user_name = user_data[0] + user_arn = user_data[1] + + bedrock_service = find_bedrock_service(last_accessed_services) + if bedrock_service is None: + continue + + report = Check_Report_AWS( + metadata=self.metadata(), + resource={"name": user_name, "arn": user_arn}, + ) + report.resource_id = user_name + report.resource_arn = user_arn + report.region = iam_client.region + for iam_user in iam_client.users: + if iam_user.arn == user_arn: + report.resource_tags = iam_user.tags + break + + evaluate_bedrock_staleness( + report, bedrock_service, max_unused_bedrock_days, user_name, "User" + ) + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/iam/lib/policy.py b/prowler/providers/aws/services/iam/lib/policy.py index ecf09d312f..562e270df6 100644 --- a/prowler/providers/aws/services/iam/lib/policy.py +++ b/prowler/providers/aws/services/iam/lib/policy.py @@ -1,9 +1,12 @@ import re +from datetime import datetime, timezone from ipaddress import ip_address, ip_network from typing import Optional, Tuple +from dateutil.parser import parse from py_iam_expand.actions import InvalidActionHandling, expand_actions +from prowler.lib.check.models import Check_Report_AWS from prowler.lib.logger import logger from prowler.providers.aws.aws_provider import read_aws_regions_file @@ -994,6 +997,93 @@ def is_codebuild_using_allowed_github_org( return False, None +def policy_allows_marketplace_subscribe_on_all_resources( + policy_document: dict, +) -> bool: + """Check if a policy document can allow aws-marketplace:Subscribe on Resource:*. + + Inspects statements with Resource ``*`` for Allow effects that grant + ``aws-marketplace:Subscribe`` via ``Action`` or ``NotAction`` (wildcard + patterns expanded through ``expand_actions``). Unconditional Deny + statements on Resource ``*`` (via either ``Action`` or ``NotAction``) + covering the same action take precedence. Conditional Deny statements + are not treated as global cancellation because the condition scope is + request-dependent and is not evaluated here. Conditional Allow + statements are still treated as potentially allowing access on + ``Resource:*``, since the wildcard scope remains risky even when + gated by a condition. + + Args: + policy_document: The IAM policy document to analyse. + + Returns: + True if the policy can allow aws-marketplace:Subscribe on all + resources, False otherwise. + """ + if not policy_document or "Statement" not in policy_document: + return False + + target_actions = set( + expand_actions( + "aws-marketplace:Subscribe", + InvalidActionHandling.REMOVE, + ) + ) + if not target_actions: + target_actions = {"aws-marketplace:Subscribe"} + + statements = policy_document.get("Statement", []) + if not isinstance(statements, list): + statements = [statements] + + allowed_on_all = set() + denied_on_all = set() + all_aws_actions = None + + for statement in statements: + effect = statement.get("Effect", "") + if not isinstance(effect, str): + continue + effect_lower = effect.strip().lower() + if effect_lower not in ("allow", "deny"): + continue + + resources = statement.get("Resource", []) + if isinstance(resources, str): + resources = [resources] + if "*" not in resources: + continue + + if effect_lower == "deny" and "Condition" in statement: + continue + + statement_actions = set() + action_patterns = _get_patterns_from_standard_value(statement.get("Action")) + for pattern in action_patterns: + statement_actions.update( + expand_actions(pattern, InvalidActionHandling.REMOVE) + ) + + not_action_patterns = _get_patterns_from_standard_value( + statement.get("NotAction") + ) + if not_action_patterns: + if all_aws_actions is None: + all_aws_actions = set(expand_actions("*", InvalidActionHandling.REMOVE)) + exclusions = set() + for pattern in not_action_patterns: + exclusions.update(expand_actions(pattern, InvalidActionHandling.REMOVE)) + statement_actions.update(all_aws_actions.difference(exclusions)) + + if effect_lower == "allow": + allowed_on_all.update(statement_actions) + else: + denied_on_all.update(statement_actions) + + effective = allowed_on_all.difference(denied_on_all) + return bool(target_actions & effective) + + def has_codebuild_trusted_principal(trust_policy: dict) -> bool: """ Returns True if the trust policy allows codebuild.amazonaws.com as a trusted principal, otherwise False. @@ -1031,3 +1121,47 @@ def has_codebuild_trusted_principal(trust_policy: dict) -> bool: ) for s in statements ) + + +def find_bedrock_service(last_accessed_services: list[dict]) -> Optional[dict]: + """Return the Bedrock entry from a service last accessed list.""" + for service in last_accessed_services: + if service.get("ServiceNamespace") == "bedrock": + return service + return None + + +def evaluate_bedrock_staleness( + report: Check_Report_AWS, + bedrock_service: dict, + max_days: int, + identity_name: str, + identity_type: str, +) -> None: + """Populate a check report based on Bedrock access recency.""" + last_authenticated = bedrock_service.get("LastAuthenticated") + if last_authenticated is None: + report.status = "FAIL" + report.status_extended = ( + f"IAM {identity_type} {identity_name} has Bedrock permissions " + f"but has never used them." + ) + return + + if isinstance(last_authenticated, str): + last_authenticated = parse(last_authenticated) + + days_since_access = (datetime.now(timezone.utc) - last_authenticated).days + + if days_since_access > max_days: + report.status = "FAIL" + report.status_extended = ( + f"IAM {identity_type} {identity_name} has not accessed Bedrock " + f"in {days_since_access} days (threshold: {max_days} days)." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"IAM {identity_type} {identity_name} accessed Bedrock " + f"{days_since_access} days ago (threshold: {max_days} days)." + ) diff --git a/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/__init__.py b/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.metadata.json b/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.metadata.json new file mode 100644 index 0000000000..746b53d8fd --- /dev/null +++ b/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.metadata.json @@ -0,0 +1,44 @@ +{ + "Provider": "aws", + "CheckID": "stepfunctions_statemachine_no_secrets_in_definition", + "CheckTitle": "Step Functions state machine has no sensitive credentials in its definition", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "TTPs/Credential Access", + "Effects/Data Exposure", + "Sensitive Data Identifications/Security" + ], + "ServiceName": "stepfunctions", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "AwsStepFunctionStateMachine", + "ResourceGroup": "serverless", + "Description": "**AWS Step Functions state machines** are inspected for **hardcoded secrets** (keys, tokens, passwords) embedded directly in the state machine **definition** (Amazon States Language JSON).\n\nSuch values indicate sensitive data is stored directly in task parameters instead of being sourced securely.", + "Risk": "Plaintext secrets in state machine definitions reduce confidentiality: values can be viewed in the AWS Console, CLI, and may leak into execution logs or public outputs. Compromised credentials enable unauthorized AWS actions, lateral movement, and data exfiltration.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/step-functions/latest/dg/concepts-amazon-states-language.html", + "https://docs.aws.amazon.com/step-functions/latest/dg/security-best-practices.html", + "https://docs.aws.amazon.com/secretsmanager/latest/userguide/integrating_how-services-use-secrets_step-functions.html", + "https://docs.aws.amazon.com/systems-manager/latest/userguide/integration-ps-secretsmanager.html" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::StepFunctions::StateMachine\n Properties:\n StateMachineName: \n RoleArn: \n DefinitionString: |\n {\n \"Comment\": \"Example state machine\",\n \"StartAt\": \"MyTask\",\n \"States\": {\n \"MyTask\": {\n \"Type\": \"Task\",\n \"Resource\": \"arn:aws:states:::aws-sdk:secretsmanager:getSecretValue\",\n \"Parameters\": {\n \"SecretId\": \"\"\n },\n \"End\": true\n }\n }\n }\n```", + "Other": "1. In AWS Console, go to Step Functions and open your state machine\n2. Click Edit\n3. Remove any hardcoded secrets from the definition\n4. Use AWS Secrets Manager or Parameter Store to retrieve secrets at runtime\n5. Grant the state machine IAM role permission to access the secret\n6. Save the updated definition", + "Terraform": "```hcl\nresource \"aws_sfn_state_machine\" \"\" {\n name = \"\"\n role_arn = \"\"\n\n definition = jsonencode({\n Comment = \"Example state machine\"\n StartAt = \"MyTask\"\n States = {\n MyTask = {\n Type = \"Task\"\n Resource = \"arn:aws:states:::aws-sdk:secretsmanager:getSecretValue\"\n Parameters = {\n SecretId = \"\" # Reference secret by name, never hardcode value\n }\n End = true\n }\n }\n })\n}\n```" + }, + "Recommendation": { + "Text": "Store secrets outside the state machine definition and retrieve them securely at runtime using **AWS Secrets Manager** or **AWS Systems Manager Parameter Store**.\n- Use the `aws-sdk:secretsmanager:getSecretValue` integration to fetch secrets dynamically\n- Enforce **least privilege** on the state machine IAM role\n- Rotate secrets regularly and never embed them in the definition", + "Url": "https://hub.prowler.com/check/stepfunctions_statemachine_no_secrets_in_definition" + } + }, + "Categories": [ + "secrets" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.py b/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.py new file mode 100644 index 0000000000..db04710029 --- /dev/null +++ b/prowler/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition.py @@ -0,0 +1,45 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.lib.utils.utils import detect_secrets_scan +from prowler.providers.aws.services.stepfunctions.stepfunctions_client import ( + stepfunctions_client, +) + + +class stepfunctions_statemachine_no_secrets_in_definition(Check): + """Check that AWS Step Functions state machine definitions contain no hardcoded secrets.""" + + def execute(self) -> list[Check_Report_AWS]: + findings = [] + secrets_ignore_patterns = stepfunctions_client.audit_config.get( + "secrets_ignore_patterns", [] + ) + for state_machine in stepfunctions_client.state_machines.values(): + report = Check_Report_AWS(metadata=self.metadata(), resource=state_machine) + report.status = "PASS" + report.status_extended = f"No secrets found in Step Functions state machine {state_machine.name} definition." + + if state_machine.definition: + detect_secrets_output = detect_secrets_scan( + data=state_machine.definition, + excluded_secrets=secrets_ignore_patterns, + detect_secrets_plugins=stepfunctions_client.audit_config.get( + "detect_secrets_plugins", + ), + ) + + if detect_secrets_output: + secrets_string = ", ".join( + [ + f"{secret['type']} on line {secret['line_number']}" + for secret in detect_secrets_output + ] + ) + report.status = "FAIL" + report.status_extended = ( + f"Potential {'secrets' if len(detect_secrets_output) > 1 else 'secret'} " + f"found in Step Functions state machine {state_machine.name} definition " + f"-> {secrets_string}." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/googleworkspace/lib/service/service.py b/prowler/providers/googleworkspace/lib/service/service.py index 2b12cda333..a6f27a4b1b 100644 --- a/prowler/providers/googleworkspace/lib/service/service.py +++ b/prowler/providers/googleworkspace/lib/service/service.py @@ -41,6 +41,22 @@ class GoogleWorkspaceService: ) return None + @staticmethod + def _is_customer_level_policy(policy: dict) -> bool: + """Check if a policy applies at the customer (domain-wide) level. + + The Cloud Identity Policy API returns policies at multiple + organizational levels (customer, OU, group). Customer-level + policies have no group targeting and no sub-OU targeting in + their policyQuery. + """ + policy_query = policy.get("policyQuery", {}) + if policy_query.get("group"): + return False + if policy_query.get("orgUnit"): + return False + return True + def _handle_api_error(self, error, context: str, resource_name: str = ""): """ Centralized Google Workspace API error handling. diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.metadata.json b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.metadata.json index 3a47f981d0..4c7e71c102 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.metadata.json +++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_invitations_warning/calendar_external_invitations_warning.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Google Calendar **warns users** when they invite guests from outside the organization to an event. This prompt gives users a chance to reconsider before sharing meeting details with external parties, reducing the likelihood of **accidental information disclosure** through calendar invitations.", + "Description": "The domain-wide Google Calendar configuration **warns users** when they invite guests from outside the organization to an event. This prompt gives users a chance to reconsider before sharing meeting details with external parties, reducing the likelihood of **accidental information disclosure** through calendar invitations.", "Risk": "Without external invitation warnings, users may unintentionally include **external guests** in internal meetings, exposing **confidential meeting details**, agendas, and internal attendee lists to unauthorized parties. This is a common vector for inadvertent data leakage through everyday calendar actions.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.metadata.json b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.metadata.json index 536e8413f2..87a82946e5 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.metadata.json +++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_primary_calendar/calendar_external_sharing_primary_calendar.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Primary calendars in the Google Workspace domain share **only free/busy information** with external users. When external sharing is set to share full event details, sensitive information such as meeting titles, attendees, locations, and descriptions is exposed to users outside the organization.", + "Description": "The domain-wide default for primary calendars shares **only free/busy information** with external users. When external sharing is set to share full event details, sensitive information such as meeting titles, attendees, locations, and descriptions is exposed to users outside the organization.", "Risk": "Overly permissive external sharing of primary calendars exposes **sensitive meeting metadata** — titles, attendees, locations, and descriptions — to users outside the organization. This increases the risk of **information disclosure**, **social engineering**, and **targeted phishing** based on insights into organizational activities.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.metadata.json b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.metadata.json index 1dadf4965c..bd2bbf5be7 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.metadata.json +++ b/prowler/providers/googleworkspace/services/calendar/calendar_external_sharing_secondary_calendar/calendar_external_sharing_secondary_calendar.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Secondary calendars in the Google Workspace domain share **only free/busy information** with external users. Secondary calendars are additional calendars users create beyond their primary calendar (e.g., for projects, teams, or personal events), and are commonly used to organize sensitive or focused activities that should not be visible to external parties.", + "Description": "The domain-wide default for secondary calendars shares **only free/busy information** with external users. Secondary calendars are additional calendars users create beyond their primary calendar (e.g., for projects, teams, or personal events), and are commonly used to organize sensitive or focused activities that should not be visible to external parties.", "Risk": "Overly permissive external sharing of secondary calendars exposes **project-specific or team-specific event details** to users outside the organization. Because secondary calendars often hold more targeted activities (e.g., product launches, internal reviews), unrestricted external sharing increases the risk of **information disclosure** and **competitive intelligence leakage**.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/googleworkspace/services/calendar/calendar_service.py b/prowler/providers/googleworkspace/services/calendar/calendar_service.py index ae71c0fdf1..f976b654c0 100644 --- a/prowler/providers/googleworkspace/services/calendar/calendar_service.py +++ b/prowler/providers/googleworkspace/services/calendar/calendar_service.py @@ -38,6 +38,8 @@ class Calendar(GoogleWorkspaceService): response = request.execute() for policy in response.get("policies", []): + if not self._is_customer_level_policy(policy): + continue setting = policy.get("setting", {}) setting_type = setting.get("type", "").removeprefix("settings/") value = setting.get("value", {}) diff --git a/prowler/providers/googleworkspace/services/drive/__init__.py b/prowler/providers/googleworkspace/services/drive/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/__init__.py b/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.metadata.json new file mode 100644 index 0000000000..a79283f9c3 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "drive_access_checker_recipients_only", + "CheckTitle": "Drive Access Checker is configured to recipients only", + "CheckType": [], + "ServiceName": "drive", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide Access Checker configuration ensures that when a user shares a Drive file via a Google product other than Drive itself (e.g. by pasting a link in Gmail), the suggestions never expand sharing to a wider audience or to anyone with the link. Access Checker is set to **recipients only**.", + "Risk": "If Access Checker suggests broader audiences or public visibility, users may **inadvertently widen access** to a file beyond the people they intended to share with. This is a common cause of unintentional internal or external over-sharing.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/60781", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Drive and Docs**\n3. Click **Sharing settings** > **Sharing options**\n4. Under **Access Checker**, select **Recipients only**\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Configure the Drive Access Checker to suggest sharing only with the explicit recipients of a link. This prevents accidental over-sharing through Gmail and other Google integrations.", + "Url": "https://hub.prowler.com/check/drive_access_checker_recipients_only" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [ + "drive_external_sharing_warn_users", + "drive_publishing_files_disabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.py b/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.py new file mode 100644 index 0000000000..33aea36f18 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only.py @@ -0,0 +1,55 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.drive.drive_client import drive_client + + +class drive_access_checker_recipients_only(Check): + """Check that Access Checker is configured to recipients only + + This check verifies that the domain-level Drive and Docs Access Checker + setting suggests granting access only to the explicit recipients of a + shared link, rather than expanding access to wider audiences or making + files publicly accessible. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if drive_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=drive_client.provider.identity, + resource_name=drive_client.provider.identity.domain, + resource_id=drive_client.provider.identity.customer_id, + customer_id=drive_client.provider.identity.customer_id, + location="global", + ) + + access_checker = drive_client.policies.access_checker_suggestions + + if access_checker == "RECIPIENTS_ONLY": + report.status = "PASS" + report.status_extended = ( + f"Drive and Docs Access Checker in domain " + f"{drive_client.provider.identity.domain} is restricted to " + f"recipients only." + ) + else: + report.status = "FAIL" + if access_checker is None: + report.status_extended = ( + f"Drive and Docs Access Checker is not explicitly " + f"configured in domain {drive_client.provider.identity.domain}. " + f"Access Checker should be set to recipients only." + ) + else: + report.status_extended = ( + f"Drive and Docs Access Checker in domain " + f"{drive_client.provider.identity.domain} is set to " + f"{access_checker}. Access Checker should be set to recipients only." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/drive/drive_client.py b/prowler/providers/googleworkspace/services/drive/drive_client.py new file mode 100644 index 0000000000..e52197020f --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.googleworkspace.services.drive.drive_service import Drive + +drive_client = Drive(Provider.get_global_provider()) diff --git a/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/__init__.py b/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.metadata.json new file mode 100644 index 0000000000..43a08b0cab --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.metadata.json @@ -0,0 +1,35 @@ +{ + "Provider": "googleworkspace", + "CheckID": "drive_desktop_access_disabled", + "CheckTitle": "Google Drive for desktop is disabled", + "CheckType": [], + "ServiceName": "drive", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide default **disables Google Drive for desktop** for the organization. The Drive for desktop client synchronizes Drive content to local devices and uses its own \"offline\" mechanism that does not respect the central offline-access device policy, so disabling it closes a synchronization channel that would otherwise place organizational content on potentially unmanaged endpoints.", + "Risk": "When Drive for desktop is enabled, organizational files are **synchronized to local devices** and remain accessible if the device is lost, stolen, or compromised. Because Drive for desktop bypasses the central offline-access controls, this channel is a frequently overlooked path for sensitive data to leave organization-managed environments.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/7491144", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Drive and Docs**\n3. Click **Features and Applications** > **Google Drive for desktop**\n4. **Uncheck** *Allow Google Drive for desktop in your organization*\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable Google Drive for desktop to prevent local synchronization of organizational content. This reduces the risk of data loss when devices are lost or stolen and closes a channel that bypasses central offline-access controls.", + "Url": "https://hub.prowler.com/check/drive_desktop_access_disabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.py b/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.py new file mode 100644 index 0000000000..d9fd8eac13 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled.py @@ -0,0 +1,57 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.drive.drive_client import drive_client + + +class drive_desktop_access_disabled(Check): + """Check that Google Drive for desktop is disabled + + This check verifies that the domain-level Drive and Docs policy disables + Google Drive for desktop. The desktop client synchronizes Drive content + to local devices and bypasses the standard offline access controls, + so disabling it reduces the risk of organizational data being lost or + stolen along with an end-user device. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if drive_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=drive_client.provider.identity, + resource_name=drive_client.provider.identity.domain, + resource_id=drive_client.provider.identity.customer_id, + customer_id=drive_client.provider.identity.customer_id, + location="global", + ) + + allow_desktop = drive_client.policies.allow_drive_for_desktop + + if allow_desktop is False: + report.status = "PASS" + report.status_extended = ( + f"Google Drive for desktop is disabled in domain " + f"{drive_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if allow_desktop is None: + report.status_extended = ( + f"Google Drive for desktop is not explicitly configured " + f"in domain {drive_client.provider.identity.domain}. " + f"Drive for desktop should be disabled to prevent local " + f"synchronization of organizational content." + ) + else: + report.status_extended = ( + f"Google Drive for desktop is enabled in domain " + f"{drive_client.provider.identity.domain}. " + f"Drive for desktop should be disabled to prevent local " + f"synchronization of organizational content." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/__init__.py b/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.metadata.json new file mode 100644 index 0000000000..8cfd0b4de8 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.metadata.json @@ -0,0 +1,43 @@ +{ + "Provider": "googleworkspace", + "CheckID": "drive_external_sharing_warn_users", + "CheckTitle": "Users are warned when sharing files outside the domain", + "CheckType": [], + "ServiceName": "drive", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide Drive and Docs configuration **warns users** when they attempt to share a file with users outside the organization. This prompt gives users an opportunity to reconsider before exposing organizational content to external parties, reducing the likelihood of **accidental data disclosure** through everyday sharing actions.", + "Risk": "Without external sharing warnings, users may unintentionally share **sensitive documents** with external recipients who are not entitled to the data. This is a common vector for inadvertent leakage of intellectual property, personally identifiable information, and confidential business data through routine Drive sharing.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/60781", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Drive and Docs**\n3. Click **Sharing settings** > **Sharing options**\n4. Under **Sharing outside of **, ensure sharing outside the domain is allowed and check **For files owned by users in warn when sharing outside of **\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable external sharing warnings so users are notified whenever they attempt to share a file outside the organization. This simple prompt helps prevent accidental disclosure of sensitive content to unintended recipients.", + "Url": "https://hub.prowler.com/check/drive_external_sharing_warn_users" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [ + "drive_publishing_files_disabled", + "drive_sharing_allowlisted_domains", + "drive_warn_sharing_with_allowlisted_domains", + "drive_access_checker_recipients_only", + "drive_internal_users_distribute_content" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.py b/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.py new file mode 100644 index 0000000000..973a672f82 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users.py @@ -0,0 +1,54 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.drive.drive_client import drive_client + + +class drive_external_sharing_warn_users(Check): + """Check that users are warned when sharing files outside the domain + + This check verifies that the domain-level Drive and Docs policy warns + users when they attempt to share a file with someone outside the + organization, reducing the risk of accidental information disclosure. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if drive_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=drive_client.provider.identity, + resource_name=drive_client.provider.identity.domain, + resource_id=drive_client.provider.identity.customer_id, + customer_id=drive_client.provider.identity.customer_id, + location="global", + ) + + warning_enabled = drive_client.policies.warn_for_external_sharing + + if warning_enabled is True: + report.status = "PASS" + report.status_extended = ( + f"External sharing warnings for Drive and Docs are enabled " + f"in domain {drive_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if warning_enabled is None: + report.status_extended = ( + f"External sharing warnings for Drive and Docs are not " + f"explicitly configured in domain " + f"{drive_client.provider.identity.domain}. " + f"Users should be warned when sharing files outside the organization." + ) + else: + report.status_extended = ( + f"External sharing warnings for Drive and Docs are disabled " + f"in domain {drive_client.provider.identity.domain}. " + f"Users should be warned when sharing files outside the organization." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/__init__.py b/prowler/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content.metadata.json new file mode 100644 index 0000000000..9194d6d19e --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "drive_internal_users_distribute_content", + "CheckTitle": "Only internal users can distribute content outside the organization", + "CheckType": [], + "ServiceName": "drive", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide default restricts distributing organizational content to shared drives owned by **another organization** to eligible **internal users** only. This prevents external collaborators with manager access to internal shared drives from moving content out of the organization.", + "Risk": "If external users can move files from internal shared drives into shared drives owned by another organization, the organization **loses authoritative control** over its own data. This is a frequently overlooked path for unintentional or malicious data exfiltration through shared drive collaboration.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/60781", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Drive and Docs**\n3. Click **Sharing settings** > **Sharing options**\n4. Under **Distributing content outside of **, select **Only users in **\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict the ability to distribute content to shared drives owned by another organization to internal users only. This preserves authoritative control over organizational data and closes a common shared-drive exfiltration path.", + "Url": "https://hub.prowler.com/check/drive_internal_users_distribute_content" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [ + "drive_external_sharing_warn_users", + "drive_publishing_files_disabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content.py b/prowler/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content.py new file mode 100644 index 0000000000..c0b8ab5c63 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content.py @@ -0,0 +1,56 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.drive.drive_client import drive_client + + +class drive_internal_users_distribute_content(Check): + """Check that only internal users can distribute content externally + + This check verifies that the domain-level Drive and Docs policy restricts + distributing content to shared drives owned by another organization to + eligible internal users only, preventing external collaborators from + moving organizational content out of the domain. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if drive_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=drive_client.provider.identity, + resource_name=drive_client.provider.identity.domain, + resource_id=drive_client.provider.identity.customer_id, + customer_id=drive_client.provider.identity.customer_id, + location="global", + ) + + allowed = drive_client.policies.allowed_parties_for_distributing_content + + if allowed in ("ELIGIBLE_INTERNAL_USERS", "NONE"): + report.status = "PASS" + report.status_extended = ( + f"Distributing content outside the organization in domain " + f"{drive_client.provider.identity.domain} is restricted to " + f"{allowed}." + ) + else: + report.status = "FAIL" + if allowed is None: + report.status_extended = ( + f"Allowed parties for distributing content externally is not " + f"explicitly configured in domain " + f"{drive_client.provider.identity.domain}. " + f"Only internal users should be allowed to distribute content externally." + ) + else: + report.status_extended = ( + f"Distributing content outside the organization in domain " + f"{drive_client.provider.identity.domain} is set to {allowed}. " + f"Only internal users should be allowed to distribute content externally." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/drive/drive_publishing_files_disabled/__init__.py b/prowler/providers/googleworkspace/services/drive/drive_publishing_files_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled.metadata.json new file mode 100644 index 0000000000..9968665841 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled.metadata.json @@ -0,0 +1,41 @@ +{ + "Provider": "googleworkspace", + "CheckID": "drive_publishing_files_disabled", + "CheckTitle": "Publishing Drive files to the web is disabled", + "CheckType": [], + "ServiceName": "drive", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide Drive and Docs default **prevents users from publishing files to the web** or making them visible to the world as public or unlisted. Publishing a file to the web exposes its content to anyone on the internet, often without any audit trail, making it one of the highest-impact misconfigurations available to end users.", + "Risk": "Allowing users to publish Drive files to the web creates a path for **unbounded data exposure**. Sensitive documents, intellectual property, customer data, or internal communications can be made publicly accessible — and indexed by search engines — with a single click, often unintentionally.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/60781", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Drive and Docs**\n3. Click **Sharing settings** > **Sharing options**\n4. Under **Sharing outside of **, **uncheck** *When sharing outside of is allowed, users in can make files and published web content visible to anyone with the link*\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable the ability for users to publish Drive files to the web or make them visible to anyone with the link. This eliminates the most direct path to unintentional public data exposure through Drive.", + "Url": "https://hub.prowler.com/check/drive_publishing_files_disabled" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [ + "drive_external_sharing_warn_users", + "drive_sharing_allowlisted_domains", + "drive_internal_users_distribute_content" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled.py b/prowler/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled.py new file mode 100644 index 0000000000..e6d68baec2 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled.py @@ -0,0 +1,54 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.drive.drive_client import drive_client + + +class drive_publishing_files_disabled(Check): + """Check that publishing Drive files to the web is disabled + + This check verifies that the domain-level Drive and Docs policy prevents + users from publishing files to the web or making them visible to anyone + with the link, blocking unintended public exposure of organizational + content. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if drive_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=drive_client.provider.identity, + resource_name=drive_client.provider.identity.domain, + resource_id=drive_client.provider.identity.customer_id, + customer_id=drive_client.provider.identity.customer_id, + location="global", + ) + + allow_publishing = drive_client.policies.allow_publishing_files + + if allow_publishing is False: + report.status = "PASS" + report.status_extended = ( + f"Publishing files to the web is disabled in domain " + f"{drive_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if allow_publishing is None: + report.status_extended = ( + f"Publishing files to the web is not explicitly configured " + f"in domain {drive_client.provider.identity.domain}. " + f"Users should not be able to publish files to the web or make them public." + ) + else: + report.status_extended = ( + f"Publishing files to the web is enabled in domain " + f"{drive_client.provider.identity.domain}. " + f"Users should not be able to publish files to the web or make them public." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/drive/drive_service.py b/prowler/providers/googleworkspace/services/drive/drive_service.py new file mode 100644 index 0000000000..e7d89f8473 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_service.py @@ -0,0 +1,150 @@ +from typing import Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService + + +class Drive(GoogleWorkspaceService): + """Google Workspace Drive and Docs service for auditing domain-level Drive policies. + + Uses the Cloud Identity Policy API v1 to read Drive and Docs sharing, + shared drive creation, and Drive for desktop settings configured in the + Admin Console. + """ + + def __init__(self, provider): + super().__init__(provider) + self.policies = DrivePolicies() + self.policies_fetched = False + self._fetch_drive_policies() + + def _fetch_drive_policies(self): + """Fetch Drive and Docs policies from the Cloud Identity Policy API v1.""" + logger.info("Drive - Fetching Drive and Docs policies...") + + try: + service = self._build_service("cloudidentity", "v1") + + if not service: + logger.error("Failed to build Cloud Identity service") + return + + request = service.policies().list(pageSize=100) + fetch_succeeded = True + + while request is not None: + try: + response = request.execute() + + for policy in response.get("policies", []): + if not self._is_customer_level_policy(policy): + continue + + setting = policy.get("setting", {}) + setting_type = setting.get("type", "").removeprefix("settings/") + value = setting.get("value", {}) + + if setting_type == "drive_and_docs.external_sharing": + self.policies.external_sharing_mode = value.get( + "externalSharingMode" + ) + self.policies.warn_for_external_sharing = value.get( + "warnForExternalSharing" + ) + self.policies.warn_for_sharing_outside_allowlisted_domains = value.get( + "warnForSharingOutsideAllowlistedDomains" + ) + self.policies.allow_publishing_files = value.get( + "allowPublishingFiles" + ) + self.policies.access_checker_suggestions = value.get( + "accessCheckerSuggestions" + ) + self.policies.allowed_parties_for_distributing_content = ( + value.get("allowedPartiesForDistributingContent") + ) + logger.debug( + "Drive external sharing settings fetched: " + f"mode={self.policies.external_sharing_mode}, " + f"warn={self.policies.warn_for_external_sharing}, " + f"publish={self.policies.allow_publishing_files}" + ) + + elif setting_type == "drive_and_docs.shared_drive_creation": + self.policies.allow_shared_drive_creation = value.get( + "allowSharedDriveCreation" + ) + self.policies.allow_managers_to_override_settings = ( + value.get("allowManagersToOverrideSettings") + ) + self.policies.allow_non_member_access = value.get( + "allowNonMemberAccess" + ) + self.policies.allowed_parties_for_download_print_copy = ( + value.get("allowedPartiesForDownloadPrintCopy") + ) + logger.debug( + "Drive shared drive creation settings fetched: " + f"creation={self.policies.allow_shared_drive_creation}, " + f"managers_override={self.policies.allow_managers_to_override_settings}" + ) + + elif setting_type == "drive_and_docs.drive_for_desktop": + self.policies.allow_drive_for_desktop = value.get( + "allowDriveForDesktop" + ) + logger.debug( + "Drive for desktop setting fetched: " + f"{self.policies.allow_drive_for_desktop}" + ) + + request = service.policies().list_next(request, response) + + except Exception as error: + self._handle_api_error( + error, + "fetching Drive and Docs policies", + self.provider.identity.customer_id, + ) + fetch_succeeded = False + break + + self.policies_fetched = fetch_succeeded + + logger.info( + f"Drive and Docs policies fetched - " + f"External sharing mode: {self.policies.external_sharing_mode}, " + f"Shared drive creation: {self.policies.allow_shared_drive_creation}, " + f"Drive for desktop: {self.policies.allow_drive_for_desktop}" + ) + + except Exception as error: + self._handle_api_error( + error, + "fetching Drive and Docs policies", + self.provider.identity.customer_id, + ) + self.policies_fetched = False + + +class DrivePolicies(BaseModel): + """Model for domain-level Drive and Docs policy settings.""" + + # drive_and_docs.external_sharing + external_sharing_mode: Optional[str] = None + warn_for_external_sharing: Optional[bool] = None + warn_for_sharing_outside_allowlisted_domains: Optional[bool] = None + allow_publishing_files: Optional[bool] = None + access_checker_suggestions: Optional[str] = None + allowed_parties_for_distributing_content: Optional[str] = None + + # drive_and_docs.shared_drive_creation + allow_shared_drive_creation: Optional[bool] = None + allow_managers_to_override_settings: Optional[bool] = None + allow_non_member_access: Optional[bool] = None + allowed_parties_for_download_print_copy: Optional[str] = None + + # drive_and_docs.drive_for_desktop + allow_drive_for_desktop: Optional[bool] = None diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/__init__.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.metadata.json new file mode 100644 index 0000000000..aba88e28b3 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "drive_shared_drive_creation_allowed", + "CheckTitle": "Users are allowed to create new shared drives", + "CheckType": [], + "ServiceName": "drive", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide default **allows users to create new shared drives**. Shared drives are owned by the organization (not the individual user), so content stored in them survives the deletion of the original creator's account, supporting data continuity and reducing the risk of accidental data loss.", + "Risk": "When users cannot create shared drives, they store collaborative content in their personal **My Drive** instead. When that user account is deleted, the data is also deleted, leading to **unintentional data loss** of organizationally significant information. Allowing shared drive creation makes data survivable across account lifecycle events.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/7212025", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Drive and Docs**\n3. Click **Sharing settings** > **Shared drive creation**\n4. **Uncheck** *Prevent users in from creating new shared drives*\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Allow users to create new shared drives. This protects the organization from data loss when user accounts are deleted by ensuring collaborative content lives in organization-owned shared drives instead of personal My Drive folders.", + "Url": "https://hub.prowler.com/check/drive_shared_drive_creation_allowed" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [ + "drive_shared_drive_managers_cannot_override", + "drive_shared_drive_members_only_access", + "drive_shared_drive_disable_download_print_copy" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py new file mode 100644 index 0000000000..c36611326f --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed.py @@ -0,0 +1,57 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.drive.drive_client import drive_client + + +class drive_shared_drive_creation_allowed(Check): + """Check that users are allowed to create new shared drives + + This check verifies that the domain-level Drive and Docs policy permits + users to create new shared drives. Allowing shared drive creation helps + prevent data loss when individual user accounts are deleted, since + content lives in shared drives owned by the organization rather than + in personal My Drive folders. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if drive_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=drive_client.provider.identity, + resource_name=drive_client.provider.identity.domain, + resource_id=drive_client.provider.identity.customer_id, + customer_id=drive_client.provider.identity.customer_id, + location="global", + ) + + allow_creation = drive_client.policies.allow_shared_drive_creation + + if allow_creation is True: + report.status = "PASS" + report.status_extended = ( + f"Users in domain {drive_client.provider.identity.domain} " + f"are allowed to create new shared drives." + ) + else: + report.status = "FAIL" + if allow_creation is None: + report.status_extended = ( + f"Shared drive creation is not explicitly configured in " + f"domain {drive_client.provider.identity.domain}. " + f"Users should be allowed to create new shared drives to avoid " + f"data loss when accounts are deleted." + ) + else: + report.status_extended = ( + f"Users in domain {drive_client.provider.identity.domain} " + f"are prevented from creating new shared drives. " + f"Users should be allowed to create new shared drives to avoid " + f"data loss when accounts are deleted." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/__init__.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.metadata.json new file mode 100644 index 0000000000..d4bf3300b7 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "drive_shared_drive_disable_download_print_copy", + "CheckTitle": "Viewers and commenters cannot download, print, or copy shared drive files", + "CheckType": [], + "ServiceName": "drive", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide default prevents viewers and commenters of files stored in shared drives from **downloading, printing, or copying** the file contents. They can only read and comment on the existing content, preventing bulk extraction of sensitive material from shared drives.", + "Risk": "When viewers and commenters can download, print, or copy shared drive files, they can **bulk-extract sensitive content** — including intellectual property, personally identifiable information, and confidential business documents — using nothing more than read access. This is one of the most direct paths to data exfiltration through Drive.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/7662202", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Drive and Docs**\n3. Click **Sharing settings** > **Shared drive creation**\n4. **Uncheck** *Allow viewers and commenters to download, print, and copy files*\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict download, print, and copy actions in shared drives to editors or managers only. This prevents bulk data exfiltration by users who only need read or comment access to the underlying content.", + "Url": "https://hub.prowler.com/check/drive_shared_drive_disable_download_print_copy" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [ + "drive_shared_drive_creation_allowed", + "drive_shared_drive_managers_cannot_override", + "drive_shared_drive_members_only_access" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.py new file mode 100644 index 0000000000..b112cd70b5 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy.py @@ -0,0 +1,56 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.drive.drive_client import drive_client + + +class drive_shared_drive_disable_download_print_copy(Check): + """Check that download/print/copy is disabled for viewers and commenters + + This check verifies that the domain-level Drive and Docs policy prevents + viewers and commenters of shared drive files from downloading, printing, + or copying their contents — limiting them to read and comment actions + only and reducing the risk of bulk data exfiltration. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if drive_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=drive_client.provider.identity, + resource_name=drive_client.provider.identity.domain, + resource_id=drive_client.provider.identity.customer_id, + customer_id=drive_client.provider.identity.customer_id, + location="global", + ) + + allowed = drive_client.policies.allowed_parties_for_download_print_copy + + if allowed in ("EDITORS_ONLY", "MANAGERS_ONLY"): + report.status = "PASS" + report.status_extended = ( + f"Download, print, and copy in shared drives in domain " + f"{drive_client.provider.identity.domain} is restricted to " + f"{allowed}." + ) + else: + report.status = "FAIL" + if allowed is None: + report.status_extended = ( + f"Download, print, and copy restrictions for shared drive " + f"viewers and commenters are not explicitly configured in " + f"domain {drive_client.provider.identity.domain}. " + f"These actions should be restricted to editors or managers only." + ) + else: + report.status_extended = ( + f"Download, print, and copy in shared drives in domain " + f"{drive_client.provider.identity.domain} is set to {allowed}. " + f"These actions should be restricted to editors or managers only." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/__init__.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override.metadata.json new file mode 100644 index 0000000000..adc3555dc4 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "drive_shared_drive_managers_cannot_override", + "CheckTitle": "Shared drive managers cannot override shared drive settings", + "CheckType": [], + "ServiceName": "drive", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide default prevents members with **manager access** to a shared drive from overriding the shared drive settings established by administrators. This ensures that security controls — such as external access, member-only access, and download restrictions — cannot be relaxed at the individual shared drive level.", + "Risk": "If shared drive managers can override organizational defaults, **unauthorized data exposure** can occur when a manager intentionally or accidentally weakens a shared drive's security posture (for example, allowing external members or enabling download for viewers).", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/7662202", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Drive and Docs**\n3. Click **Sharing settings** > **Shared drive creation**\n4. **Uncheck** *Allow members with manager access to override the settings below*\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Prevent shared drive managers from overriding organizationally established shared drive settings. This ensures that security controls remain consistent across all shared drives and cannot be relaxed by non-administrators.", + "Url": "https://hub.prowler.com/check/drive_shared_drive_managers_cannot_override" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [ + "drive_shared_drive_creation_allowed", + "drive_shared_drive_members_only_access", + "drive_shared_drive_disable_download_print_copy" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override.py new file mode 100644 index 0000000000..49b710ba8f --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override.py @@ -0,0 +1,57 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.drive.drive_client import drive_client + + +class drive_shared_drive_managers_cannot_override(Check): + """Check that shared drive managers cannot override shared drive settings + + This check verifies that the domain-level Drive and Docs policy prevents + members with manager access from overriding the shared drive settings + configured by administrators, ensuring that security controls cannot be + relaxed at the shared drive level. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if drive_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=drive_client.provider.identity, + resource_name=drive_client.provider.identity.domain, + resource_id=drive_client.provider.identity.customer_id, + customer_id=drive_client.provider.identity.customer_id, + location="global", + ) + + allow_override = drive_client.policies.allow_managers_to_override_settings + + if allow_override is False: + report.status = "PASS" + report.status_extended = ( + f"Shared drive managers in domain " + f"{drive_client.provider.identity.domain} cannot override " + f"shared drive settings." + ) + else: + report.status = "FAIL" + if allow_override is None: + report.status_extended = ( + f"Manager override of shared drive settings is not " + f"explicitly configured in domain " + f"{drive_client.provider.identity.domain}. " + f"Managers should not be allowed to override shared drive settings." + ) + else: + report.status_extended = ( + f"Shared drive managers in domain " + f"{drive_client.provider.identity.domain} are allowed to " + f"override shared drive settings. " + f"Managers should not be allowed to override shared drive settings." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/__init__.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access.metadata.json new file mode 100644 index 0000000000..0949a55040 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "googleworkspace", + "CheckID": "drive_shared_drive_members_only_access", + "CheckTitle": "Shared drive file access is restricted to members only", + "CheckType": [], + "ServiceName": "drive", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide default restricts shared drive file access to **explicit members** only. Non-members cannot be added to individual files inside the drive, preserving the shared drive's membership boundary as the authoritative access control surface.", + "Risk": "If non-members can be added to files inside a shared drive, the **drive's membership becomes meaningless** as a security control. Sensitive content scoped to a specific team can be silently extended to users who were never granted access to the drive itself, leading to unintended information disclosure.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/7662202", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Drive and Docs**\n3. Click **Sharing settings** > **Shared drive creation**\n4. **Uncheck** *Allow people who aren't shared drive members to be added to files*\n5. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict shared drive file access to explicit shared drive members. This preserves the drive membership as the authoritative access boundary and prevents silent expansion of access to non-members.", + "Url": "https://hub.prowler.com/check/drive_shared_drive_members_only_access" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [ + "drive_shared_drive_creation_allowed", + "drive_shared_drive_managers_cannot_override", + "drive_shared_drive_disable_download_print_copy" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access.py b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access.py new file mode 100644 index 0000000000..527192e2d6 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access.py @@ -0,0 +1,56 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.drive.drive_client import drive_client + + +class drive_shared_drive_members_only_access(Check): + """Check that shared drive file access is restricted to members only + + This check verifies that the domain-level Drive and Docs policy prevents + people who are not shared drive members from being added to files within + a shared drive, restricting file access to that drive's explicit + membership. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if drive_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=drive_client.provider.identity, + resource_name=drive_client.provider.identity.domain, + resource_id=drive_client.provider.identity.customer_id, + customer_id=drive_client.provider.identity.customer_id, + location="global", + ) + + allow_non_member = drive_client.policies.allow_non_member_access + + if allow_non_member is False: + report.status = "PASS" + report.status_extended = ( + f"Shared drive file access in domain " + f"{drive_client.provider.identity.domain} is restricted to " + f"shared drive members only." + ) + else: + report.status = "FAIL" + if allow_non_member is None: + report.status_extended = ( + f"Shared drive non-member access is not explicitly " + f"configured in domain {drive_client.provider.identity.domain}. " + f"Shared drive file access should be restricted to members only." + ) + else: + report.status_extended = ( + f"Shared drive file access in domain " + f"{drive_client.provider.identity.domain} allows non-members " + f"to be added to files. " + f"Shared drive file access should be restricted to members only." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/__init__.py b/prowler/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains.metadata.json new file mode 100644 index 0000000000..46caa58bf7 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains.metadata.json @@ -0,0 +1,41 @@ +{ + "Provider": "googleworkspace", + "CheckID": "drive_sharing_allowlisted_domains", + "CheckTitle": "Document sharing is restricted to allowlisted domains", + "CheckType": [], + "ServiceName": "drive", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "The domain-wide default restricts external sharing of Drive and Docs files to **a curated list of allowlisted domains**, rather than allowing sharing with arbitrary external recipients. This converts external sharing from an open default into a controlled allow-list that aligns with documented business relationships.", + "Risk": "When external sharing is unrestricted, users can share organizational content with **any external Google account**, including untrusted or unknown parties. Restricting sharing to allowlisted domains drastically reduces the surface area for accidental and malicious data exfiltration through Drive.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/60781", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Drive and Docs**\n3. Click **Sharing settings** > **Sharing options**\n4. Under **Sharing outside of **, select **ALLOWLISTED DOMAINS - Files owned by users in can be shared with Google Accounts in compatible allowlisted domains**\n5. Configure the allowlisted domains list as appropriate for your organization\n6. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict Drive and Docs external sharing to allowlisted domains. This converts external sharing from an open default into a controlled allow-list aligned with documented business relationships and reduces the risk of accidental or malicious data exposure.", + "Url": "https://hub.prowler.com/check/drive_sharing_allowlisted_domains" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [ + "drive_external_sharing_warn_users", + "drive_warn_sharing_with_allowlisted_domains", + "drive_publishing_files_disabled" + ], + "Notes": "" +} diff --git a/prowler/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains.py b/prowler/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains.py new file mode 100644 index 0000000000..276c2f434d --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains.py @@ -0,0 +1,54 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.drive.drive_client import drive_client + + +class drive_sharing_allowlisted_domains(Check): + """Check that document sharing is restricted to allowlisted domains + + This check verifies that the domain-level Drive and Docs policy restricts + external sharing to a list of explicitly allowlisted domains, blocking + sharing with arbitrary external recipients. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if drive_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=drive_client.provider.identity, + resource_name=drive_client.provider.identity.domain, + resource_id=drive_client.provider.identity.customer_id, + customer_id=drive_client.provider.identity.customer_id, + location="global", + ) + + mode = drive_client.policies.external_sharing_mode + + if mode == "ALLOWLISTED_DOMAINS": + report.status = "PASS" + report.status_extended = ( + f"Drive and Docs external sharing in domain " + f"{drive_client.provider.identity.domain} is restricted to " + f"allowlisted domains." + ) + else: + report.status = "FAIL" + if mode is None: + report.status_extended = ( + f"Drive and Docs external sharing mode is not explicitly " + f"configured in domain {drive_client.provider.identity.domain}. " + f"Sharing should be restricted to allowlisted domains." + ) + else: + report.status_extended = ( + f"Drive and Docs external sharing in domain " + f"{drive_client.provider.identity.domain} is set to {mode}. " + f"Sharing should be restricted to allowlisted domains." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/__init__.py b/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.metadata.json b/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.metadata.json new file mode 100644 index 0000000000..e41d074a23 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "googleworkspace", + "CheckID": "drive_warn_sharing_with_allowlisted_domains", + "CheckTitle": "Users are warned when sharing files with allowlisted domains", + "CheckType": [], + "ServiceName": "drive", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "At the domain level, even when external sharing is restricted to allowlisted domains, Google Drive **warns users** before they share a file with a user in an allowlisted domain. This second-step prompt helps users recognize when they are crossing the organizational boundary, even within permitted destinations.", + "Risk": "Allowlisted domains are still external. Users may not realize that even an allowlisted recipient is outside the organization, leading to **unintentional disclosure of sensitive content** to legitimate but external collaborators. A warning prompt at share time mitigates that without preventing the sharing itself.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.google.com/a/answer/60781", + "https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Drive and Docs**\n3. Click **Sharing settings** > **Sharing options**\n4. Under **Sharing outside of **, ensure **ALLOWLISTED DOMAINS** is selected\n5. Check **Warn when files owned by users or shared drives in are shared with users in allowlisted domains**\n6. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable warnings for sharing with allowlisted domains so users are reminded that they are sharing externally, even when the destination is permitted. This preserves the convenience of allowlisted sharing while reducing accidental disclosure.", + "Url": "https://hub.prowler.com/check/drive_warn_sharing_with_allowlisted_domains" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [ + "drive_external_sharing_warn_users", + "drive_sharing_allowlisted_domains" + ], + "Notes": "This check is meaningful only when external sharing is restricted to allowlisted domains. See the related check drive_sharing_allowlisted_domains." +} diff --git a/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.py b/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.py new file mode 100644 index 0000000000..e5f243ae36 --- /dev/null +++ b/prowler/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains.py @@ -0,0 +1,57 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGoogleWorkspace +from prowler.providers.googleworkspace.services.drive.drive_client import drive_client + + +class drive_warn_sharing_with_allowlisted_domains(Check): + """Check that users are warned when sharing with allowlisted domains + + This check verifies that the domain-level Drive and Docs policy warns + users when they share files with users in allowlisted domains, providing + an opportunity to reconsider before sharing externally even within + permitted domains. + """ + + def execute(self) -> List[CheckReportGoogleWorkspace]: + findings = [] + + if drive_client.policies_fetched: + report = CheckReportGoogleWorkspace( + metadata=self.metadata(), + resource=drive_client.provider.identity, + resource_name=drive_client.provider.identity.domain, + resource_id=drive_client.provider.identity.customer_id, + customer_id=drive_client.provider.identity.customer_id, + location="global", + ) + + warn_enabled = ( + drive_client.policies.warn_for_sharing_outside_allowlisted_domains + ) + + if warn_enabled is True: + report.status = "PASS" + report.status_extended = ( + f"Users are warned when sharing files with allowlisted " + f"domains in domain {drive_client.provider.identity.domain}." + ) + else: + report.status = "FAIL" + if warn_enabled is None: + report.status_extended = ( + f"Warning when sharing with allowlisted domains is not " + f"explicitly configured in domain " + f"{drive_client.provider.identity.domain}. " + f"Users should be warned when sharing files with users in allowlisted domains." + ) + else: + report.status_extended = ( + f"Warning when sharing with allowlisted domains is disabled " + f"in domain {drive_client.provider.identity.domain}. " + f"Users should be warned when sharing files with users in allowlisted domains." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/__init__.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.metadata.json new file mode 100644 index 0000000000..3df70aea46 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "m365", + "CheckID": "entra_conditional_access_policy_all_apps_all_users", + "CheckTitle": "Conditional Access policy covers all cloud apps and all users", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "Helper check that verifies whether **Microsoft Entra Conditional Access** includes at least one **enabled** policy scoped to **all cloud applications** and **all users**. Policies that only require a **password change** and **report-only** policies are excluded from passing. The result is intended to support other, more specific Conditional Access checks rather than enforce a baseline on its own.", + "Risk": "Without a Conditional Access policy that targets **all cloud apps** and **all users**, newly added applications or user accounts may not be covered by existing access controls until policies are explicitly updated.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/plan-conditional-access", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com.\n2. Expand **Protection** > **Conditional Access** and select **Policies**.\n3. Click **New policy**.\n4. Under **Users**, select **Include** > **All users**. Exclude break-glass accounts as needed.\n5. Under **Target resources**, select **Include** > **All cloud apps**.\n6. Under **Grant**, select the desired access controls.\n7. Set the policy to **On** and click **Create**.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure there is at least one **enabled** Conditional Access policy that targets **all cloud apps** and **all users** so that other Conditional Access checks have broad coverage to evaluate. Exclude only **break-glass accounts**.", + "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_all_apps_all_users" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Generic helper check intended to support other Conditional Access checks. Conditional Access policies require Microsoft Entra ID P1 or P2 licenses." +} diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.py new file mode 100644 index 0000000000..ecf0968066 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.py @@ -0,0 +1,121 @@ +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client +from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, +) + + +class entra_conditional_access_policy_all_apps_all_users(Check): + """Check if at least one Conditional Access policy covers all cloud apps and all users. + + This check verifies that at least one enabled Conditional Access policy + targets all cloud applications and all users, ensuring baseline protection + across the entire tenant. Policies that only require a password change are + excluded because they do not provide meaningful access control. + + - PASS: An enabled Conditional Access policy covers all apps and all users with no exclusions. + - MANUAL: A policy targets all apps and all users but includes exclusions that require review. + - FAIL: No Conditional Access policy provides coverage for all apps and all users. + """ + + def execute(self) -> list[CheckReportM365]: + """Execute the check to verify Conditional Access coverage for all apps and all users. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + report = CheckReportM365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + report.status = "FAIL" + report.status_extended = ( + "No Conditional Access Policy covers all cloud apps and all users." + ) + + manual_policy = None + reporting_only_policy = None + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + application_conditions = policy.conditions.application_conditions + user_conditions = policy.conditions.user_conditions + if not application_conditions or not user_conditions: + continue + + if "All" not in application_conditions.included_applications: + continue + + if "All" not in user_conditions.included_users: + continue + + # Exclude policies that only require a password change, + # as they do not provide meaningful access control. + if policy.grant_controls.built_in_controls == [ + ConditionalAccessGrantControl.PASSWORD_CHANGE + ]: + continue + + has_exclusions = bool( + application_conditions.excluded_applications + or user_conditions.excluded_users + or user_conditions.excluded_groups + or user_conditions.excluded_roles + ) + + if has_exclusions: + if manual_policy is None: + manual_policy = policy + continue + + if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: + if reporting_only_policy is None: + reporting_only_policy = policy + continue + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.display_name, + resource_id=policy.id, + ) + report.status = "PASS" + report.status_extended = f"Conditional Access Policy {policy.display_name} covers all cloud apps and all users." + findings.append(report) + return findings + + if manual_policy is not None: + report = CheckReportM365( + metadata=self.metadata(), + resource=manual_policy, + resource_name=manual_policy.display_name, + resource_id=manual_policy.id, + ) + report.status = "MANUAL" + report.status_extended = ( + f"Conditional Access Policy {manual_policy.display_name} " + "targets all cloud apps and all users but includes exclusions. " + "Review excluded users/groups/roles/applications and verify " + "compensating policies protect excluded identities and apps." + ) + elif reporting_only_policy is not None: + report = CheckReportM365( + metadata=self.metadata(), + resource=reporting_only_policy, + resource_name=reporting_only_policy.display_name, + resource_id=reporting_only_policy.id, + ) + report.status = "FAIL" + report.status_extended = ( + f"Conditional Access Policy {reporting_only_policy.display_name} " + "covers all cloud apps and all users but is only in report-only mode." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_approved_client_app_required_for_mobile/entra_conditional_access_policy_approved_client_app_required_for_mobile.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_approved_client_app_required_for_mobile/entra_conditional_access_policy_approved_client_app_required_for_mobile.py index dd865064c2..e9560f01ee 100644 --- a/prowler/providers/m365/services/entra/entra_conditional_access_policy_approved_client_app_required_for_mobile/entra_conditional_access_policy_approved_client_app_required_for_mobile.py +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_approved_client_app_required_for_mobile/entra_conditional_access_policy_approved_client_app_required_for_mobile.py @@ -23,13 +23,6 @@ class entra_conditional_access_policy_approved_client_app_required_for_mobile(Ch ConditionalAccessGrantControl.COMPLIANT_APPLICATION, } - @staticmethod - def _normalize_platform(platform: object) -> str: - normalized_platform = getattr(platform, "value", platform) - return ( - normalized_platform.lower() if isinstance(normalized_platform, str) else "" - ) - def execute(self) -> list[CheckReportM365]: """Execute the check logic. @@ -54,22 +47,12 @@ class entra_conditional_access_policy_approved_client_app_required_for_mobile(Ch if not policy.conditions.platform_conditions: continue - included_platforms = { - normalized_platform - for normalized_platform in map( - self._normalize_platform, - policy.conditions.platform_conditions.include_platforms, - ) - if normalized_platform - } - excluded_platforms = { - normalized_platform - for normalized_platform in map( - self._normalize_platform, - policy.conditions.platform_conditions.exclude_platforms, - ) - if normalized_platform - } + included_platforms = set( + policy.conditions.platform_conditions.include_platforms + ) + excluded_platforms = set( + policy.conditions.platform_conditions.exclude_platforms + ) targets_mobile_platforms = ( "all" in included_platforms @@ -102,10 +85,10 @@ class entra_conditional_access_policy_approved_client_app_required_for_mobile(Ch ) if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: report.status = "FAIL" - report.status_extended = f"Conditional Access Policy '{policy.display_name}' reports the requirement of approved client apps or app protection for mobile devices but does not enforce it." + report.status_extended = f"Conditional Access Policy {policy.display_name} reports the requirement of approved client apps or app protection for mobile devices but does not enforce it." else: report.status = "PASS" - report.status_extended = f"Conditional Access Policy '{policy.display_name}' requires approved client apps or app protection for mobile devices." + report.status_extended = f"Conditional Access Policy {policy.display_name} requires approved client apps or app protection for mobile devices." break findings.append(report) diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_block_unknown_device_platforms/__init__.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_block_unknown_device_platforms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_block_unknown_device_platforms/entra_conditional_access_policy_block_unknown_device_platforms.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_block_unknown_device_platforms/entra_conditional_access_policy_block_unknown_device_platforms.metadata.json new file mode 100644 index 0000000000..e4b06e2bce --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_block_unknown_device_platforms/entra_conditional_access_policy_block_unknown_device_platforms.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "m365", + "CheckID": "entra_conditional_access_policy_block_unknown_device_platforms", + "CheckTitle": "Conditional Access policy blocks access from unknown or unsupported device platforms", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "Microsoft Entra **Conditional Access** can block sign-ins from device platforms that the organization does not recognize or support. A policy that includes **all** platforms and excludes the known ones (Android, iOS, Windows, macOS, Linux) effectively blocks any **unknown or unsupported** platform, reducing the attack surface from unmanaged or unexpected devices.", + "Risk": "Without blocking unknown device platforms, attackers may authenticate from **spoofed or uncommon operating systems** that bypass platform-specific security controls. This increases the risk of **unauthorized access** and makes it harder to enforce device compliance, potentially leading to **credential theft** or **data exfiltration** from unmanaged environments.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-policy-unknown-unsupported-device", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-conditions#device-platforms" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com.\n2. Expand **Protection** > **Conditional Access** and select **Policies**.\n3. Select **New policy**.\n4. Under **Users**, include the desired scope. Microsoft recommends **All users** with appropriate exclusions.\n5. Under **Target resources**, include the applications or resources you want to protect. Microsoft recommends **All resources / All cloud apps**.\n6. Under **Conditions** > **Device platforms**, set **Configure** to **Yes**.\n7. Under **Include**, select **Any device**.\n8. Under **Exclude**, select **Android**, **iOS**, **Windows**, **macOS**, and **Linux**.\n9. Under **Grant**, select **Block access**.\n10. Set **Enable policy** to **On** and click **Create**.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Create a Conditional Access policy that includes **all device platforms** and excludes the known ones (Android, iOS, Windows, macOS, Linux), then set the grant control to **Block access**. This ensures that only recognized platforms can authenticate, following a **zero-trust** approach to device management.", + "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_block_unknown_device_platforms" + } + }, + "Categories": [ + "trust-boundaries", + "e3" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "The policy must include all platforms and exclude the five known platforms (Android, iOS, Windows, macOS, Linux) so that only unknown or unsupported platforms are blocked. The check requires the policy to be fully enabled; report-only policies are treated as non-compliant. Microsoft recommends applying this policy broadly to **All users** and **All resources / All cloud apps**, but that broader scope is guidance rather than a requirement enforced by this check." +} diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_block_unknown_device_platforms/entra_conditional_access_policy_block_unknown_device_platforms.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_block_unknown_device_platforms/entra_conditional_access_policy_block_unknown_device_platforms.py new file mode 100644 index 0000000000..b53abb56b0 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_block_unknown_device_platforms/entra_conditional_access_policy_block_unknown_device_platforms.py @@ -0,0 +1,86 @@ +"""Check for Conditional Access policy blocking unknown or unsupported device platforms.""" + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client +from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, +) + + +class entra_conditional_access_policy_block_unknown_device_platforms(Check): + """Ensure a Conditional Access policy blocks access from unknown or unsupported device platforms. + + This check verifies that at least one enabled Conditional Access policy + blocks access when the device platform is unknown or unsupported. The + recommended configuration includes all device platforms and excludes the + known platforms (Android, iOS, Windows, macOS, Linux), so only + unrecognised platforms are blocked. + + - PASS: An enabled policy blocks access from unknown or unsupported device platforms. + - FAIL: No policy blocks access from unknown or unsupported device platforms. + """ + + KNOWN_PLATFORMS = {"android", "ios", "windows", "macos", "linux"} + + def execute(self) -> list[CheckReportM365]: + """Execute the check logic. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + + report = CheckReportM365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + report.status = "FAIL" + report.status_extended = "No Conditional Access Policy blocks access from unknown or unsupported device platforms." + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + if not policy.conditions.platform_conditions: + continue + + if "all" not in policy.conditions.platform_conditions.include_platforms: + continue + + if not self.KNOWN_PLATFORMS.issubset( + set(policy.conditions.platform_conditions.exclude_platforms) + ): + continue + + if ( + ConditionalAccessGrantControl.BLOCK + not in policy.grant_controls.built_in_controls + ): + continue + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.display_name, + resource_id=policy.id, + ) + + if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: + report.status = "FAIL" + report.status_extended = ( + f"Conditional Access Policy {policy.display_name} reports " + "blocking unknown or unsupported device platforms but does not enforce it." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"Conditional Access Policy {policy.display_name} blocks " + "access from unknown or unsupported device platforms." + ) + break + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced/__init__.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.metadata.json new file mode 100644 index 0000000000..8e9b110a89 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "m365", + "CheckID": "entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced", + "CheckTitle": "Conditional Access policy enforces sign-in frequency for non-corporate devices", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "Microsoft Entra **Conditional Access** policy with **sign-in frequency** controls how often users must re-authenticate when accessing resources from non-corporate devices.\n\nThis ensures that sessions on unmanaged devices are time-limited, reducing the window of opportunity for unauthorized access through stale sessions.", + "Risk": "Without sign-in frequency enforcement on non-corporate devices, user sessions may persist indefinitely on unmanaged devices.\n\n- **Session hijacking** on shared or public devices becomes more likely\n- **Stolen session tokens** remain valid for extended periods\n- **Unauthorized access** from compromised personal devices", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-session-lifetime" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com.\n2. Expand **Protection** > **Conditional Access** and select **Policies**.\n3. Click **New policy**.\n4. Under **Users**, select **All users**.\n5. Under **Target resources**, select **All cloud apps**.\n6. Under **Conditions** > **Filter for devices**, select **Include filtered devices**.\n7. Set the rule to `device.isCompliant -eq False`.\n8. Under **Session**, enable **Sign-in frequency** and set it to **1 hour**.\n9. Set the policy to **On** and click **Create**.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Configure a Conditional Access policy to enforce **sign-in frequency** on non-corporate devices. Use device filters to target unmanaged endpoints and set a time-based re-authentication interval to limit session duration.\n\nThis aligns with **zero trust** principles by ensuring sessions on untrusted devices are regularly validated.", + "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced" + } + }, + "Categories": [ + "e3" + ], + "DependsOn": [], + "RelatedTo": [ + "entra_conditional_access_policy_app_enforced_restrictions" + ], + "Notes": "A qualifying policy must target all users, all applications, enforce time-based sign-in frequency, and include a device filter targeting non-corporate devices." +} diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.py new file mode 100644 index 0000000000..360901569a --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.py @@ -0,0 +1,109 @@ +import re + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client +from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicyState, + DeviceFilterMode, + SignInFrequencyInterval, +) + + +class entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced( + Check +): + """Check if at least one Conditional Access policy enforces sign-in frequency for non-corporate devices. + + This check verifies that the tenant has at least one enabled Conditional Access policy + that enforces time-based sign-in frequency targeting all users and all applications, + with a device filter scoping the policy to non-corporate (unmanaged) devices. + + - PASS: At least one enabled policy enforces sign-in frequency with a device filter + targeting non-corporate devices, for all users and all applications. + - FAIL: No enabled policy meets the sign-in frequency enforcement criteria for + non-corporate devices. + """ + + NON_CORPORATE_INCLUDE_PATTERNS = ( + r"device\.iscompliant\s*-ne\s*true", + r"device\.iscompliant\s*-eq\s*false", + r'device\.trusttype\s*-ne\s*"serverad"', + r"device\.trusttype\s*-ne\s*'serverad'", + ) + CORPORATE_EXCLUDE_PATTERNS = ( + r"device\.iscompliant\s*-eq\s*true", + r'device\.trusttype\s*-eq\s*"serverad"', + r"device\.trusttype\s*-eq\s*'serverad'", + ) + + def execute(self) -> list[CheckReportM365]: + """Execute the check for sign-in frequency enforcement in Conditional Access policies. + + Returns: + list[CheckReportM365]: A list containing the result of the check. + """ + findings = [] + report = CheckReportM365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + report.status = "FAIL" + report.status_extended = "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + if "All" not in policy.conditions.user_conditions.included_users: + continue + + if ( + "All" + not in policy.conditions.application_conditions.included_applications + ): + continue + + sign_in_freq = policy.session_controls.sign_in_frequency + if not ( + sign_in_freq.is_enabled + and sign_in_freq.interval == SignInFrequencyInterval.TIME_BASED + ): + continue + + device_conditions = policy.conditions.device_conditions + if ( + not device_conditions + or not device_conditions.device_filter_mode + or not device_conditions.device_filter_rule + ): + continue + + rule = device_conditions.device_filter_rule.lower() + if device_conditions.device_filter_mode == DeviceFilterMode.INCLUDE: + patterns = self.NON_CORPORATE_INCLUDE_PATTERNS + elif device_conditions.device_filter_mode == DeviceFilterMode.EXCLUDE: + patterns = self.CORPORATE_EXCLUDE_PATTERNS + else: + continue + + if not any(re.search(pattern, rule) for pattern in patterns): + continue + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.display_name, + resource_id=policy.id, + ) + if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: + report.status = "FAIL" + report.status_extended = f"Conditional Access Policy {policy.display_name} reports sign-in frequency for non-corporate devices but does not enforce it." + else: + report.status = "PASS" + report.status_extended = f"Conditional Access Policy {policy.display_name} enforces sign-in frequency for non-corporate devices." + break + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_directory_sync_account_excluded/__init__.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_directory_sync_account_excluded/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_directory_sync_account_excluded/entra_conditional_access_policy_directory_sync_account_excluded.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_directory_sync_account_excluded/entra_conditional_access_policy_directory_sync_account_excluded.metadata.json new file mode 100644 index 0000000000..b7266f1f56 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_directory_sync_account_excluded/entra_conditional_access_policy_directory_sync_account_excluded.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "m365", + "CheckID": "entra_conditional_access_policy_directory_sync_account_excluded", + "CheckTitle": "Conditional Access policy excludes Directory Synchronization Accounts to protect Entra Connect sync", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "Conditional Access policies scoped to **all users** and **all cloud applications** are evaluated to confirm the **Directory Synchronization Accounts** role is explicitly excluded. The Microsoft Entra Connect Sync Account does not support multifactor authentication, so it must be excluded from restrictive policies to maintain directory synchronization.", + "Risk": "If the Directory Synchronization Accounts role is not excluded from Conditional Access policies requiring MFA or blocking access, the Entra Connect Sync Account will be unable to authenticate. This breaks hybrid identity synchronization between on-premises Active Directory and Entra ID, potentially causing authentication failures and identity inconsistencies.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-all-users-mfa", + "https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/reference-connect-accounts-permissions" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com.\n2. Expand **Protection** > **Conditional Access** and select **Policies**.\n3. Open each policy that targets **All users** and **All cloud apps**.\n4. Under **Users** > **Exclude**, select **Directory roles** and add the **Directory Synchronization Accounts** role.\n5. Save the policy.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Exclude the Directory Synchronization Accounts role from all Conditional Access policies that target all users and all cloud applications. This prevents breaking Entra Connect directory synchronization while maintaining security controls for interactive users.", + "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_directory_sync_account_excluded" + } + }, + "Categories": [ + "identity-access", + "e3" + ], + "DependsOn": [], + "RelatedTo": [ + "entra_conditional_access_policy_require_mfa_for_management_api" + ], + "Notes": "The Directory Synchronization Accounts role template ID is d29b2b05-8046-44ba-8758-1e26182fcf32." +} diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_directory_sync_account_excluded/entra_conditional_access_policy_directory_sync_account_excluded.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_directory_sync_account_excluded/entra_conditional_access_policy_directory_sync_account_excluded.py new file mode 100644 index 0000000000..ee37e57121 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_directory_sync_account_excluded/entra_conditional_access_policy_directory_sync_account_excluded.py @@ -0,0 +1,91 @@ +"""Check if Conditional Access policies exclude the Directory Synchronization Account.""" + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client +from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicyState, +) + +# The Directory Synchronization Accounts built-in role template ID in Entra ID. +# This role is assigned to the Microsoft Entra Connect Sync service account and +# does not support multifactor authentication. +DIRECTORY_SYNC_ROLE_TEMPLATE_ID = "d29b2b05-8046-44ba-8758-1e26182fcf32" + + +class entra_conditional_access_policy_directory_sync_account_excluded(Check): + """Check that Conditional Access policies exclude the Directory Synchronization Account. + + The Microsoft Entra Connect Sync Account cannot support MFA. Conditional + Access policies scoped to all users and all cloud apps must explicitly + exclude the Directory Synchronization Accounts role to prevent breaking + directory synchronization. + + - PASS: The policy excludes the Directory Synchronization Accounts role. + - FAIL: The policy does not exclude the Directory Synchronization Accounts role. + """ + + def execute(self) -> list[CheckReportM365]: + """Execute the check for Directory Sync Account exclusion from Conditional Access policies. + + Iterates through all enabled Conditional Access policies that target + all users and all cloud applications, verifying each one excludes the + Directory Synchronization Accounts role. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + if not policy.conditions.user_conditions: + continue + + if "All" not in policy.conditions.user_conditions.included_users: + continue + + if not policy.conditions.application_conditions: + continue + + if ( + "All" + not in policy.conditions.application_conditions.included_applications + ): + continue + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.display_name, + resource_id=policy.id, + ) + + if ( + DIRECTORY_SYNC_ROLE_TEMPLATE_ID + in policy.conditions.user_conditions.excluded_roles + ): + report.status = "PASS" + report.status_extended = f"Conditional Access Policy {policy.display_name} excludes the Directory Synchronization Accounts role." + else: + report.status = "FAIL" + if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: + report.status_extended = f"Conditional Access Policy {policy.display_name} reports excluding the Directory Synchronization Accounts role but does not enforce it." + else: + report.status_extended = f"Conditional Access Policy {policy.display_name} does not exclude the Directory Synchronization Accounts role, which may break Entra Connect sync." + + findings.append(report) + + if not findings: + report = CheckReportM365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + report.status = "PASS" + report.status_extended = "No Conditional Access Policy targets all users and all cloud apps, so no Directory Synchronization Accounts exclusion is needed." + findings.append(report) + + return findings diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_mfa_enforced_for_guest_users/__init__.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_mfa_enforced_for_guest_users/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_mfa_enforced_for_guest_users/entra_conditional_access_policy_mfa_enforced_for_guest_users.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_mfa_enforced_for_guest_users/entra_conditional_access_policy_mfa_enforced_for_guest_users.metadata.json new file mode 100644 index 0000000000..f99c94b7a7 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_mfa_enforced_for_guest_users/entra_conditional_access_policy_mfa_enforced_for_guest_users.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "m365", + "CheckID": "entra_conditional_access_policy_mfa_enforced_for_guest_users", + "CheckTitle": "Conditional Access Policy enforces MFA for guest and external users", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "Microsoft Entra **Conditional Access** is verified to have at least one **enabled** policy that requires **multifactor authentication** for all **guest and external user types** across all cloud applications. This includes internal guests, B2B collaboration guests and members, B2B direct connect users, other external users, and service providers.", + "Risk": "Without MFA for guest users, compromised external accounts can access tenant resources using only a password. Attackers may exploit **B2B collaboration**, **direct connect**, or **service provider** accounts to exfiltrate data, escalate privileges, or move laterally across the organization.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-alt-require-mfa-guest-access" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center (https://entra.microsoft.com).\n2. Expand **Protection** > **Conditional Access** and select **Policies**.\n3. Click **New policy**.\n4. Under **Users**, select **Include** > **Select users and groups** > check **Guest or external users** > select all guest user types.\n5. Under **Target resources**, select **Include** > **All cloud apps**.\n6. Under **Grant**, select **Grant access** > check **Require multifactor authentication** > click **Select**.\n7. Set the policy to **Report-only** until validated, then enable it.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enforce **MFA** via **Conditional Access** for all **guest and external user types** across all cloud applications. Prefer **phishing-resistant** methods, monitor guest sign-ins, and regularly review external collaboration settings.", + "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_mfa_enforced_for_guest_users" + } + }, + "Categories": [ + "identity-access", + "trust-boundaries", + "e3" + ], + "DependsOn": [], + "RelatedTo": [ + "entra_policy_guest_users_access_restrictions", + "entra_policy_guest_invite_only_for_admin_roles", + "entra_dynamic_group_for_guests_created" + ], + "Notes": "Conditional Access policies require Microsoft Entra ID P1 or P2 licenses." +} diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_mfa_enforced_for_guest_users/entra_conditional_access_policy_mfa_enforced_for_guest_users.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_mfa_enforced_for_guest_users/entra_conditional_access_policy_mfa_enforced_for_guest_users.py new file mode 100644 index 0000000000..7d27741df9 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_mfa_enforced_for_guest_users/entra_conditional_access_policy_mfa_enforced_for_guest_users.py @@ -0,0 +1,143 @@ +"""Check if at least one Conditional Access policy requires MFA for guest users.""" + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client +from prowler.providers.m365.services.entra.entra_service import ( + ALL_GUEST_USER_TYPES, + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + ExternalTenantsMembershipKind, +) + + +class entra_conditional_access_policy_mfa_enforced_for_guest_users(Check): + """Check if at least one enabled Conditional Access policy requires MFA for guest users. + + This check verifies that the Microsoft Entra tenant has at least one + enabled Conditional Access policy that requires multifactor authentication + (MFA) for all guest and external user types across all cloud applications. + + - PASS: At least one enabled CA policy requires MFA for all guest user types. + - FAIL: No enabled CA policy enforces MFA for guest users. + """ + + def execute(self) -> list[CheckReportM365]: + """Execute the check logic. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + report = CheckReportM365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + report.status = "FAIL" + report.status_extended = ( + "No Conditional Access Policy requires MFA for guest users." + ) + + reporting_policy = None + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + # Policy must require MFA (built-in control or authentication strength) + # and must not only require password change. + has_mfa = ( + ConditionalAccessGrantControl.MFA + in policy.grant_controls.built_in_controls + ) + has_auth_strength = ( + policy.grant_controls.authentication_strength is not None + ) + only_password_change = policy.grant_controls.built_in_controls == [ + ConditionalAccessGrantControl.PASSWORD_CHANGE + ] + + if not (has_mfa or has_auth_strength) or only_password_change: + continue + + # Policy must target all cloud applications. + if not policy.conditions.application_conditions: + continue + + if ( + "All" + not in policy.conditions.application_conditions.included_applications + ): + continue + + # Policy must target guest users: either include all users, or + # specifically include all guest/external user types. + targets_all_users = ( + "All" in policy.conditions.user_conditions.included_users + ) + targets_guests_via_include = ( + "GuestsOrExternalUsers" + in policy.conditions.user_conditions.included_users + ) + excludes_all_guests = ( + "GuestsOrExternalUsers" + in policy.conditions.user_conditions.excluded_users + ) + + included_guests = ( + policy.conditions.user_conditions.included_guests_or_external_users + ) + targets_all_guest_types = included_guests is not None and ( + ALL_GUEST_USER_TYPES + <= set(included_guests.guest_or_external_user_types) + and included_guests.external_tenants_membership_kind + in (None, ExternalTenantsMembershipKind.ALL) + ) + + if not ( + targets_all_users + or targets_guests_via_include + or targets_all_guest_types + ): + continue + + # Policy must not exclude guest/external user types. + excluded_guests = ( + policy.conditions.user_conditions.excluded_guests_or_external_users + ) + if excludes_all_guests or ( + excluded_guests is not None + and excluded_guests.guest_or_external_user_types + ): + continue + + if policy.state == ConditionalAccessPolicyState.ENABLED: + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.display_name, + resource_id=policy.id, + ) + report.status = "PASS" + report.status_extended = f"Conditional Access Policy {policy.display_name} requires MFA for guest users." + break + + if ( + policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING + and reporting_policy is None + ): + reporting_policy = policy + + if report.status == "FAIL" and reporting_policy: + report = CheckReportM365( + metadata=self.metadata(), + resource=reporting_policy, + resource_name=reporting_policy.display_name, + resource_id=reporting_policy.id, + ) + report.status = "FAIL" + report.status_extended = f"Conditional Access Policy {reporting_policy.display_name} targets guest users with MFA but is only in report-only mode." + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/entra/entra_service.py b/prowler/providers/m365/services/entra/entra_service.py index b224aabdcc..2511e4056e 100644 --- a/prowler/providers/m365/services/entra/entra_service.py +++ b/prowler/providers/m365/services/entra/entra_service.py @@ -9,7 +9,7 @@ from msgraph.generated.models.o_data_errors.o_data_error import ODataError from msgraph.generated.security.microsoft_graph_security_run_hunting_query.run_hunting_query_post_request_body import ( RunHuntingQueryPostRequestBody, ) -from pydantic.v1 import BaseModel +from pydantic.v1 import BaseModel, validator from prowler.lib.logger import logger from prowler.providers.m365.lib.service.service import M365Service @@ -264,6 +264,20 @@ class Entra(M365Service): [], ) ], + included_guests_or_external_users=self._parse_guests_or_external_users( + getattr( + policy.conditions.users, + "include_guests_or_external_users", + None, + ) + ), + excluded_guests_or_external_users=self._parse_guests_or_external_users( + getattr( + policy.conditions.users, + "exclude_guests_or_external_users", + None, + ) + ), ), client_app_types=[ ClientAppType(client_app_type) @@ -331,6 +345,57 @@ class Entra(M365Service): authentication_flows=self._parse_authentication_flows( raw_auth_flows_map.get(policy.id) ), + device_conditions=DeviceConditions( + device_filter_mode=( + DeviceFilterMode( + getattr( + getattr( + getattr( + policy.conditions, + "devices", + None, + ), + "device_filter", + None, + ), + "mode", + None, + ) + ) + if getattr( + getattr(policy.conditions, "devices", None), + "device_filter", + None, + ) + and getattr( + getattr( + getattr(policy.conditions, "devices", None), + "device_filter", + None, + ), + "mode", + None, + ) + else None + ), + device_filter_rule=( + getattr( + getattr( + getattr(policy.conditions, "devices", None), + "device_filter", + None, + ), + "rule", + None, + ) + if getattr( + getattr(policy.conditions, "devices", None), + "device_filter", + None, + ) + else None + ), + ), ), grant_controls=GrantControls( built_in_controls=( @@ -546,6 +611,56 @@ class Entra(M365Service): return AuthenticationFlows(transfer_methods=transfer_methods) + @staticmethod + def _parse_guests_or_external_users( + sdk_obj, + ) -> "GuestsOrExternalUsers | None": + """Parse guest or external user conditions from the MS Graph SDK object. + + The SDK deserializes ``guestOrExternalUserTypes`` via + ``get_collection_of_enum_values``, returning a list of SDK enum members. + + Args: + sdk_obj: A ``ConditionalAccessGuestsOrExternalUsers`` SDK object, or ``None``. + + Returns: + A ``GuestsOrExternalUsers`` model instance, or ``None`` if the input is absent. + """ + if sdk_obj is None: + return None + + raw_types = getattr(sdk_obj, "guest_or_external_user_types", None) or [] + raw_membership_kind = getattr( + getattr(sdk_obj, "external_tenants", None), + "membership_kind", + None, + ) + membership_kind = None + if raw_membership_kind is not None: + raw_membership_kind = getattr( + raw_membership_kind, + "value", + raw_membership_kind, + ) + try: + membership_kind = ExternalTenantsMembershipKind(raw_membership_kind) + except ValueError: + logger.warning( + f"Unknown external tenants membership kind: {raw_membership_kind}" + ) + + guest_types: list[GuestOrExternalUserType] = [] + for raw_type in raw_types: + try: + guest_types.append(GuestOrExternalUserType(raw_type.value)) + except (ValueError, AttributeError): + logger.warning(f"Unknown guest or external user type: {raw_type}") + + return GuestsOrExternalUsers( + guest_or_external_user_types=guest_types, + external_tenants_membership_kind=membership_kind, + ) + @staticmethod def _parse_app_management_restrictions(restrictions): """Parse credential restrictions from the Graph API response into AppManagementRestrictions.""" @@ -957,13 +1072,58 @@ class ApplicationsConditions(BaseModel): included_user_actions: List[UserAction] +class GuestOrExternalUserType(Enum): + """Guest or external user types for Conditional Access policies. + + Reference: https://learn.microsoft.com/en-us/graph/api/resources/conditionalaccessguestsorexternalusers + """ + + NONE = "none" + INTERNAL_GUEST = "internalGuest" + B2B_COLLABORATION_GUEST = "b2bCollaborationGuest" + B2B_COLLABORATION_MEMBER = "b2bCollaborationMember" + B2B_DIRECT_CONNECT_USER = "b2bDirectConnectUser" + OTHER_EXTERNAL_USER = "otherExternalUser" + SERVICE_PROVIDER = "serviceProvider" + + +class ExternalTenantsMembershipKind(Enum): + """External tenant scope for guest or external user conditions.""" + + ALL = "all" + ENUMERATED = "enumerated" + UNKNOWN_FUTURE_VALUE = "unknownFutureValue" + + +# All guest/external user types that represent actual guest or external users. +ALL_GUEST_USER_TYPES = { + GuestOrExternalUserType.INTERNAL_GUEST, + GuestOrExternalUserType.B2B_COLLABORATION_GUEST, + GuestOrExternalUserType.B2B_COLLABORATION_MEMBER, + GuestOrExternalUserType.B2B_DIRECT_CONNECT_USER, + GuestOrExternalUserType.OTHER_EXTERNAL_USER, + GuestOrExternalUserType.SERVICE_PROVIDER, +} + + +class GuestsOrExternalUsers(BaseModel): + """Model representing guest or external user conditions in Conditional Access policies.""" + + guest_or_external_user_types: List[GuestOrExternalUserType] = [] + external_tenants_membership_kind: Optional[ExternalTenantsMembershipKind] = None + + class UsersConditions(BaseModel): + """Model representing user conditions for Conditional Access policies.""" + included_groups: List[str] excluded_groups: List[str] included_users: List[str] excluded_users: List[str] included_roles: List[str] excluded_roles: List[str] + included_guests_or_external_users: Optional[GuestsOrExternalUsers] = None + excluded_guests_or_external_users: Optional[GuestsOrExternalUsers] = None class RiskLevel(Enum): @@ -992,12 +1152,40 @@ class InsiderRiskLevel(Enum): ELEVATED = "elevated" +class DeviceFilterMode(Enum): + """Mode for device filter in Conditional Access policies.""" + + INCLUDE = "include" + EXCLUDE = "exclude" + + +class DeviceConditions(BaseModel): + """Model representing device conditions for Conditional Access policies.""" + + device_filter_mode: Optional[DeviceFilterMode] = None + device_filter_rule: Optional[str] = None + + class PlatformConditions(BaseModel): """Model representing platform conditions for Conditional Access policies.""" include_platforms: List[str] = [] exclude_platforms: List[str] = [] + @validator("include_platforms", "exclude_platforms", pre=True) + @classmethod + def normalize_platforms(cls, values): + if not values: + return [] + + normalized = [] + for platform in values: + value = getattr(platform, "value", platform) + if isinstance(value, str) and value: + normalized.append(value.lower()) + + return normalized + class TransferMethod(Enum): """Transfer methods for authentication flows in Conditional Access policies.""" @@ -1013,6 +1201,8 @@ class AuthenticationFlows(BaseModel): class Conditions(BaseModel): + """Model representing conditions for Conditional Access policies.""" + application_conditions: Optional[ApplicationsConditions] user_conditions: Optional[UsersConditions] client_app_types: Optional[List[ClientAppType]] @@ -1021,6 +1211,7 @@ class Conditions(BaseModel): insider_risk_levels: Optional[InsiderRiskLevel] = None platform_conditions: Optional[PlatformConditions] = None authentication_flows: Optional[AuthenticationFlows] = None + device_conditions: Optional[DeviceConditions] = None class PersistentBrowser(BaseModel): diff --git a/prowler/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/__init__.py b/prowler/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled.metadata.json new file mode 100644 index 0000000000..5d46fcde6f --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "m365", + "CheckID": "exchange_organization_delicensing_resiliency_enabled", + "CheckTitle": "Delicensing Resiliency protects Exchange Online mailboxes from immediate access loss during license changes", + "CheckType": [], + "ServiceName": "exchange", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "collaboration", + "Description": "**Microsoft 365 Exchange Online** Delicensing Resiliency provides a grace period when licenses expire or are reassigned, preventing immediate mailbox access loss.\n\nThis evaluates whether the organization has **Delayed Delicensing** enabled to protect mailbox data during licensing transitions. Note: This feature is only available to tenants with 5000 or more paid licenses.", + "Risk": "Without **Delicensing Resiliency**, removing or reassigning an Exchange Online license causes **immediate mailbox inaccessibility**. This can lead to data loss, business disruption, and inability to recover mailbox contents during organizational changes such as role transitions or license optimizations.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/exchange/recipients-in-exchange-online/manage-user-mailboxes/delicensing-resiliency" + ], + "Remediation": { + "Code": { + "CLI": "Set-OrganizationConfig -DelayedDelicensingEnabled $true", + "NativeIaC": "", + "Other": "1. Connect to Exchange Online PowerShell\n2. Run: Set-OrganizationConfig -DelayedDelicensingEnabled $true\n3. Verify with: Get-OrganizationConfig | Format-List DelayedDelicensingEnabled", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable **Delicensing Resiliency** to ensure mailbox data is preserved during license transitions. This provides a grace period allowing administrators to reassign licenses or export data before access is permanently revoked, maintaining **business continuity** and **data protection**.", + "Url": "https://hub.prowler.com/check/exchange_organization_delicensing_resiliency_enabled" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check includes an automated fixer that runs `Set-OrganizationConfig -DelayedDelicensingEnabled $true` via Exchange Online PowerShell. Delicensing Resiliency is only available to tenants with 5000 or more paid licenses; trial licenses also count toward this threshold but cannot be discerned from the SDK, so tenants at or above the threshold (or with an unknown license count) are reported as a preventive FAIL and eligibility can be confirmed by running the fixer, which succeeds on qualifying tenants and fails otherwise." +} diff --git a/prowler/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled.py b/prowler/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled.py new file mode 100644 index 0000000000..66fe4d4272 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled.py @@ -0,0 +1,79 @@ +"""Check for Exchange Online Delicensing Resiliency configuration.""" + +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.exchange.exchange_client import exchange_client + +DELICENSING_LICENSE_THRESHOLD = 5000 + + +class exchange_organization_delicensing_resiliency_enabled(Check): + """ + Check if Delicensing Resiliency is enabled for Exchange Online. + + Delicensing Resiliency provides a grace period when licenses expire or are + reassigned, preventing immediate mailbox access loss and allowing + organizations time to manage licensing transitions. + + This feature is only available to tenants with 5000 or more paid licenses. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> List[CheckReportM365]: + """ + Execute the check for Delicensing Resiliency in Exchange Online. + + Iterates over the Exchange Online organization configuration and + evaluates whether Delicensing Resiliency is enabled, taking into + account the tenant's paid license count. + + Returns: + List[CheckReportM365]: A list of reports containing the result of the check. + """ + findings = [] + organization_config = exchange_client.organization_config + if organization_config: + report = CheckReportM365( + metadata=self.metadata(), + resource=organization_config, + resource_name=organization_config.name, + resource_id=organization_config.guid, + ) + + if organization_config.delayed_delicensing_enabled: + report.status = "PASS" + report.status_extended = ( + "Delicensing Resiliency is enabled for Exchange Online, " + "providing a grace period when licenses are removed." + ) + elif ( + organization_config.total_paid_licenses is not None + and organization_config.total_paid_licenses + < DELICENSING_LICENSE_THRESHOLD + ): + report.status = "PASS" + report.status_extended = ( + f"Delicensing Resiliency is not applicable for this tenant. " + f"The tenant has {organization_config.total_paid_licenses} " + f"total licenses, which is below the " + f"{DELICENSING_LICENSE_THRESHOLD} paid license threshold " + f"required by Microsoft for this feature." + ) + else: + report.status = "FAIL" + report.status_extended = ( + "Delicensing Resiliency is not enabled for Exchange Online. " + "This feature requires the tenant to have 5000 or more paid " + "licenses, and trial licenses also count toward this " + "threshold but cannot be discerned from the SDK, so this " + "is reported as a preventive FAIL. Running the fixer will " + "enable the feature when the tenant qualifies and will " + "fail otherwise, confirming eligibility." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled_fixer.py b/prowler/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled_fixer.py new file mode 100644 index 0000000000..c921686655 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled_fixer.py @@ -0,0 +1,57 @@ +"""Fixer for Exchange Online Delicensing Resiliency.""" + +from prowler.lib.logger import logger +from prowler.providers.common.provider import Provider +from prowler.providers.m365.lib.powershell.m365_powershell import M365PowerShell + + +def fixer(resource_id: str = "") -> bool: + """Enable Delicensing Resiliency in Exchange Online. + + Args: + resource_id (str): Unused for this organization-level fixer. + + Returns: + bool: True when the fixer command succeeds, False otherwise. + """ + session = None + + try: + provider = Provider.get_global_provider() + if not provider: + logger.error("Unable to load the global M365 provider for Exchange Online.") + return False + + credentials = getattr(provider, "credentials", None) + identity = getattr(provider, "identity", None) + if not credentials or not identity: + logger.error( + "Unable to load the M365 credentials required for Exchange Online." + ) + return False + + session = M365PowerShell(credentials, identity) + if not session.connect_exchange_online(): + logger.error("Unable to connect to Exchange Online PowerShell.") + return False + + result = session.execute( + "Set-OrganizationConfig -DelayedDelicensingEnabled $true", + timeout=30, + ) + if result: + logger.error( + "PowerShell execution failed while running " + '"Set-OrganizationConfig -DelayedDelicensingEnabled $true": ' + f"{result}" + ) + return False + return True + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return False + finally: + if session: + session.close() diff --git a/prowler/providers/m365/services/exchange/exchange_service.py b/prowler/providers/m365/services/exchange/exchange_service.py index 978a9b2a10..9b3ee47da9 100644 --- a/prowler/providers/m365/services/exchange/exchange_service.py +++ b/prowler/providers/m365/services/exchange/exchange_service.py @@ -1,3 +1,4 @@ +import asyncio from enum import Enum from typing import Optional @@ -47,6 +48,50 @@ class Exchange(M365Service): self.shared_mailboxes = self._get_shared_mailboxes() self.powershell.close() + # Fetch license count via Graph API + created_loop = False + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + created_loop = True + + if loop.is_closed(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + created_loop = True + + if not loop.is_running(): + total_paid_licenses = loop.run_until_complete( + self._get_total_paid_licenses() + ) + + if created_loop: + asyncio.set_event_loop(None) + loop.close() + + if self.organization_config is not None: + self.organization_config.total_paid_licenses = total_paid_licenses + + async def _get_total_paid_licenses(self) -> Optional[int]: + """Fetch total paid license count from Microsoft Graph subscribed SKUs.""" + logger.info("Microsoft365 - Getting total paid license count...") + try: + subscribed_skus = await self.client.subscribed_skus.get() + total = 0 + for sku in getattr(subscribed_skus, "value", []) or []: + prepaid_units = getattr(sku, "prepaid_units", None) + if prepaid_units: + enabled = getattr(prepaid_units, "enabled", 0) or 0 + total += enabled + return total + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return None + def _get_organization_config(self): logger.info("Microsoft365 - Getting Exchange Organization configuration...") organization_config = None @@ -74,6 +119,9 @@ class Exchange(M365Service): mailtips_large_audience_threshold=organization_configuration.get( "MailTipsLargeAudienceThreshold", 25 ), + delayed_delicensing_enabled=organization_configuration.get( + "DelayedDelicensingEnabled", False + ), ) except Exception as error: logger.error( @@ -309,6 +357,22 @@ class Exchange(M365Service): class Organization(BaseModel): + """ + Model for Exchange Online organization configuration. + + Attributes: + name: Organization display name. + guid: Organization unique identifier. + audit_disabled: Whether auditing is disabled for the organization. + oauth_enabled: Whether OAuth 2.0 (Modern Authentication) is enabled. + mailtips_enabled: Whether MailTips are enabled. + mailtips_external_recipient_enabled: Whether MailTips for external recipients are enabled. + mailtips_group_metrics_enabled: Whether MailTips group metrics are enabled. + mailtips_large_audience_threshold: Threshold for large audience MailTips. + delayed_delicensing_enabled: Whether Delicensing Resiliency is enabled. + total_paid_licenses: Total paid licenses in the tenant, or None if unknown. + """ + name: str guid: str audit_disabled: bool @@ -317,6 +381,8 @@ class Organization(BaseModel): mailtips_external_recipient_enabled: bool mailtips_group_metrics_enabled: bool mailtips_large_audience_threshold: int + delayed_delicensing_enabled: bool = False + total_paid_licenses: Optional[int] = None class MailboxAuditConfig(BaseModel): diff --git a/prowler/providers/m365/services/intune/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default/__init__.py b/prowler/providers/m365/services/intune/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/intune/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.metadata.json b/prowler/providers/m365/services/intune/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.metadata.json new file mode 100644 index 0000000000..0d7fac5ba5 --- /dev/null +++ b/prowler/providers/m365/services/intune/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "m365", + "CheckID": "intune_device_compliance_policy_unassigned_devices_not_compliant_by_default", + "CheckTitle": "Built-in Device Compliance Policy marks devices without an assigned compliance policy as Not compliant by default", + "CheckType": [], + "ServiceName": "intune", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "security", + "Description": "Intune has a built-in Device Compliance Policy that governs how devices without an explicit compliance policy are treated. When the default behavior marks those devices as Compliant, unmanaged devices can be treated as compliant and gain access to corporate resources. This check verifies the default is set to Not compliant (secureByDefault = true).", + "Risk": "If the built-in policy marks devices without a compliance policy as Compliant, those devices can bypass Conditional Access policies requiring device compliance, granting unauthorized access to corporate resources from unmanaged or non-compliant endpoints.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/graph/api/resources/intune-deviceconfig-devicemanagementsettings?view=graph-rest-1.0" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Microsoft Intune admin center (intune.microsoft.com)\n2. Go to Devices > Compliance\n3. Select Compliance policy settings\n4. Set 'Mark devices with no compliance policy assigned as' to 'Not compliant'\n5. Save the settings", + "Terraform": "" + }, + "Recommendation": { + "Text": "Set the built-in Device Compliance Policy default so devices with no compliance policy assigned are marked as Not compliant.", + "Url": "https://hub.prowler.com/check/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "The check evaluates the secureByDefault property from the deviceManagement/settings Microsoft Graph endpoint." +} diff --git a/prowler/providers/m365/services/intune/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.py b/prowler/providers/m365/services/intune/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.py new file mode 100644 index 0000000000..a7f377baea --- /dev/null +++ b/prowler/providers/m365/services/intune/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.py @@ -0,0 +1,52 @@ +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.intune.intune_client import intune_client + + +class intune_device_compliance_policy_unassigned_devices_not_compliant_by_default( + Check +): + """Ensure the built-in Device Compliance Policy marks unassigned devices as Not compliant by default.""" + + def execute(self) -> list[CheckReportM365]: + findings = [] + + report = CheckReportM365( + metadata=self.metadata(), + resource=intune_client.settings or {}, + resource_name="Intune Device Compliance Settings", + resource_id="deviceManagement/settings", + ) + + verification_error = getattr(intune_client, "verification_error", None) + settings = getattr(intune_client, "settings", None) + secure_by_default = getattr(settings, "secure_by_default", None) + + if verification_error: + report.status = "MANUAL" + report.status_extended = ( + "Intune built-in Device Compliance Policy could not be verified. " + f"{verification_error}" + ) + elif settings is None or secure_by_default is None: + report.status = "MANUAL" + report.status_extended = ( + "Intune built-in Device Compliance Policy could not be verified " + "because Microsoft Graph did not return the secure-by-default " + "compliance setting." + ) + elif secure_by_default is True: + report.status = "PASS" + report.status_extended = ( + "Intune built-in Device Compliance Policy marks devices " + "with no compliance policy assigned as Not compliant." + ) + else: + report.status = "FAIL" + report.status_extended = ( + "Intune built-in Device Compliance Policy marks devices " + "with no compliance policy assigned as Compliant. " + "Change the default to Not compliant in Intune settings." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/intune/intune_service.py b/prowler/providers/m365/services/intune/intune_service.py index 4b62f64e99..2f6c0febae 100644 --- a/prowler/providers/m365/services/intune/intune_service.py +++ b/prowler/providers/m365/services/intune/intune_service.py @@ -125,6 +125,15 @@ class Intune(M365Service): request_configuration=request_configuration ) settings = getattr(device_management, "settings", None) + secure_by_default = getattr(settings, "secure_by_default", None) + + # Some tenants/API responses omit nested settings when $select is used. + # Retry without query parameters before concluding the value is unavailable. + if settings is None or secure_by_default is None: + device_management = await self.client.device_management.get() + settings = getattr(device_management, "settings", None) + secure_by_default = getattr(settings, "secure_by_default", None) + if settings is None: return ( IntuneSettings(secure_by_default=None), @@ -132,9 +141,7 @@ class Intune(M365Service): ) return ( - IntuneSettings( - secure_by_default=getattr(settings, "secure_by_default", None) - ), + IntuneSettings(secure_by_default=secure_by_default), None, ) except Exception as error: diff --git a/prowler/providers/vercel/services/authentication/authentication_no_stale_tokens/authentication_no_stale_tokens.metadata.json b/prowler/providers/vercel/services/authentication/authentication_no_stale_tokens/authentication_no_stale_tokens.metadata.json index d94e12f0b9..f4863ada5f 100644 --- a/prowler/providers/vercel/services/authentication/authentication_no_stale_tokens/authentication_no_stale_tokens.metadata.json +++ b/prowler/providers/vercel/services/authentication/authentication_no_stale_tokens/authentication_no_stale_tokens.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Regularly audit API tokens and revoke any that have not been used within 90 days. Implement a token lifecycle management process that includes periodic reviews, automatic expiration dates, and documentation of each token's purpose and owner.", - "Url": "https://hub.prowler.com/checks/vercel/authentication_no_stale_tokens" + "Url": "https://hub.prowler.com/check/authentication_no_stale_tokens" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/authentication/authentication_token_not_expired/authentication_token_not_expired.metadata.json b/prowler/providers/vercel/services/authentication/authentication_token_not_expired/authentication_token_not_expired.metadata.json index dca48e73bf..47e5087cf5 100644 --- a/prowler/providers/vercel/services/authentication/authentication_token_not_expired/authentication_token_not_expired.metadata.json +++ b/prowler/providers/vercel/services/authentication/authentication_token_not_expired/authentication_token_not_expired.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Remove expired tokens and create new ones with appropriate expiration dates. Implement a token rotation schedule to ensure tokens are refreshed before they expire. Update all integrations and automation that depend on the replaced tokens.", - "Url": "https://hub.prowler.com/checks/vercel/authentication_token_not_expired" + "Url": "https://hub.prowler.com/check/authentication_token_not_expired" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/deployment/deployment_production_uses_stable_target/deployment_production_uses_stable_target.metadata.json b/prowler/providers/vercel/services/deployment/deployment_production_uses_stable_target/deployment_production_uses_stable_target.metadata.json index 8a7cc70d40..25416e6882 100644 --- a/prowler/providers/vercel/services/deployment/deployment_production_uses_stable_target/deployment_production_uses_stable_target.metadata.json +++ b/prowler/providers/vercel/services/deployment/deployment_production_uses_stable_target/deployment_production_uses_stable_target.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Configure the production branch to main or master and ensure all production deployments go through the standard merge workflow. Use branch protection rules in your Git provider to prevent direct pushes to the production branch.", - "Url": "https://hub.prowler.com/checks/vercel/deployment_production_uses_stable_target" + "Url": "https://hub.prowler.com/check/deployment_production_uses_stable_target" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/domain/domain_dns_properly_configured/domain_dns_properly_configured.metadata.json b/prowler/providers/vercel/services/domain/domain_dns_properly_configured/domain_dns_properly_configured.metadata.json index f9104ee960..e2450da8f1 100644 --- a/prowler/providers/vercel/services/domain/domain_dns_properly_configured/domain_dns_properly_configured.metadata.json +++ b/prowler/providers/vercel/services/domain/domain_dns_properly_configured/domain_dns_properly_configured.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Update DNS records at your domain registrar to correctly point to Vercel. Use a CNAME record for subdomains or an A record for apex domains. Verify the configuration in the Vercel dashboard after making changes.", - "Url": "https://hub.prowler.com/checks/vercel/domain_dns_properly_configured" + "Url": "https://hub.prowler.com/check/domain_dns_properly_configured" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/domain/domain_ssl_certificate_valid/domain_ssl_certificate_valid.metadata.json b/prowler/providers/vercel/services/domain/domain_ssl_certificate_valid/domain_ssl_certificate_valid.metadata.json index f15892df61..ac683cd71f 100644 --- a/prowler/providers/vercel/services/domain/domain_ssl_certificate_valid/domain_ssl_certificate_valid.metadata.json +++ b/prowler/providers/vercel/services/domain/domain_ssl_certificate_valid/domain_ssl_certificate_valid.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Ensure domain DNS records are properly configured to point to Vercel. Once DNS is validated, Vercel automatically provisions and renews SSL/TLS certificates. Check the domain configuration in the Vercel dashboard if the certificate is not being issued.", - "Url": "https://hub.prowler.com/checks/vercel/domain_ssl_certificate_valid" + "Url": "https://hub.prowler.com/check/domain_ssl_certificate_valid" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/domain/domain_verified/domain_verified.metadata.json b/prowler/providers/vercel/services/domain/domain_verified/domain_verified.metadata.json index f520fb00d8..1e79a433ba 100644 --- a/prowler/providers/vercel/services/domain/domain_verified/domain_verified.metadata.json +++ b/prowler/providers/vercel/services/domain/domain_verified/domain_verified.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Complete domain verification by configuring the required DNS records at your domain registrar. Remove any domains that are no longer needed to reduce the attack surface. Regularly audit domain configurations to ensure all domains remain verified.", - "Url": "https://hub.prowler.com/checks/vercel/domain_verified" + "Url": "https://hub.prowler.com/check/domain_verified" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/project/project_auto_expose_system_env_disabled/project_auto_expose_system_env_disabled.metadata.json b/prowler/providers/vercel/services/project/project_auto_expose_system_env_disabled/project_auto_expose_system_env_disabled.metadata.json index bf7adc2832..21c3f118e1 100644 --- a/prowler/providers/vercel/services/project/project_auto_expose_system_env_disabled/project_auto_expose_system_env_disabled.metadata.json +++ b/prowler/providers/vercel/services/project/project_auto_expose_system_env_disabled/project_auto_expose_system_env_disabled.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Disable automatic exposure of system environment variables and explicitly define only the variables required by your application. This follows the principle of least privilege and reduces the risk of leaking internal infrastructure details through client-side code.", - "Url": "https://hub.prowler.com/checks/vercel/project_auto_expose_system_env_disabled" + "Url": "https://hub.prowler.com/check/project_auto_expose_system_env_disabled" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/project/project_deployment_protection_enabled/project_deployment_protection_enabled.metadata.json b/prowler/providers/vercel/services/project/project_deployment_protection_enabled/project_deployment_protection_enabled.metadata.json index c704b42784..521610c617 100644 --- a/prowler/providers/vercel/services/project/project_deployment_protection_enabled/project_deployment_protection_enabled.metadata.json +++ b/prowler/providers/vercel/services/project/project_deployment_protection_enabled/project_deployment_protection_enabled.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Enable deployment protection on preview deployments to require authentication before visitors can access preview URLs. Use 'Standard Protection' for Vercel Authentication or configure trusted IP ranges for more granular control.", - "Url": "https://hub.prowler.com/checks/vercel/project_deployment_protection_enabled" + "Url": "https://hub.prowler.com/check/project_deployment_protection_enabled" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/project/project_directory_listing_disabled/project_directory_listing_disabled.metadata.json b/prowler/providers/vercel/services/project/project_directory_listing_disabled/project_directory_listing_disabled.metadata.json index b9de49aa0e..b477a5984f 100644 --- a/prowler/providers/vercel/services/project/project_directory_listing_disabled/project_directory_listing_disabled.metadata.json +++ b/prowler/providers/vercel/services/project/project_directory_listing_disabled/project_directory_listing_disabled.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Disable directory listing to prevent visitors from browsing the file structure of your deployments. Ensure that all directories either contain an index file or return a 404 response when accessed directly.", - "Url": "https://hub.prowler.com/checks/vercel/project_directory_listing_disabled" + "Url": "https://hub.prowler.com/check/project_directory_listing_disabled" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/project/project_environment_no_overly_broad_target/project_environment_no_overly_broad_target.metadata.json b/prowler/providers/vercel/services/project/project_environment_no_overly_broad_target/project_environment_no_overly_broad_target.metadata.json index 2467c1b104..c9a418a503 100644 --- a/prowler/providers/vercel/services/project/project_environment_no_overly_broad_target/project_environment_no_overly_broad_target.metadata.json +++ b/prowler/providers/vercel/services/project/project_environment_no_overly_broad_target/project_environment_no_overly_broad_target.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Follow the **principle of least privilege** for environment variable targeting.\n- Assign each variable to only the environments where it is actually needed\n- Use different credentials for production, preview, and development environments\n- Non-sensitive configuration (e.g. feature flags, public URLs) may be acceptable in multiple environments but should still be reviewed\n- Regularly audit environment variable targets to prevent scope creep", - "Url": "https://hub.prowler.com/checks/vercel/project_environment_no_overly_broad_target" + "Url": "https://hub.prowler.com/check/project_environment_no_overly_broad_target" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/project/project_environment_no_secrets_in_plain_type/project_environment_no_secrets_in_plain_type.metadata.json b/prowler/providers/vercel/services/project/project_environment_no_secrets_in_plain_type/project_environment_no_secrets_in_plain_type.metadata.json index f8d5621914..90fea53eea 100644 --- a/prowler/providers/vercel/services/project/project_environment_no_secrets_in_plain_type/project_environment_no_secrets_in_plain_type.metadata.json +++ b/prowler/providers/vercel/services/project/project_environment_no_secrets_in_plain_type/project_environment_no_secrets_in_plain_type.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Use the **Sensitive** type for all environment variables that contain secrets, keys, tokens, or passwords.\n- Sensitive variables are never exposed in the dashboard or API responses after creation\n- Rotate all credentials that were previously stored as plain text\n- Implement naming conventions that make it easy to identify secret variables", - "Url": "https://hub.prowler.com/checks/vercel/project_environment_no_secrets_in_plain_type" + "Url": "https://hub.prowler.com/check/project_environment_no_secrets_in_plain_type" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/project/project_environment_production_vars_not_in_preview/project_environment_production_vars_not_in_preview.metadata.json b/prowler/providers/vercel/services/project/project_environment_production_vars_not_in_preview/project_environment_production_vars_not_in_preview.metadata.json index 55f468fa8a..6fc3e3af79 100644 --- a/prowler/providers/vercel/services/project/project_environment_production_vars_not_in_preview/project_environment_production_vars_not_in_preview.metadata.json +++ b/prowler/providers/vercel/services/project/project_environment_production_vars_not_in_preview/project_environment_production_vars_not_in_preview.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Maintain strict **environment separation** between production and preview deployments.\n- Use dedicated, limited-scope credentials for preview environments\n- Never share production database credentials, API keys, or signing keys with preview builds\n- Enable Vercel's deployment protection features to further restrict access to preview deployments\n- Regularly audit which environment variables target multiple environments", - "Url": "https://hub.prowler.com/checks/vercel/project_environment_production_vars_not_in_preview" + "Url": "https://hub.prowler.com/check/project_environment_production_vars_not_in_preview" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/project/project_git_fork_protection_enabled/project_git_fork_protection_enabled.metadata.json b/prowler/providers/vercel/services/project/project_git_fork_protection_enabled/project_git_fork_protection_enabled.metadata.json index 744a227d61..8e3db04fcd 100644 --- a/prowler/providers/vercel/services/project/project_git_fork_protection_enabled/project_git_fork_protection_enabled.metadata.json +++ b/prowler/providers/vercel/services/project/project_git_fork_protection_enabled/project_git_fork_protection_enabled.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Enable Git fork protection to require explicit authorization before pull requests from forked repositories can trigger deployments. This prevents untrusted contributors from accessing environment variables and secrets through the build process. For open-source projects, review fork PRs manually before allowing builds.", - "Url": "https://hub.prowler.com/checks/vercel/project_git_fork_protection_enabled" + "Url": "https://hub.prowler.com/check/project_git_fork_protection_enabled" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/project/project_password_protection_enabled/project_password_protection_enabled.metadata.json b/prowler/providers/vercel/services/project/project_password_protection_enabled/project_password_protection_enabled.metadata.json index 58e3e46e41..2db77410d9 100644 --- a/prowler/providers/vercel/services/project/project_password_protection_enabled/project_password_protection_enabled.metadata.json +++ b/prowler/providers/vercel/services/project/project_password_protection_enabled/project_password_protection_enabled.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Enable password protection to add a shared-password gate to your deployments. This is especially recommended for preview deployments shared with external clients or stakeholders who do not have Vercel accounts. Combine with Vercel Authentication for defense-in-depth.", - "Url": "https://hub.prowler.com/checks/vercel/project_password_protection_enabled" + "Url": "https://hub.prowler.com/check/project_password_protection_enabled" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/project/project_production_deployment_protection_enabled/project_production_deployment_protection_enabled.metadata.json b/prowler/providers/vercel/services/project/project_production_deployment_protection_enabled/project_production_deployment_protection_enabled.metadata.json index 213bc51d07..3760eefb57 100644 --- a/prowler/providers/vercel/services/project/project_production_deployment_protection_enabled/project_production_deployment_protection_enabled.metadata.json +++ b/prowler/providers/vercel/services/project/project_production_deployment_protection_enabled/project_production_deployment_protection_enabled.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Enable deployment protection on production deployments for applications that should not be publicly accessible. This is critical for internal tools, admin dashboards, and pre-launch applications where unauthorized access could lead to data exposure or system compromise.", - "Url": "https://hub.prowler.com/checks/vercel/project_production_deployment_protection_enabled" + "Url": "https://hub.prowler.com/check/project_production_deployment_protection_enabled" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/project/project_skew_protection_enabled/project_skew_protection_enabled.metadata.json b/prowler/providers/vercel/services/project/project_skew_protection_enabled/project_skew_protection_enabled.metadata.json index b73aaa6991..a0a4f70cee 100644 --- a/prowler/providers/vercel/services/project/project_skew_protection_enabled/project_skew_protection_enabled.metadata.json +++ b/prowler/providers/vercel/services/project/project_skew_protection_enabled/project_skew_protection_enabled.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Enable skew protection to ensure that all client requests during a deployment rollout are routed to the same deployment version that served the initial page. This prevents version mismatch errors and ensures a consistent user experience during deployments.", - "Url": "https://hub.prowler.com/checks/vercel/project_skew_protection_enabled" + "Url": "https://hub.prowler.com/check/project_skew_protection_enabled" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/security/security_custom_rules_configured/security_custom_rules_configured.metadata.json b/prowler/providers/vercel/services/security/security_custom_rules_configured/security_custom_rules_configured.metadata.json index c3f986b173..a4b53baf4f 100644 --- a/prowler/providers/vercel/services/security/security_custom_rules_configured/security_custom_rules_configured.metadata.json +++ b/prowler/providers/vercel/services/security/security_custom_rules_configured/security_custom_rules_configured.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Configure custom firewall rules to protect application-specific endpoints and enforce security policies. Focus on protecting admin panels, API routes, authentication endpoints, and any paths that handle sensitive data.", - "Url": "https://hub.prowler.com/checks/vercel/security_custom_rules_configured" + "Url": "https://hub.prowler.com/check/security_custom_rules_configured" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/security/security_ip_blocking_rules_configured/security_ip_blocking_rules_configured.metadata.json b/prowler/providers/vercel/services/security/security_ip_blocking_rules_configured/security_ip_blocking_rules_configured.metadata.json index cc7712d4ec..a02cd35324 100644 --- a/prowler/providers/vercel/services/security/security_ip_blocking_rules_configured/security_ip_blocking_rules_configured.metadata.json +++ b/prowler/providers/vercel/services/security/security_ip_blocking_rules_configured/security_ip_blocking_rules_configured.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Configure IP blocking rules to deny traffic from known malicious sources. Maintain a blocklist of IPs identified through security monitoring, threat intelligence feeds, or incident investigation. Regularly review and update the blocklist.", - "Url": "https://hub.prowler.com/checks/vercel/security_ip_blocking_rules_configured" + "Url": "https://hub.prowler.com/check/security_ip_blocking_rules_configured" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/security/security_managed_rulesets_enabled/security_managed_rulesets_enabled.metadata.json b/prowler/providers/vercel/services/security/security_managed_rulesets_enabled/security_managed_rulesets_enabled.metadata.json index 95d7db6d20..ee66a3be21 100644 --- a/prowler/providers/vercel/services/security/security_managed_rulesets_enabled/security_managed_rulesets_enabled.metadata.json +++ b/prowler/providers/vercel/services/security/security_managed_rulesets_enabled/security_managed_rulesets_enabled.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Enable managed WAF rulesets to benefit from Vercel-curated protection against common attack patterns. If you are on a plan that does not support managed rulesets, consider upgrading to the Enterprise plan for enhanced security features.", - "Url": "https://hub.prowler.com/checks/vercel/security_managed_rulesets_enabled" + "Url": "https://hub.prowler.com/check/security_managed_rulesets_enabled" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/security/security_rate_limiting_configured/security_rate_limiting_configured.metadata.json b/prowler/providers/vercel/services/security/security_rate_limiting_configured/security_rate_limiting_configured.metadata.json index 28a9c44d5f..8a804233a5 100644 --- a/prowler/providers/vercel/services/security/security_rate_limiting_configured/security_rate_limiting_configured.metadata.json +++ b/prowler/providers/vercel/services/security/security_rate_limiting_configured/security_rate_limiting_configured.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Configure rate limiting rules to protect critical endpoints such as authentication, API routes, and form submissions. Start with conservative thresholds and adjust based on traffic patterns to avoid blocking legitimate users.", - "Url": "https://hub.prowler.com/checks/vercel/security_rate_limiting_configured" + "Url": "https://hub.prowler.com/check/security_rate_limiting_configured" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/security/security_waf_enabled/security_waf_enabled.metadata.json b/prowler/providers/vercel/services/security/security_waf_enabled/security_waf_enabled.metadata.json index c758c82f18..7598e7cccd 100644 --- a/prowler/providers/vercel/services/security/security_waf_enabled/security_waf_enabled.metadata.json +++ b/prowler/providers/vercel/services/security/security_waf_enabled/security_waf_enabled.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Enable the Vercel Web Application Firewall to protect your application against common web attacks. Start with managed rulesets for baseline protection and add custom rules as needed based on your application's threat model.", - "Url": "https://hub.prowler.com/checks/vercel/security_waf_enabled" + "Url": "https://hub.prowler.com/check/security_waf_enabled" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/team/team_directory_sync_enabled/team_directory_sync_enabled.metadata.json b/prowler/providers/vercel/services/team/team_directory_sync_enabled/team_directory_sync_enabled.metadata.json index 37019b79da..fda5c7b94d 100644 --- a/prowler/providers/vercel/services/team/team_directory_sync_enabled/team_directory_sync_enabled.metadata.json +++ b/prowler/providers/vercel/services/team/team_directory_sync_enabled/team_directory_sync_enabled.metadata.json @@ -25,7 +25,7 @@ }, "Recommendation": { "Text": "Enable directory sync (SCIM) to automate user lifecycle management. This ensures that team membership stays synchronized with your identity provider, automatically provisioning new members and revoking access when employees leave or change roles.", - "Url": "https://hub.prowler.com/checks/vercel/team_directory_sync_enabled" + "Url": "https://hub.prowler.com/check/team_directory_sync_enabled" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/team/team_member_role_least_privilege/team_member_role_least_privilege.metadata.json b/prowler/providers/vercel/services/team/team_member_role_least_privilege/team_member_role_least_privilege.metadata.json index 0e4750d703..37abf769ef 100644 --- a/prowler/providers/vercel/services/team/team_member_role_least_privilege/team_member_role_least_privilege.metadata.json +++ b/prowler/providers/vercel/services/team/team_member_role_least_privilege/team_member_role_least_privilege.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Limit the number of team owners to the minimum required for administration. Assign the least privileged role necessary for each member's responsibilities. Use MEMBER, DEVELOPER, or VIEWER roles for non-administrative team members.", - "Url": "https://hub.prowler.com/checks/vercel/team_member_role_least_privilege" + "Url": "https://hub.prowler.com/check/team_member_role_least_privilege" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/team/team_no_stale_invitations/team_no_stale_invitations.metadata.json b/prowler/providers/vercel/services/team/team_no_stale_invitations/team_no_stale_invitations.metadata.json index d05461c5c1..16a1d942e1 100644 --- a/prowler/providers/vercel/services/team/team_no_stale_invitations/team_no_stale_invitations.metadata.json +++ b/prowler/providers/vercel/services/team/team_no_stale_invitations/team_no_stale_invitations.metadata.json @@ -24,7 +24,7 @@ }, "Recommendation": { "Text": "Regularly review and revoke stale team invitations. Establish a process to follow up on pending invitations within a reasonable timeframe and revoke those that are no longer needed to reduce the risk of unauthorized access.", - "Url": "https://hub.prowler.com/checks/vercel/team_no_stale_invitations" + "Url": "https://hub.prowler.com/check/team_no_stale_invitations" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/team/team_saml_sso_enabled/team_saml_sso_enabled.metadata.json b/prowler/providers/vercel/services/team/team_saml_sso_enabled/team_saml_sso_enabled.metadata.json index ebbe85ebc9..21785bf482 100644 --- a/prowler/providers/vercel/services/team/team_saml_sso_enabled/team_saml_sso_enabled.metadata.json +++ b/prowler/providers/vercel/services/team/team_saml_sso_enabled/team_saml_sso_enabled.metadata.json @@ -25,7 +25,7 @@ }, "Recommendation": { "Text": "Enable SAML SSO for the Vercel team to centralize authentication through your organization's identity provider. This ensures consistent security policies, simplifies user lifecycle management, and enables enforcement of MFA and other access controls.", - "Url": "https://hub.prowler.com/checks/vercel/team_saml_sso_enabled" + "Url": "https://hub.prowler.com/check/team_saml_sso_enabled" } }, "Categories": [ diff --git a/prowler/providers/vercel/services/team/team_saml_sso_enforced/team_saml_sso_enforced.metadata.json b/prowler/providers/vercel/services/team/team_saml_sso_enforced/team_saml_sso_enforced.metadata.json index f4de8e7ebe..feb43bc179 100644 --- a/prowler/providers/vercel/services/team/team_saml_sso_enforced/team_saml_sso_enforced.metadata.json +++ b/prowler/providers/vercel/services/team/team_saml_sso_enforced/team_saml_sso_enforced.metadata.json @@ -25,7 +25,7 @@ }, "Recommendation": { "Text": "Enforce SAML SSO for all team members to ensure authentication is managed exclusively through your identity provider. This prevents credential bypass, enforces MFA policies, and provides centralized access control and audit capabilities.", - "Url": "https://hub.prowler.com/checks/vercel/team_saml_sso_enforced" + "Url": "https://hub.prowler.com/check/team_saml_sso_enforced" } }, "Categories": [ diff --git a/pyproject.toml b/pyproject.toml index a3b6f7dece..0b4294a547 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "License :: OSI Approved :: Apache Software License" + "Programming Language :: Python :: 3.12" ] dependencies = [ "awsipranges==0.3.3", @@ -49,11 +48,11 @@ dependencies = [ "cryptography==46.0.6", "dash==3.1.1", "dash-bootstrap-components==2.0.3", - "defusedxml>=0.7.1", + "defusedxml==0.7.1", "detect-secrets==1.5.0", "dulwich==0.23.0", "google-api-python-client==2.163.0", - "google-auth-httplib2>=0.1,<0.3", + "google-auth-httplib2==0.2.0", "jsonschema==4.23.0", "kubernetes==32.0.1", "markdown==3.10.2", @@ -63,9 +62,9 @@ dependencies = [ "openstacksdk==4.2.0", "pandas==2.2.3", "py-ocsf-models==0.8.1", - "pydantic (>=2.0,<3.0)", + "pydantic==2.12.5", "pygithub==2.8.0", - "python-dateutil (>=2.9.0.post0,<3.0.0)", + "python-dateutil==2.9.0.post0", "pytz==2025.1", "schema==0.7.5", "shodan==1.31.0", @@ -96,7 +95,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"} name = "prowler" readme = "README.md" requires-python = ">=3.10,<3.13" -version = "5.23.0" +version = "5.24.0" [project.scripts] prowler = "prowler.__main__:prowler" @@ -127,7 +126,7 @@ mock = "5.2.0" moto = {extras = ["all"], version = "5.1.11"} openapi-schema-validator = "0.6.3" openapi-spec-validator = "0.7.1" -pre-commit = "4.2.0" +prek = "0.3.9" pylint = "3.3.4" pytest = "8.3.5" pytest-cov = "6.0.0" diff --git a/scripts/setup-git-hooks.sh b/scripts/setup-git-hooks.sh index ab7c01479d..c89ab607a7 100755 --- a/scripts/setup-git-hooks.sh +++ b/scripts/setup-git-hooks.sh @@ -1,7 +1,8 @@ #!/bin/bash # Setup Git Hooks for Prowler -# This script installs pre-commit hooks using the project's Poetry environment +# This script installs prek hooks using the project's Poetry environment +# or a system-wide prek installation set -e @@ -23,43 +24,50 @@ if ! git rev-parse --git-dir >/dev/null 2>&1; then exit 1 fi -# Check if Poetry is installed -if ! command -v poetry &>/dev/null; then - echo -e "${RED}❌ Poetry is not installed${NC}" - echo -e "${YELLOW} Install Poetry: https://python-poetry.org/docs/#installation${NC}" - exit 1 -fi - -# Check if pyproject.toml exists -if [ ! -f "pyproject.toml" ]; then - echo -e "${RED}❌ pyproject.toml not found${NC}" - echo -e "${YELLOW} Please run this script from the repository root${NC}" - exit 1 -fi - -# Check if dependencies are already installed -if ! poetry run python -c "import pre_commit" 2>/dev/null; then - echo -e "${YELLOW}📦 Installing project dependencies (including pre-commit)...${NC}" - poetry install --with dev -else - echo -e "${GREEN}✓${NC} Dependencies already installed" -fi - -echo "" -# Clear any existing core.hooksPath to avoid pre-commit conflicts +# Clear any existing core.hooksPath to avoid conflicts if git config --get core.hooksPath >/dev/null 2>&1; then echo -e "${YELLOW}🧹 Clearing existing core.hooksPath configuration...${NC}" git config --unset-all core.hooksPath fi -echo -e "${YELLOW}🔗 Installing pre-commit hooks...${NC}" -poetry run pre-commit install +echo "" + +# Full setup requires Poetry for system hooks (pylint, bandit, safety, vulture, trufflehog) +# These are installed as Python dev dependencies and used by local hooks in .pre-commit-config.yaml +if command -v poetry &>/dev/null && [ -f "pyproject.toml" ]; then + if poetry run prek --version &>/dev/null 2>&1; then + echo -e "${GREEN}✓${NC} prek and dependencies found via Poetry" + else + echo -e "${YELLOW}📦 Installing project dependencies (including prek)...${NC}" + poetry install --with dev + fi + echo -e "${YELLOW}🔗 Installing prek hooks...${NC}" + poetry run prek install --overwrite +elif command -v prek &>/dev/null; then + # prek is available system-wide but without Poetry dev deps + echo -e "${GREEN}✓${NC} prek found in PATH" + echo -e "${YELLOW}🔗 Installing prek hooks...${NC}" + prek install --overwrite + echo "" + echo -e "${YELLOW}⚠️ Warning: Some hooks require Python tools installed via Poetry:${NC}" + echo -e " pylint, bandit, safety, vulture, trufflehog" + echo -e " These hooks will be skipped unless you install them or run:" + echo -e " ${GREEN}poetry install --with dev${NC}" +else + echo -e "${RED}❌ prek is not installed${NC}" + echo -e "${YELLOW} Install prek using one of these methods:${NC}" + echo -e " • brew install prek" + echo -e " • pnpm add -g @j178/prek" + echo -e " • pip install prek" + echo -e " • See https://prek.j178.dev/installation/ for more options" + exit 1 +fi echo "" echo -e "${GREEN}✅ Git hooks successfully configured!${NC}" echo "" -echo -e "${YELLOW}📋 Pre-commit system:${NC}" -echo -e " • Python pre-commit manages all git hooks" +echo -e "${YELLOW}📋 Prek hook system:${NC}" +echo -e " • Prek manages all git hooks" echo -e " • API files: Python checks (black, flake8, bandit, etc.)" echo -e " • UI files: UI checks (TypeScript, ESLint, Claude Code validation)" echo "" diff --git a/tests/lib/check/check_test.py b/tests/lib/check/check_test.py index 84708b96b1..cda33e8fc6 100644 --- a/tests/lib/check/check_test.py +++ b/tests/lib/check/check_test.py @@ -1048,6 +1048,34 @@ class TestCheck: ) self.verify_metadata_check_id(base_directory) + def test_vercel_checks_metadata_is_valid(self): + base_directory = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "../../../", + "prowler/providers/vercel/services", + ) + ) + self.verify_metadata_check_id(base_directory) + + def test_vercel_checks_metadata_use_canonical_hub_urls(self): + base_directory = pathlib.Path(__file__).resolve().parents[3] / "prowler" + provider_path = base_directory / "providers" / "vercel" / "services" + + invalid_urls = [] + + for metadata_file_path in provider_path.rglob("*.metadata.json"): + with metadata_file_path.open("r") as metadata_file: + data = json.load(metadata_file) + + recommendation = data.get("Remediation", {}).get("Recommendation", {}) + url = recommendation.get("Url", "") + + if url.startswith("https://hub.prowler.com/checks/vercel/"): + invalid_urls.append(f"{metadata_file_path}: {url}") + + assert not invalid_urls, "\n".join(invalid_urls) + def verify_metadata_check_id(self, provider_path): errors = [] # Walk through the base directory to find all service directories diff --git a/tests/lib/outputs/compliance/ccc/__init__.py b/tests/lib/outputs/compliance/ccc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/lib/outputs/compliance/ccc/ccc_aws_test.py b/tests/lib/outputs/compliance/ccc/ccc_aws_test.py new file mode 100644 index 0000000000..39460fd0ec --- /dev/null +++ b/tests/lib/outputs/compliance/ccc/ccc_aws_test.py @@ -0,0 +1,138 @@ +from io import StringIO +from unittest import mock + +from freezegun import freeze_time +from mock import patch + +from prowler.lib.outputs.compliance.ccc.ccc_aws import CCC_AWS +from prowler.lib.outputs.compliance.ccc.models import CCC_AWSModel +from tests.lib.outputs.compliance.fixtures import CCC_AWS_FIXTURE +from tests.lib.outputs.fixtures.fixtures import generate_finding_output +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 + + +class TestAWSCCC: + def test_output_transform_evaluated_requirement(self): + findings = [ + generate_finding_output(compliance={"CCC-v2025.10": "CCC.Core.CN01.AR01"}) + ] + + output = CCC_AWS(findings, CCC_AWS_FIXTURE) + output_data = output.data[0] + + assert isinstance(output_data, CCC_AWSModel) + assert output_data.Provider == "aws" + assert output_data.AccountId == AWS_ACCOUNT_NUMBER + assert output_data.Region == AWS_REGION_EU_WEST_1 + assert output_data.Description == CCC_AWS_FIXTURE.Description + assert output_data.Requirements_Id == CCC_AWS_FIXTURE.Requirements[0].Id + assert ( + output_data.Requirements_Description + == CCC_AWS_FIXTURE.Requirements[0].Description + ) + attribute = CCC_AWS_FIXTURE.Requirements[0].Attributes[0] + assert output_data.Requirements_Attributes_FamilyName == attribute.FamilyName + assert ( + output_data.Requirements_Attributes_FamilyDescription + == attribute.FamilyDescription + ) + assert output_data.Requirements_Attributes_Section == attribute.Section + assert output_data.Requirements_Attributes_SubSection == attribute.SubSection + assert ( + output_data.Requirements_Attributes_SubSectionObjective + == attribute.SubSectionObjective + ) + assert ( + output_data.Requirements_Attributes_Applicability == attribute.Applicability + ) + assert ( + output_data.Requirements_Attributes_Recommendation + == attribute.Recommendation + ) + assert ( + output_data.Requirements_Attributes_SectionThreatMappings + == attribute.SectionThreatMappings + ) + assert ( + output_data.Requirements_Attributes_SectionGuidelineMappings + == attribute.SectionGuidelineMappings + ) + assert output_data.Status == "PASS" + assert output_data.StatusExtended == "" + assert output_data.ResourceId == "" + assert output_data.ResourceName == "" + assert output_data.CheckId == "service_test_check_id" + assert output_data.Muted is False + + def test_output_transform_manual_requirement(self): + # Use a finding for the evaluated requirement so the manual one is appended + # by the manual-loop branch (Checks=[]). + findings = [ + generate_finding_output(compliance={"CCC-v2025.10": "CCC.Core.CN01.AR01"}) + ] + + output = CCC_AWS(findings, CCC_AWS_FIXTURE) + # data[0] is the evaluated PASS row, data[1] is the manual row + manual_row = output.data[1] + + assert isinstance(manual_row, CCC_AWSModel) + assert manual_row.Provider == "aws" + assert manual_row.AccountId == "" + assert manual_row.Region == "" + assert manual_row.Description == CCC_AWS_FIXTURE.Description + assert manual_row.Requirements_Id == CCC_AWS_FIXTURE.Requirements[1].Id + manual_attribute = CCC_AWS_FIXTURE.Requirements[1].Attributes[0] + assert ( + manual_row.Requirements_Attributes_FamilyName == manual_attribute.FamilyName + ) + assert manual_row.Requirements_Attributes_Section == manual_attribute.Section + assert manual_row.Status == "MANUAL" + assert manual_row.StatusExtended == "Manual check" + assert manual_row.ResourceId == "manual_check" + assert manual_row.ResourceName == "Manual check" + assert manual_row.CheckId == "manual" + assert manual_row.Muted is False + + @freeze_time("2025-01-01 00:00:00") + @mock.patch( + "prowler.lib.outputs.compliance.ccc.ccc_aws.timestamp", + "2025-01-01 00:00:00", + ) + def test_batch_write_data_to_file(self): + mock_file = StringIO() + findings = [ + generate_finding_output(compliance={"CCC-v2025.10": "CCC.Core.CN01.AR01"}) + ] + output = CCC_AWS(findings, CCC_AWS_FIXTURE) + output._file_descriptor = mock_file + + with patch.object(mock_file, "close", return_value=None): + output.batch_write_data_to_file() + + mock_file.seek(0) + content = mock_file.read() + + # Header check: AWS-specific columns must be present + header = content.split("\r\n", 1)[0] + assert "ACCOUNTID" in header + assert "REGION" in header + assert "REQUIREMENTS_ATTRIBUTES_FAMILYNAME" in header + assert "REQUIREMENTS_ATTRIBUTES_SECTION" in header + assert "REQUIREMENTS_ATTRIBUTES_APPLICABILITY" in header + assert "REQUIREMENTS_ATTRIBUTES_SECTIONTHREATMAPPINGS" in header + # Header should NOT contain Azure or GCP-only columns + assert "SUBSCRIPTIONID" not in header + assert "PROJECTID" not in header + + # Body checks: evaluated row + manual row + rows = [r for r in content.split("\r\n") if r] + assert len(rows) == 3 # header + evaluated + manual + assert "CCC.Core.CN01.AR01" in rows[1] + assert "PASS" in rows[1] + assert AWS_ACCOUNT_NUMBER in rows[1] + assert AWS_REGION_EU_WEST_1 in rows[1] + assert "CCC.IAM.CN01.AR01" in rows[2] + assert "MANUAL" in rows[2] + assert "manual_check" in rows[2] + # The frozen timestamp should appear + assert "2025-01-01 00:00:00" in rows[1] diff --git a/tests/lib/outputs/compliance/ccc/ccc_azure_test.py b/tests/lib/outputs/compliance/ccc/ccc_azure_test.py new file mode 100644 index 0000000000..a3a2f7d028 --- /dev/null +++ b/tests/lib/outputs/compliance/ccc/ccc_azure_test.py @@ -0,0 +1,99 @@ +from io import StringIO +from unittest import mock + +from freezegun import freeze_time +from mock import patch + +from prowler.lib.outputs.compliance.ccc.ccc_azure import CCC_Azure +from prowler.lib.outputs.compliance.ccc.models import CCC_AzureModel +from tests.lib.outputs.compliance.fixtures import CCC_AZURE_FIXTURE +from tests.lib.outputs.fixtures.fixtures import generate_finding_output +from tests.providers.azure.azure_fixtures import AZURE_SUBSCRIPTION_ID + +AZURE_LOCATION = "westeurope" + + +class TestAzureCCC: + def test_output_transform_evaluated_requirement(self): + findings = [ + generate_finding_output( + provider="azure", + compliance={"CCC-v2025.10": "CCC.Core.CN01.AR01"}, + account_uid=AZURE_SUBSCRIPTION_ID, + region=AZURE_LOCATION, + ) + ] + + output = CCC_Azure(findings, CCC_AZURE_FIXTURE) + output_data = output.data[0] + + assert isinstance(output_data, CCC_AzureModel) + assert output_data.Provider == "azure" + assert output_data.SubscriptionId == AZURE_SUBSCRIPTION_ID + assert output_data.Location == AZURE_LOCATION + assert output_data.Description == CCC_AZURE_FIXTURE.Description + assert output_data.Requirements_Id == CCC_AZURE_FIXTURE.Requirements[0].Id + attribute = CCC_AZURE_FIXTURE.Requirements[0].Attributes[0] + assert output_data.Requirements_Attributes_FamilyName == attribute.FamilyName + assert output_data.Requirements_Attributes_Section == attribute.Section + assert ( + output_data.Requirements_Attributes_Applicability == attribute.Applicability + ) + assert output_data.Status == "PASS" + assert output_data.CheckId == "service_test_check_id" + + def test_output_transform_manual_requirement(self): + findings = [ + generate_finding_output( + provider="azure", + compliance={"CCC-v2025.10": "CCC.Core.CN01.AR01"}, + account_uid=AZURE_SUBSCRIPTION_ID, + region=AZURE_LOCATION, + ) + ] + output = CCC_Azure(findings, CCC_AZURE_FIXTURE) + manual_row = output.data[1] + + assert isinstance(manual_row, CCC_AzureModel) + assert manual_row.Provider == "azure" + assert manual_row.SubscriptionId == "" + assert manual_row.Location == "" + assert manual_row.Requirements_Id == CCC_AZURE_FIXTURE.Requirements[1].Id + assert manual_row.Status == "MANUAL" + assert manual_row.CheckId == "manual" + + @freeze_time("2025-01-01 00:00:00") + @mock.patch( + "prowler.lib.outputs.compliance.ccc.ccc_azure.timestamp", + "2025-01-01 00:00:00", + ) + def test_batch_write_data_to_file(self): + mock_file = StringIO() + findings = [ + generate_finding_output( + provider="azure", + compliance={"CCC-v2025.10": "CCC.Core.CN01.AR01"}, + account_uid=AZURE_SUBSCRIPTION_ID, + region=AZURE_LOCATION, + ) + ] + output = CCC_Azure(findings, CCC_AZURE_FIXTURE) + output._file_descriptor = mock_file + + with patch.object(mock_file, "close", return_value=None): + output.batch_write_data_to_file() + + mock_file.seek(0) + content = mock_file.read() + header = content.split("\r\n", 1)[0] + assert "SUBSCRIPTIONID" in header + assert "LOCATION" in header + assert "ACCOUNTID" not in header + assert "PROJECTID" not in header + assert "REGION" not in header + rows = [r for r in content.split("\r\n") if r] + assert len(rows) == 3 + assert "CCC.Core.CN01.AR01" in rows[1] + assert AZURE_SUBSCRIPTION_ID in rows[1] + assert "CCC.IAM.CN01.AR01" in rows[2] + assert "MANUAL" in rows[2] diff --git a/tests/lib/outputs/compliance/ccc/ccc_gcp_test.py b/tests/lib/outputs/compliance/ccc/ccc_gcp_test.py new file mode 100644 index 0000000000..9ba2127235 --- /dev/null +++ b/tests/lib/outputs/compliance/ccc/ccc_gcp_test.py @@ -0,0 +1,99 @@ +from io import StringIO +from unittest import mock + +from freezegun import freeze_time +from mock import patch + +from prowler.lib.outputs.compliance.ccc.ccc_gcp import CCC_GCP +from prowler.lib.outputs.compliance.ccc.models import CCC_GCPModel +from tests.lib.outputs.compliance.fixtures import CCC_GCP_FIXTURE +from tests.lib.outputs.fixtures.fixtures import generate_finding_output + +GCP_PROJECT_ID = "test-project" +GCP_LOCATION = "europe-west1" + + +class TestGCPCCC: + def test_output_transform_evaluated_requirement(self): + findings = [ + generate_finding_output( + provider="gcp", + compliance={"CCC-v2025.10": "CCC.Core.CN01.AR01"}, + account_uid=GCP_PROJECT_ID, + region=GCP_LOCATION, + ) + ] + + output = CCC_GCP(findings, CCC_GCP_FIXTURE) + output_data = output.data[0] + + assert isinstance(output_data, CCC_GCPModel) + assert output_data.Provider == "gcp" + assert output_data.ProjectId == GCP_PROJECT_ID + assert output_data.Location == GCP_LOCATION + assert output_data.Description == CCC_GCP_FIXTURE.Description + assert output_data.Requirements_Id == CCC_GCP_FIXTURE.Requirements[0].Id + attribute = CCC_GCP_FIXTURE.Requirements[0].Attributes[0] + assert output_data.Requirements_Attributes_FamilyName == attribute.FamilyName + assert output_data.Requirements_Attributes_Section == attribute.Section + assert ( + output_data.Requirements_Attributes_Applicability == attribute.Applicability + ) + assert output_data.Status == "PASS" + assert output_data.CheckId == "service_test_check_id" + + def test_output_transform_manual_requirement(self): + findings = [ + generate_finding_output( + provider="gcp", + compliance={"CCC-v2025.10": "CCC.Core.CN01.AR01"}, + account_uid=GCP_PROJECT_ID, + region=GCP_LOCATION, + ) + ] + output = CCC_GCP(findings, CCC_GCP_FIXTURE) + manual_row = output.data[1] + + assert isinstance(manual_row, CCC_GCPModel) + assert manual_row.Provider == "gcp" + assert manual_row.ProjectId == "" + assert manual_row.Location == "" + assert manual_row.Requirements_Id == CCC_GCP_FIXTURE.Requirements[1].Id + assert manual_row.Status == "MANUAL" + assert manual_row.CheckId == "manual" + + @freeze_time("2025-01-01 00:00:00") + @mock.patch( + "prowler.lib.outputs.compliance.ccc.ccc_gcp.timestamp", + "2025-01-01 00:00:00", + ) + def test_batch_write_data_to_file(self): + mock_file = StringIO() + findings = [ + generate_finding_output( + provider="gcp", + compliance={"CCC-v2025.10": "CCC.Core.CN01.AR01"}, + account_uid=GCP_PROJECT_ID, + region=GCP_LOCATION, + ) + ] + output = CCC_GCP(findings, CCC_GCP_FIXTURE) + output._file_descriptor = mock_file + + with patch.object(mock_file, "close", return_value=None): + output.batch_write_data_to_file() + + mock_file.seek(0) + content = mock_file.read() + header = content.split("\r\n", 1)[0] + assert "PROJECTID" in header + assert "LOCATION" in header + assert "ACCOUNTID" not in header + assert "SUBSCRIPTIONID" not in header + assert "REGION" not in header + rows = [r for r in content.split("\r\n") if r] + assert len(rows) == 3 + assert "CCC.Core.CN01.AR01" in rows[1] + assert GCP_PROJECT_ID in rows[1] + assert "CCC.IAM.CN01.AR01" in rows[2] + assert "MANUAL" in rows[2] diff --git a/tests/lib/outputs/compliance/fixtures.py b/tests/lib/outputs/compliance/fixtures.py index 7b29411663..41c68d148f 100644 --- a/tests/lib/outputs/compliance/fixtures.py +++ b/tests/lib/outputs/compliance/fixtures.py @@ -1,5 +1,6 @@ from prowler.lib.check.compliance_models import ( AWS_Well_Architected_Requirement_Attribute, + CCC_Requirement_Attribute, CIS_Requirement_Attribute, Compliance, Compliance_Requirement, @@ -1022,3 +1023,169 @@ PROWLER_THREATSCORE_M365 = Compliance( ), ], ) + + +# CCC fixtures cover the three providers Prowler ships catalogs for. Each +# fixture has one auto-evaluated requirement (with Checks) and one manual +# requirement (Checks=[]) so test suites can exercise both paths. +CCC_AWS_FIXTURE = Compliance( + Framework="CCC", + Name="Common Cloud Controls Catalog (CCC)", + Provider="AWS", + Version="v2025.10", + Description="Common Cloud Controls Catalog (CCC) for AWS", + Requirements=[ + Compliance_Requirement( + Checks=["service_test_check_id"], + Id="CCC.Core.CN01.AR01", + Description="When a port is exposed for non-SSH network traffic, all traffic MUST include a TLS handshake AND be encrypted using TLS 1.3 or higher.", + Attributes=[ + CCC_Requirement_Attribute( + FamilyName="Data", + FamilyDescription="The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + Section="CCC.Core.CN01 Encrypt Data for Transmission", + SubSection="", + SubSectionObjective="Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + Applicability=["tlp-green", "tlp-amber", "tlp-red"], + Recommendation="Most cloud services enable TLS 1.3 by default.", + SectionThreatMappings=[ + {"ReferenceId": "CCC", "Identifiers": ["CCC.Core.TH02"]} + ], + SectionGuidelineMappings=[ + {"ReferenceId": "CCM", "Identifiers": ["CEK-03", "CEK-04"]} + ], + ) + ], + ), + Compliance_Requirement( + Checks=[], + Id="CCC.IAM.CN01.AR01", + Description="When an identity policy for a non-administrative principal is evaluated, it MUST NOT grant permissions for creating credentials or generating temporary session tokens.", + Attributes=[ + CCC_Requirement_Attribute( + FamilyName="Identity and Access Management", + FamilyDescription="Controls that restrict who can access and modify IAM resources.", + Section="CCC.IAM.CN01 Restrict IAM User Credentials Creation", + SubSection="", + SubSectionObjective="Prevent non-administrative principals from creating new long-lived credentials.", + Applicability=["tlp-clear", "tlp-green", "tlp-amber", "tlp-red"], + Recommendation="", + SectionThreatMappings=[ + {"ReferenceId": "CCC", "Identifiers": ["CCC.IAM.TH03"]} + ], + SectionGuidelineMappings=[ + {"ReferenceId": "NIST-CSF", "Identifiers": ["PR.AA-05"]} + ], + ) + ], + ), + ], +) + +CCC_AZURE_FIXTURE = Compliance( + Framework="CCC", + Name="Common Cloud Controls Catalog (CCC)", + Provider="Azure", + Version="v2025.10", + Description="Common Cloud Controls Catalog (CCC) for Azure", + Requirements=[ + Compliance_Requirement( + Checks=["service_test_check_id"], + Id="CCC.Core.CN01.AR01", + Description="When a port is exposed for non-SSH network traffic, all traffic MUST include a TLS handshake AND be encrypted using TLS 1.3 or higher.", + Attributes=[ + CCC_Requirement_Attribute( + FamilyName="Data", + FamilyDescription="The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + Section="CCC.Core.CN01 Encrypt Data for Transmission", + SubSection="", + SubSectionObjective="Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + Applicability=["tlp-green", "tlp-amber", "tlp-red"], + Recommendation="Most cloud services enable TLS 1.3 by default.", + SectionThreatMappings=[ + {"ReferenceId": "CCC", "Identifiers": ["CCC.Core.TH02"]} + ], + SectionGuidelineMappings=[ + {"ReferenceId": "CCM", "Identifiers": ["CEK-03", "CEK-04"]} + ], + ) + ], + ), + Compliance_Requirement( + Checks=[], + Id="CCC.IAM.CN01.AR01", + Description="When an identity policy for a non-administrative principal is evaluated, it MUST NOT grant permissions for creating credentials.", + Attributes=[ + CCC_Requirement_Attribute( + FamilyName="Identity and Access Management", + FamilyDescription="Controls that restrict who can access and modify IAM resources.", + Section="CCC.IAM.CN01 Restrict IAM User Credentials Creation", + SubSection="", + SubSectionObjective="Prevent non-administrative principals from creating new long-lived credentials.", + Applicability=["tlp-clear", "tlp-green", "tlp-amber", "tlp-red"], + Recommendation="", + SectionThreatMappings=[ + {"ReferenceId": "CCC", "Identifiers": ["CCC.IAM.TH03"]} + ], + SectionGuidelineMappings=[ + {"ReferenceId": "NIST-CSF", "Identifiers": ["PR.AA-05"]} + ], + ) + ], + ), + ], +) + +CCC_GCP_FIXTURE = Compliance( + Framework="CCC", + Name="Common Cloud Controls Catalog (CCC)", + Provider="GCP", + Version="v2025.10", + Description="Common Cloud Controls Catalog (CCC) for GCP", + Requirements=[ + Compliance_Requirement( + Checks=["service_test_check_id"], + Id="CCC.Core.CN01.AR01", + Description="When a port is exposed for non-SSH network traffic, all traffic MUST include a TLS handshake AND be encrypted using TLS 1.3 or higher.", + Attributes=[ + CCC_Requirement_Attribute( + FamilyName="Data", + FamilyDescription="The Data control family ensures the confidentiality, integrity, availability, and sovereignty of data across its lifecycle.", + Section="CCC.Core.CN01 Encrypt Data for Transmission", + SubSection="", + SubSectionObjective="Ensure that all communications are encrypted in transit to protect data integrity and confidentiality.", + Applicability=["tlp-green", "tlp-amber", "tlp-red"], + Recommendation="Most cloud services enable TLS 1.3 by default.", + SectionThreatMappings=[ + {"ReferenceId": "CCC", "Identifiers": ["CCC.Core.TH02"]} + ], + SectionGuidelineMappings=[ + {"ReferenceId": "CCM", "Identifiers": ["CEK-03", "CEK-04"]} + ], + ) + ], + ), + Compliance_Requirement( + Checks=[], + Id="CCC.IAM.CN01.AR01", + Description="When an identity policy for a non-administrative principal is evaluated, it MUST NOT grant permissions for creating credentials.", + Attributes=[ + CCC_Requirement_Attribute( + FamilyName="Identity and Access Management", + FamilyDescription="Controls that restrict who can access and modify IAM resources.", + Section="CCC.IAM.CN01 Restrict IAM User Credentials Creation", + SubSection="", + SubSectionObjective="Prevent non-administrative principals from creating new long-lived credentials.", + Applicability=["tlp-clear", "tlp-green", "tlp-amber", "tlp-red"], + Recommendation="", + SectionThreatMappings=[ + {"ReferenceId": "CCC", "Identifiers": ["CCC.IAM.TH03"]} + ], + SectionGuidelineMappings=[ + {"ReferenceId": "NIST-CSF", "Identifiers": ["PR.AA-05"]} + ], + ) + ], + ), + ], +) diff --git a/tests/providers/aws/services/bedrock/bedrock_full_access_policy_attached/bedrock_full_access_policy_attached_test.py b/tests/providers/aws/services/bedrock/bedrock_full_access_policy_attached/bedrock_full_access_policy_attached_test.py new file mode 100644 index 0000000000..1a261d96cc --- /dev/null +++ b/tests/providers/aws/services/bedrock/bedrock_full_access_policy_attached/bedrock_full_access_policy_attached_test.py @@ -0,0 +1,357 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from prowler.providers.aws.services.iam.iam_service import Role +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + +AWS_ACCOUNT_ID = "123456789012" + +ASSUME_ROLE_POLICY_DOCUMENT = { + "Version": "2012-10-17", + "Statement": { + "Sid": "test", + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{AWS_ACCOUNT_ID}:root"}, + "Action": "sts:AssumeRole", + }, +} + + +class Test_bedrock_full_access_policy_attached: + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_no_roles(self): + from prowler.providers.aws.services.iam.iam_service import IAM + + 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.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached import ( + bedrock_full_access_policy_attached, + ) + + check = bedrock_full_access_policy_attached() + result = check.execute() + assert len(result) == 0 + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_role_without_bedrock_full_access_policy(self): + iam = client("iam") + role_name = "test" + response = iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ASSUME_ROLE_POLICY_DOCUMENT), + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + from prowler.providers.aws.services.iam.iam_service import IAM + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached import ( + bedrock_full_access_policy_attached, + ) + + check = bedrock_full_access_policy_attached() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "IAM Role test does not have AmazonBedrockFullAccess policy attached." + ) + assert result[0].resource_id == "test" + assert result[0].resource_arn == response["Role"]["Arn"] + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_role_with_other_policy(self): + iam = client("iam") + role_name = "test" + response = iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ASSUME_ROLE_POLICY_DOCUMENT), + ) + iam.attach_role_policy( + RoleName=role_name, + PolicyArn="arn:aws:iam::aws:policy/SecurityAudit", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + from prowler.providers.aws.services.iam.iam_service import IAM + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached import ( + bedrock_full_access_policy_attached, + ) + + check = bedrock_full_access_policy_attached() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "IAM Role test does not have AmazonBedrockFullAccess policy attached." + ) + assert result[0].resource_id == "test" + assert result[0].resource_arn == response["Role"]["Arn"] + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_role_with_bedrock_full_access_policy(self): + iam = client("iam") + role_name = "test" + response = iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ASSUME_ROLE_POLICY_DOCUMENT), + ) + iam.attach_role_policy( + RoleName=role_name, + PolicyArn="arn:aws:iam::aws:policy/AmazonBedrockFullAccess", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + from prowler.providers.aws.services.iam.iam_service import IAM + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached import ( + bedrock_full_access_policy_attached, + ) + + check = bedrock_full_access_policy_attached() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "IAM Role test has AmazonBedrockFullAccess policy attached." + ) + assert result[0].resource_id == "test" + assert result[0].resource_arn == response["Role"]["Arn"] + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_asterisk_principal_role_with_bedrock_full_access_policy(self): + iam = client("iam") + role_name = "test" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": { + "Sid": "test", + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "sts:AssumeRole", + }, + } + response = iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + ) + iam.attach_role_policy( + RoleName=role_name, + PolicyArn="arn:aws:iam::aws:policy/AmazonBedrockFullAccess", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + from prowler.providers.aws.services.iam.iam_service import IAM + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached import ( + bedrock_full_access_policy_attached, + ) + + check = bedrock_full_access_policy_attached() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "IAM Role test has AmazonBedrockFullAccess policy attached." + ) + assert result[0].resource_id == "test" + assert result[0].resource_arn == response["Role"]["Arn"] + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_multiple_roles_mixed_policies(self): + iam = client("iam") + + # Create a compliant role (no AmazonBedrockFullAccess) + compliant_response = iam.create_role( + RoleName="compliant-role", + AssumeRolePolicyDocument=dumps(ASSUME_ROLE_POLICY_DOCUMENT), + ) + iam.attach_role_policy( + RoleName="compliant-role", + PolicyArn="arn:aws:iam::aws:policy/SecurityAudit", + ) + + # Create a non-compliant role (with AmazonBedrockFullAccess) + non_compliant_response = iam.create_role( + RoleName="non-compliant-role", + AssumeRolePolicyDocument=dumps(ASSUME_ROLE_POLICY_DOCUMENT), + ) + iam.attach_role_policy( + RoleName="non-compliant-role", + PolicyArn="arn:aws:iam::aws:policy/AmazonBedrockFullAccess", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + from prowler.providers.aws.services.iam.iam_service import IAM + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached import ( + bedrock_full_access_policy_attached, + ) + + check = bedrock_full_access_policy_attached() + result = check.execute() + assert len(result) == 2 + + # Sort results by resource_id for deterministic assertions + result = sorted(result, key=lambda r: r.resource_id) + + # Compliant role + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "IAM Role compliant-role does not have AmazonBedrockFullAccess policy attached." + ) + assert result[0].resource_id == "compliant-role" + assert result[0].resource_arn == compliant_response["Role"]["Arn"] + assert result[0].region == AWS_REGION_US_EAST_1 + + # Non-compliant role + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "IAM Role non-compliant-role has AmazonBedrockFullAccess policy attached." + ) + assert result[1].resource_id == "non-compliant-role" + assert result[1].resource_arn == non_compliant_response["Role"]["Arn"] + assert result[1].region == AWS_REGION_US_EAST_1 + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_only_aws_service_linked_roles(self): + iam_client = mock.MagicMock + iam_client.roles = [] + iam_client.roles.append( + Role( + name="AWSServiceRoleForAmazonGuardDuty", + arn="arn:aws:iam::106908755756:role/aws-service-role/guardduty.amazonaws.com/AWSServiceRoleForAmazonGuardDuty", + assume_role_policy={ + "Version": "2008-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "ec2.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + }, + is_service_role=True, + ) + ) + + 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.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached.iam_client", + new=iam_client, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached import ( + bedrock_full_access_policy_attached, + ) + + check = bedrock_full_access_policy_attached() + result = check.execute() + assert len(result) == 0 + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_access_denied(self): + iam_client = mock.MagicMock + iam_client.roles = None + + 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.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached.iam_client", + new=iam_client, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_full_access_policy_attached.bedrock_full_access_policy_attached import ( + bedrock_full_access_policy_attached, + ) + + check = bedrock_full_access_policy_attached() + result = check.execute() + assert len(result) == 0 diff --git a/tests/providers/aws/services/bedrock/bedrock_vpc_endpoints_configured/bedrock_vpc_endpoints_configured_test.py b/tests/providers/aws/services/bedrock/bedrock_vpc_endpoints_configured/bedrock_vpc_endpoints_configured_test.py new file mode 100644 index 0000000000..a4850d8a39 --- /dev/null +++ b/tests/providers/aws/services/bedrock/bedrock_vpc_endpoints_configured/bedrock_vpc_endpoints_configured_test.py @@ -0,0 +1,558 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from prowler.providers.aws.services.bedrock.bedrock_service import ( + Guardrail, + LoggingConfiguration, +) +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + +BEDROCK_SERVICES = [ + "com.amazonaws.us-east-1.bedrock", + "com.amazonaws.us-east-1.bedrock-runtime", + "com.amazonaws.us-east-1.bedrock-agent", + "com.amazonaws.us-east-1.bedrock-agent-runtime", + "com.amazonaws.us-east-1.bedrock-mantle", +] + +MOCK_BEDROCK_CLIENT = mock.MagicMock( + logging_configurations={AWS_REGION_US_EAST_1: LoggingConfiguration(enabled=True)}, + guardrails={}, +) + +MOCK_BEDROCK_AGENT_CLIENT = mock.MagicMock(agents={}) + +MOCK_BEDROCK_CLIENT_NO_ACTIVITY = mock.MagicMock( + logging_configurations={AWS_REGION_US_EAST_1: LoggingConfiguration(enabled=False)}, + guardrails={}, +) + +MOCK_BEDROCK_AGENT_CLIENT_NO_ACTIVITY = mock.MagicMock(agents={}) + +CHECK_MODULE = "prowler.providers.aws.services.bedrock.bedrock_vpc_endpoints_configured.bedrock_vpc_endpoints_configured" + + +class Test_bedrock_vpc_endpoints_configured: + @mock_aws + def test_no_resources(self): + """Test with no in-use VPCs and scan_unused_services disabled - should return no results.""" + client("ec2", region_name=AWS_REGION_US_EAST_1) + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with ( + mock.patch( + f"{CHECK_MODULE}.vpc_client", + new=VPC(aws_provider), + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_client", + new=MOCK_BEDROCK_CLIENT, + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_agent_client", + new=MOCK_BEDROCK_AGENT_CLIENT, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_vpc_endpoints_configured.bedrock_vpc_endpoints_configured import ( + bedrock_vpc_endpoints_configured, + ) + + check = bedrock_vpc_endpoints_configured() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_no_bedrock_activity(self): + """Test VPCs in region with no Bedrock activity - should return no results.""" + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + ec2_client.describe_vpcs() + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + 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( + f"{CHECK_MODULE}.vpc_client", + new=VPC(aws_provider), + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_client", + new=MOCK_BEDROCK_CLIENT_NO_ACTIVITY, + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_agent_client", + new=MOCK_BEDROCK_AGENT_CLIENT_NO_ACTIVITY, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_vpc_endpoints_configured.bedrock_vpc_endpoints_configured import ( + bedrock_vpc_endpoints_configured, + ) + + check = bedrock_vpc_endpoints_configured() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_vpc_no_endpoints(self): + """Test VPC with no VPC endpoints at all - should FAIL with all services missing.""" + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + vpc_id = ec2_client.describe_vpcs()["Vpcs"][0]["VpcId"] + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + 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( + f"{CHECK_MODULE}.vpc_client", + new=VPC(aws_provider), + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_client", + new=MOCK_BEDROCK_CLIENT, + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_agent_client", + new=MOCK_BEDROCK_AGENT_CLIENT, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_vpc_endpoints_configured.bedrock_vpc_endpoints_configured import ( + bedrock_vpc_endpoints_configured, + ) + + check = bedrock_vpc_endpoints_configured() + result = check.execute() + + assert len(result) == 1 + assert result[0].resource_id == vpc_id + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].status == "FAIL" + assert "Bedrock control plane" in result[0].status_extended + assert "Bedrock runtime" in result[0].status_extended + assert "Bedrock agent control plane" in result[0].status_extended + assert "Bedrock agent runtime" in result[0].status_extended + assert "Bedrock Mantle" in result[0].status_extended + + @mock_aws + def test_vpc_only_bedrock_runtime_endpoint(self): + """Test VPC with only Bedrock runtime endpoint - should FAIL with three services missing.""" + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + + vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"] + + route_table = ec2_client.create_route_table(VpcId=vpc["VpcId"])["RouteTable"] + ec2_client.create_vpc_endpoint( + VpcId=vpc["VpcId"], + ServiceName="com.amazonaws.us-east-1.bedrock-runtime", + RouteTableIds=[route_table["RouteTableId"]], + VpcEndpointType="Interface", + ) + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + 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( + f"{CHECK_MODULE}.vpc_client", + new=VPC(aws_provider), + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_client", + new=MOCK_BEDROCK_CLIENT, + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_agent_client", + new=MOCK_BEDROCK_AGENT_CLIENT, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_vpc_endpoints_configured.bedrock_vpc_endpoints_configured import ( + bedrock_vpc_endpoints_configured, + ) + + check = bedrock_vpc_endpoints_configured() + result = check.execute() + + # Default VPC + created VPC + assert len(result) == 2 + finding = next(f for f in result if f.resource_id == vpc["VpcId"]) + assert finding.region == AWS_REGION_US_EAST_1 + assert finding.status == "FAIL" + assert "Bedrock runtime" not in finding.status_extended + assert "Bedrock control plane" in finding.status_extended + assert "Bedrock agent control plane" in finding.status_extended + assert "Bedrock agent runtime" in finding.status_extended + assert "Bedrock Mantle" in finding.status_extended + assert ( + finding.resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:vpc/{vpc['VpcId']}" + ) + + @mock_aws + def test_vpc_only_bedrock_agent_runtime_endpoint(self): + """Test VPC with only Bedrock agent runtime endpoint - should FAIL with three services missing.""" + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + + vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"] + + route_table = ec2_client.create_route_table(VpcId=vpc["VpcId"])["RouteTable"] + ec2_client.create_vpc_endpoint( + VpcId=vpc["VpcId"], + ServiceName="com.amazonaws.us-east-1.bedrock-agent-runtime", + RouteTableIds=[route_table["RouteTableId"]], + VpcEndpointType="Interface", + ) + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + 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( + f"{CHECK_MODULE}.vpc_client", + new=VPC(aws_provider), + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_client", + new=MOCK_BEDROCK_CLIENT, + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_agent_client", + new=MOCK_BEDROCK_AGENT_CLIENT, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_vpc_endpoints_configured.bedrock_vpc_endpoints_configured import ( + bedrock_vpc_endpoints_configured, + ) + + check = bedrock_vpc_endpoints_configured() + result = check.execute() + + assert len(result) == 2 + finding = next(f for f in result if f.resource_id == vpc["VpcId"]) + assert finding.region == AWS_REGION_US_EAST_1 + assert finding.status == "FAIL" + assert "Bedrock agent runtime" not in finding.status_extended + assert "Bedrock control plane" in finding.status_extended + assert "Bedrock runtime" in finding.status_extended + assert "Bedrock agent control plane" in finding.status_extended + assert "Bedrock Mantle" in finding.status_extended + assert ( + finding.resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:vpc/{vpc['VpcId']}" + ) + + @mock_aws + def test_vpc_all_bedrock_endpoints(self): + """Test VPC with all four Bedrock endpoints - should PASS.""" + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + + vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"] + vpc_default_id = ec2_client.describe_vpcs()["Vpcs"][0]["VpcId"] + + route_table = ec2_client.create_route_table(VpcId=vpc["VpcId"])["RouteTable"] + for svc in BEDROCK_SERVICES: + ec2_client.create_vpc_endpoint( + VpcId=vpc["VpcId"], + ServiceName=svc, + RouteTableIds=[route_table["RouteTableId"]], + VpcEndpointType="Interface", + ) + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + 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( + f"{CHECK_MODULE}.vpc_client", + new=VPC(aws_provider), + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_client", + new=MOCK_BEDROCK_CLIENT, + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_agent_client", + new=MOCK_BEDROCK_AGENT_CLIENT, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_vpc_endpoints_configured.bedrock_vpc_endpoints_configured import ( + bedrock_vpc_endpoints_configured, + ) + + check = bedrock_vpc_endpoints_configured() + result = check.execute() + + assert len(result) == 2 + finding = next(f for f in result if f.resource_id == vpc["VpcId"]) + assert finding.region == AWS_REGION_US_EAST_1 + assert finding.status == "PASS" + assert ( + finding.status_extended + == f"VPC {vpc['VpcId']} has VPC endpoints for all Bedrock services." + ) + assert ( + finding.resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:vpc/{vpc['VpcId']}" + ) + + # Default VPC should FAIL + default_finding = next( + f for f in result if f.resource_id == vpc_default_id + ) + assert default_finding.status == "FAIL" + + @mock_aws + def test_vpc_only_runtime_endpoints(self): + """Test VPC with only both runtime endpoints but missing control plane endpoints - should FAIL.""" + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + + vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"] + + route_table = ec2_client.create_route_table(VpcId=vpc["VpcId"])["RouteTable"] + ec2_client.create_vpc_endpoint( + VpcId=vpc["VpcId"], + ServiceName="com.amazonaws.us-east-1.bedrock-runtime", + RouteTableIds=[route_table["RouteTableId"]], + VpcEndpointType="Interface", + ) + ec2_client.create_vpc_endpoint( + VpcId=vpc["VpcId"], + ServiceName="com.amazonaws.us-east-1.bedrock-agent-runtime", + RouteTableIds=[route_table["RouteTableId"]], + VpcEndpointType="Interface", + ) + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + 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( + f"{CHECK_MODULE}.vpc_client", + new=VPC(aws_provider), + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_client", + new=MOCK_BEDROCK_CLIENT, + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_agent_client", + new=MOCK_BEDROCK_AGENT_CLIENT, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_vpc_endpoints_configured.bedrock_vpc_endpoints_configured import ( + bedrock_vpc_endpoints_configured, + ) + + check = bedrock_vpc_endpoints_configured() + result = check.execute() + + assert len(result) == 2 + finding = next(f for f in result if f.resource_id == vpc["VpcId"]) + assert finding.status == "FAIL" + assert "Bedrock control plane" in finding.status_extended + assert "Bedrock agent control plane" in finding.status_extended + assert "Bedrock Mantle" in finding.status_extended + assert "Bedrock runtime" not in finding.status_extended + assert "Bedrock agent runtime" not in finding.status_extended + + @mock_aws + def test_vpc_unrelated_endpoint_only(self): + """Test VPC with only an unrelated endpoint (S3) - should FAIL with all services missing.""" + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + + vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"] + + route_table = ec2_client.create_route_table(VpcId=vpc["VpcId"])["RouteTable"] + ec2_client.create_vpc_endpoint( + VpcId=vpc["VpcId"], + ServiceName="com.amazonaws.us-east-1.s3", + RouteTableIds=[route_table["RouteTableId"]], + VpcEndpointType="Gateway", + ) + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + 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( + f"{CHECK_MODULE}.vpc_client", + new=VPC(aws_provider), + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_client", + new=MOCK_BEDROCK_CLIENT, + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_agent_client", + new=MOCK_BEDROCK_AGENT_CLIENT, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_vpc_endpoints_configured.bedrock_vpc_endpoints_configured import ( + bedrock_vpc_endpoints_configured, + ) + + check = bedrock_vpc_endpoints_configured() + result = check.execute() + + assert len(result) == 2 + finding = next(f for f in result if f.resource_id == vpc["VpcId"]) + assert finding.region == AWS_REGION_US_EAST_1 + assert finding.status == "FAIL" + assert "Bedrock control plane" in finding.status_extended + assert "Bedrock runtime" in finding.status_extended + assert "Bedrock agent control plane" in finding.status_extended + assert "Bedrock agent runtime" in finding.status_extended + assert "Bedrock Mantle" in finding.status_extended + + @mock_aws + def test_vpc_only_bedrock_mantle_endpoint(self): + """Test VPC with only Bedrock Mantle endpoint - should FAIL with four services missing.""" + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + + vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"] + + route_table = ec2_client.create_route_table(VpcId=vpc["VpcId"])["RouteTable"] + ec2_client.create_vpc_endpoint( + VpcId=vpc["VpcId"], + ServiceName="com.amazonaws.us-east-1.bedrock-mantle", + RouteTableIds=[route_table["RouteTableId"]], + VpcEndpointType="Interface", + ) + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + 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( + f"{CHECK_MODULE}.vpc_client", + new=VPC(aws_provider), + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_client", + new=MOCK_BEDROCK_CLIENT, + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_agent_client", + new=MOCK_BEDROCK_AGENT_CLIENT, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_vpc_endpoints_configured.bedrock_vpc_endpoints_configured import ( + bedrock_vpc_endpoints_configured, + ) + + check = bedrock_vpc_endpoints_configured() + result = check.execute() + + assert len(result) == 2 + finding = next(f for f in result if f.resource_id == vpc["VpcId"]) + assert finding.status == "FAIL" + assert "Bedrock Mantle" not in finding.status_extended + assert "Bedrock control plane" in finding.status_extended + assert "Bedrock runtime" in finding.status_extended + assert "Bedrock agent control plane" in finding.status_extended + assert "Bedrock agent runtime" in finding.status_extended + + @mock_aws + def test_bedrock_activity_via_guardrail(self): + """Test that Bedrock activity is detected via guardrails.""" + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + vpc_id = ec2_client.describe_vpcs()["Vpcs"][0]["VpcId"] + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + bedrock_with_guardrail = mock.MagicMock( + logging_configurations={ + AWS_REGION_US_EAST_1: LoggingConfiguration(enabled=False) + }, + guardrails={ + "arn:aws:bedrock:us-east-1:123456789012:guardrail/test": Guardrail( + id="test", + name="test-guardrail", + arn="arn:aws:bedrock:us-east-1:123456789012:guardrail/test", + region=AWS_REGION_US_EAST_1, + ) + }, + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with ( + mock.patch( + f"{CHECK_MODULE}.vpc_client", + new=VPC(aws_provider), + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_client", + new=bedrock_with_guardrail, + ), + mock.patch( + f"{CHECK_MODULE}.bedrock_agent_client", + new=MOCK_BEDROCK_AGENT_CLIENT_NO_ACTIVITY, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_vpc_endpoints_configured.bedrock_vpc_endpoints_configured import ( + bedrock_vpc_endpoints_configured, + ) + + check = bedrock_vpc_endpoints_configured() + result = check.execute() + + # Region has Bedrock activity via guardrail, so VPC should be checked + assert len(result) == 1 + assert result[0].resource_id == vpc_id + assert result[0].status == "FAIL" diff --git a/tests/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe_test.py b/tests/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe_test.py new file mode 100644 index 0000000000..8f9c57a1e1 --- /dev/null +++ b/tests/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe_test.py @@ -0,0 +1,461 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from prowler.providers.aws.services.iam.iam_service import IAM +from tests.providers.aws.utils import ( + ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + +CHECK_MODULE_PATH = "prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe" + + +class Test_iam_inline_policy_no_wildcard_marketplace_subscribe: + @mock_aws + def test_inline_policy_allows_marketplace_subscribe_on_all_resources(self): + """FAIL: Inline policy allows aws-marketplace:Subscribe on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "marketplace_subscribe_wildcard" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Inline policy {policy_name} attached to role {role_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == f"{role_name}/{policy_name}" + assert result[0].resource_arn == role_arn + assert result[0].region == "eu-west-1" + + @mock_aws + def test_inline_policy_allows_marketplace_wildcard_action(self): + """FAIL: Inline policy allows aws-marketplace:* on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "marketplace_all_actions" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:*", + "Resource": "*", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Inline policy {policy_name} attached to role {role_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == f"{role_name}/{policy_name}" + assert result[0].resource_arn == role_arn + assert result[0].region == "eu-west-1" + + @mock_aws + def test_inline_policy_scoped_resource(self): + """PASS: Inline policy allows aws-marketplace:Subscribe on a specific resource.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "marketplace_subscribe_scoped" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "arn:aws:aws-marketplace::123456789012:product/example-product-id", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Inline policy {policy_name} attached to role {role_name} does not allow 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == f"{role_name}/{policy_name}" + assert result[0].resource_arn == role_arn + assert result[0].region == "eu-west-1" + + @mock_aws + def test_inline_policy_unrelated_action(self): + """PASS: Inline policy allows an unrelated action on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "ec2_full_access" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ec2:*", + "Resource": "*", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Inline policy {policy_name} attached to role {role_name} does not allow 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == f"{role_name}/{policy_name}" + assert result[0].resource_arn == role_arn + assert result[0].region == "eu-west-1" + + @mock_aws + def test_inline_policy_deny_overrides_allow(self): + """PASS: Allow + Deny on * for same action — Deny wins.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "marketplace_subscribe_denied" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Inline policy {policy_name} attached to role {role_name} does not allow 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == f"{role_name}/{policy_name}" + assert result[0].resource_arn == role_arn + assert result[0].region == "eu-west-1" + + @mock_aws + def test_inline_policy_deny_specific_resource_does_not_override(self): + """FAIL: Deny on specific resource does not negate Allow on *.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "deny_specific_only" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "aws-marketplace:Subscribe", + "Resource": "arn:aws:aws-marketplace::123456789012:product/blocked", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Inline policy {policy_name} attached to role {role_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == f"{role_name}/{policy_name}" + assert result[0].resource_arn == role_arn + assert result[0].region == "eu-west-1" + + @mock_aws + def test_inline_policy_not_action_allows_marketplace_subscribe(self): + """FAIL: Allow + NotAction excludes only unrelated actions so Subscribe is granted.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "marketplace_not_action" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "NotAction": "ec2:Describe*", + "Resource": "*", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert result[0].resource_arn == role_arn + + @mock_aws + def test_inline_policy_not_action_excludes_marketplace_subscribe(self): + """PASS: Allow + NotAction that excludes aws-marketplace:Subscribe does not grant it.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "marketplace_not_action_excluded" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "NotAction": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "PASS" + assert result[0].resource_arn == role_arn + + @mock_aws + def test_no_inline_policies(self): + """No findings when there are no inline policies.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert len(result) == 0 diff --git a/tests/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe_test.py b/tests/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe_test.py new file mode 100644 index 0000000000..b667e7dbfb --- /dev/null +++ b/tests/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe_test.py @@ -0,0 +1,509 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from prowler.providers.aws.services.iam.iam_service import IAM +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + +CHECK_MODULE_PATH = "prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe" + + +class Test_iam_policy_no_wildcard_marketplace_subscribe: + @mock_aws + def test_policy_allows_marketplace_subscribe_on_all_resources(self): + """FAIL: Policy explicitly allows aws-marketplace:Subscribe on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "marketplace_subscribe_wildcard" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_allows_marketplace_wildcard_action_on_all_resources(self): + """FAIL: Policy allows aws-marketplace:* on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "marketplace_all_actions" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:*", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_allows_full_wildcard_on_all_resources(self): + """FAIL: Policy allows * (all actions) on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "full_admin_access" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "*", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_allows_marketplace_subscribe_on_specific_resource(self): + """PASS: Policy allows aws-marketplace:Subscribe on a specific resource ARN.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "marketplace_subscribe_scoped" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "arn:aws:aws-marketplace::123456789012:product/example-product-id", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} does not allow 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_unrelated_action_on_all_resources(self): + """PASS: Policy allows an unrelated action on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "ec2_full_access" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ec2:*", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} does not allow 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_marketplace_subscribe_denied_on_all_resources(self): + """PASS: Allow + Deny on * for same action — Deny wins.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "marketplace_subscribe_denied" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} does not allow 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_deny_specific_resource_does_not_override_allow_all(self): + """FAIL: Deny on a specific resource does not negate Allow on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "deny_specific_resource" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "aws-marketplace:Subscribe", + "Resource": "arn:aws:aws-marketplace::123456789012:product/blocked-product", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_deny_unrelated_action_does_not_override_allow(self): + """FAIL: Deny for a different action does not override the Allow for Subscribe.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "deny_unrelated_action" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "ec2:TerminateInstances", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_case_insensitive_action_matching(self): + """FAIL: Mixed-case action still matches aws-marketplace:Subscribe.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "mixed_case_subscribe" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "AWS-Marketplace:SUBSCRIBE", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_not_action_allows_marketplace_subscribe(self): + """FAIL: Allow + NotAction excludes only unrelated actions so Subscribe is granted.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "marketplace_not_action" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "NotAction": "ec2:Describe*", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert result[0].resource_arn == arn + + @mock_aws + def test_policy_not_action_excludes_marketplace_subscribe(self): + """PASS: Allow + NotAction that excludes aws-marketplace:Subscribe does not grant it.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "marketplace_not_action_excluded" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "NotAction": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "PASS" + assert result[0].resource_arn == arn + + @mock_aws + def test_no_custom_policies(self): + """No findings when there are no custom IAM policies.""" + 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( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import ( + iam_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert len(result) == 0 diff --git a/tests/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/__init__.py b/tests/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock_test.py b/tests/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock_test.py new file mode 100644 index 0000000000..ecff542b28 --- /dev/null +++ b/tests/providers/aws/services/iam/iam_role_access_not_stale_to_bedrock/iam_role_access_not_stale_to_bedrock_test.py @@ -0,0 +1,240 @@ +from datetime import datetime, timedelta, timezone +from unittest import mock + +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +IAM_ROLE_NAME = "test-role" +IAM_ROLE_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/{IAM_ROLE_NAME}" +ROLE_DATA = (IAM_ROLE_NAME, IAM_ROLE_ARN) + +CHECK_MODULE = ( + "prowler.providers.aws.services.iam." + "iam_role_access_not_stale_to_bedrock.iam_role_access_not_stale_to_bedrock" +) + + +class Test_iam_role_access_not_stale_to_bedrock: + @mock_aws + def test_no_roles_with_bedrock_permissions(self): + """No findings when no roles have Bedrock in last accessed services.""" + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + iam.role_last_accessed_services = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_role_access_not_stale_to_bedrock.iam_role_access_not_stale_to_bedrock import ( + iam_role_access_not_stale_to_bedrock, + ) + + check = iam_role_access_not_stale_to_bedrock() + assert len(check.execute()) == 0 + + @mock_aws + def test_role_bedrock_access_stale(self): + """FAIL when a role last accessed Bedrock more than 60 days ago.""" + from prowler.providers.aws.services.iam.iam_service import IAM, Role + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_role = Role( + name=IAM_ROLE_NAME, + arn=IAM_ROLE_ARN, + assume_role_policy={}, + is_service_role=False, + attached_policies=[], + inline_policies=[], + ) + iam.roles = [mock_role] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=120) + iam.role_last_accessed_services = { + ROLE_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_role_access_not_stale_to_bedrock.iam_role_access_not_stale_to_bedrock import ( + iam_role_access_not_stale_to_bedrock, + ) + + check = iam_role_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "has not accessed Bedrock" in result[0].status_extended + assert "Role" in result[0].status_extended + assert IAM_ROLE_NAME in result[0].status_extended + assert result[0].resource_id == IAM_ROLE_NAME + assert result[0].resource_arn == IAM_ROLE_ARN + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_role_bedrock_access_recent(self): + """PASS when a role accessed Bedrock recently.""" + from prowler.providers.aws.services.iam.iam_service import IAM, Role + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_role = Role( + name=IAM_ROLE_NAME, + arn=IAM_ROLE_ARN, + assume_role_policy={}, + is_service_role=False, + attached_policies=[], + inline_policies=[], + ) + iam.roles = [mock_role] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=5) + iam.role_last_accessed_services = { + ROLE_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_role_access_not_stale_to_bedrock.iam_role_access_not_stale_to_bedrock import ( + iam_role_access_not_stale_to_bedrock, + ) + + check = iam_role_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "accessed Bedrock" in result[0].status_extended + assert "Role" in result[0].status_extended + assert IAM_ROLE_NAME in result[0].status_extended + + @mock_aws + def test_role_bedrock_never_accessed(self): + """FAIL when a role has Bedrock permissions but never accessed them.""" + from prowler.providers.aws.services.iam.iam_service import IAM, Role + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_role = Role( + name=IAM_ROLE_NAME, + arn=IAM_ROLE_ARN, + assume_role_policy={}, + is_service_role=False, + attached_policies=[], + inline_policies=[], + ) + iam.roles = [mock_role] + + iam.role_last_accessed_services = { + ROLE_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_role_access_not_stale_to_bedrock.iam_role_access_not_stale_to_bedrock import ( + iam_role_access_not_stale_to_bedrock, + ) + + check = iam_role_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "has never used them" in result[0].status_extended + assert "Role" in result[0].status_extended + + @mock_aws + def test_role_tags_are_populated(self): + """Verify resource_tags are populated from the role object.""" + from prowler.providers.aws.services.iam.iam_service import IAM, Role + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + role_tags = [{"Key": "Team", "Value": "ml-platform"}] + mock_role = Role( + name=IAM_ROLE_NAME, + arn=IAM_ROLE_ARN, + assume_role_policy={}, + is_service_role=False, + attached_policies=[], + inline_policies=[], + tags=role_tags, + ) + iam.roles = [mock_role] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=5) + iam.role_last_accessed_services = { + ROLE_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_role_access_not_stale_to_bedrock.iam_role_access_not_stale_to_bedrock import ( + iam_role_access_not_stale_to_bedrock, + ) + + check = iam_role_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_tags == role_tags diff --git a/tests/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/__init__.py b/tests/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock_test.py b/tests/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock_test.py new file mode 100644 index 0000000000..c05b9362c9 --- /dev/null +++ b/tests/providers/aws/services/iam/iam_user_access_not_stale_to_bedrock/iam_user_access_not_stale_to_bedrock_test.py @@ -0,0 +1,449 @@ +from datetime import datetime, timedelta, timezone +from unittest import mock + +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +IAM_USER_NAME = "test-user" +IAM_USER_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/{IAM_USER_NAME}" +USER_DATA = (IAM_USER_NAME, IAM_USER_ARN) + +CHECK_MODULE = ( + "prowler.providers.aws.services.iam." + "iam_user_access_not_stale_to_bedrock.iam_user_access_not_stale_to_bedrock" +) + + +class Test_iam_user_access_not_stale_to_bedrock: + @mock_aws + def test_no_users_with_bedrock_permissions(self): + """No findings when no users have Bedrock in last accessed services.""" + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + iam.last_accessed_services = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_bedrock.iam_user_access_not_stale_to_bedrock import ( + iam_user_access_not_stale_to_bedrock, + ) + + check = iam_user_access_not_stale_to_bedrock() + assert len(check.execute()) == 0 + + @mock_aws + def test_user_without_bedrock_permissions(self): + """User with non-Bedrock services is skipped.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + iam.last_accessed_services = { + USER_DATA: [ + {"ServiceNamespace": "iam", "ServiceName": "IAM"}, + {"ServiceNamespace": "s3", "ServiceName": "S3"}, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_bedrock.iam_user_access_not_stale_to_bedrock import ( + iam_user_access_not_stale_to_bedrock, + ) + + check = iam_user_access_not_stale_to_bedrock() + assert len(check.execute()) == 0 + + @mock_aws + def test_user_bedrock_access_recent(self): + """PASS when user accessed Bedrock within the threshold.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=10) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_bedrock.iam_user_access_not_stale_to_bedrock import ( + iam_user_access_not_stale_to_bedrock, + ) + + check = iam_user_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "accessed Bedrock" in result[0].status_extended + assert IAM_USER_NAME in result[0].status_extended + assert result[0].resource_id == IAM_USER_NAME + assert result[0].resource_arn == IAM_USER_ARN + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_user_bedrock_access_stale(self): + """FAIL when user last accessed Bedrock more than 60 days ago.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=90) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_bedrock.iam_user_access_not_stale_to_bedrock import ( + iam_user_access_not_stale_to_bedrock, + ) + + check = iam_user_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "has not accessed Bedrock" in result[0].status_extended + assert "90 days" in result[0].status_extended + assert IAM_USER_NAME in result[0].status_extended + + @mock_aws + def test_user_bedrock_never_accessed(self): + """FAIL when user has Bedrock permissions but has never used them.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_bedrock.iam_user_access_not_stale_to_bedrock import ( + iam_user_access_not_stale_to_bedrock, + ) + + check = iam_user_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "has never used them" in result[0].status_extended + + @mock_aws + def test_custom_threshold_via_audit_config(self): + """Custom threshold from audit_config is respected.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + iam.audit_config = {"max_unused_bedrock_access_days": 30} + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=45) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_bedrock.iam_user_access_not_stale_to_bedrock import ( + iam_user_access_not_stale_to_bedrock, + ) + + check = iam_user_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "45 days" in result[0].status_extended + assert "threshold: 30 days" in result[0].status_extended + + @mock_aws + def test_user_bedrock_access_at_exact_threshold(self): + """PASS when user accessed Bedrock exactly at the 60-day boundary.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=60) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_bedrock.iam_user_access_not_stale_to_bedrock import ( + iam_user_access_not_stale_to_bedrock, + ) + + check = iam_user_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "60 days ago" in result[0].status_extended + assert "threshold: 60 days" in result[0].status_extended + + @mock_aws + def test_user_bedrock_access_one_day_over_threshold(self): + """FAIL when user accessed Bedrock 61 days ago.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=61) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_bedrock.iam_user_access_not_stale_to_bedrock import ( + iam_user_access_not_stale_to_bedrock, + ) + + check = iam_user_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "61 days" in result[0].status_extended + assert "threshold: 60 days" in result[0].status_extended + + @mock_aws + def test_user_bedrock_access_with_string_date(self): + """PASS when LastAuthenticated is an ISO string instead of a datetime object.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + ) + iam.users = [mock_user] + + last_access_date = datetime.now(timezone.utc) - timedelta(days=5) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_access_date.isoformat(), + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_bedrock.iam_user_access_not_stale_to_bedrock import ( + iam_user_access_not_stale_to_bedrock, + ) + + check = iam_user_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + + @mock_aws + def test_user_tags_are_populated(self): + """Verify resource_tags are populated from the user object.""" + from prowler.providers.aws.services.iam.iam_service import IAM, User + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + user_tags = [{"Key": "Environment", "Value": "production"}] + mock_user = User( + name=IAM_USER_NAME, + arn=IAM_USER_ARN, + attached_policies=[], + inline_policies=[], + tags=user_tags, + ) + iam.users = [mock_user] + + last_authenticated = datetime.now(timezone.utc) - timedelta(days=10) + iam.last_accessed_services = { + USER_DATA: [ + { + "ServiceNamespace": "bedrock", + "ServiceName": "Amazon Bedrock", + "LastAuthenticated": last_authenticated, + }, + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch(f"{CHECK_MODULE}.iam_client", new=iam), + ): + from prowler.providers.aws.services.iam.iam_user_access_not_stale_to_bedrock.iam_user_access_not_stale_to_bedrock import ( + iam_user_access_not_stale_to_bedrock, + ) + + check = iam_user_access_not_stale_to_bedrock() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_tags == user_tags diff --git a/tests/providers/aws/services/iam/lib/policy_test.py b/tests/providers/aws/services/iam/lib/policy_test.py index 50cca4cbad..bf64ff9211 100644 --- a/tests/providers/aws/services/iam/lib/policy_test.py +++ b/tests/providers/aws/services/iam/lib/policy_test.py @@ -15,6 +15,7 @@ from prowler.providers.aws.services.iam.lib.policy import ( is_condition_restricting_from_private_ip, is_condition_restricting_to_trusted_ips, is_policy_public, + policy_allows_marketplace_subscribe_on_all_resources, ) TRUSTED_AWS_ACCOUNT_NUMBER = "123456789012" @@ -137,6 +138,47 @@ class Test_Policy: assert result == {"s3:GetObject", "s3:ListBucket"} assert "s3:PutObject" not in result + def test_policy_allows_marketplace_subscribe_conditional_deny_does_not_cancel(self): + """Conditional deny should not globally cancel a wildcard marketplace allow.""" + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + "Condition": {"StringEquals": {"aws:RequestedRegion": "us-east-1"}}, + }, + ], + } + + assert policy_allows_marketplace_subscribe_on_all_resources(policy) + + def test_policy_allows_marketplace_subscribe_unconditional_deny_cancels(self): + """Unconditional deny on Resource:* should cancel the wildcard marketplace allow.""" + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + + assert not policy_allows_marketplace_subscribe_on_all_resources(policy) + # Test lowercase context key name --> aws def test_condition_parser_string_equals_aws_SourceAccount_list(self): condition_statement = { diff --git a/tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/__init__.py b/tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition_test.py b/tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition_test.py new file mode 100644 index 0000000000..628525e542 --- /dev/null +++ b/tests/providers/aws/services/stepfunctions/stepfunctions_statemachine_no_secrets_in_definition/stepfunctions_statemachine_no_secrets_in_definition_test.py @@ -0,0 +1,180 @@ +from datetime import datetime +from unittest import mock + +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 + + +class Test_stepfunctions_statemachine_no_secrets_in_definition: + def test_no_statemachines(self): + stepfunctions_client = mock.MagicMock() + stepfunctions_client.state_machines = {} + stepfunctions_client.audit_config = {"secrets_ignore_patterns": []} + + with ( + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_service.StepFunctions", + stepfunctions_client, + ), + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_client", + stepfunctions_client, + ), + ): + from prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition import ( + stepfunctions_statemachine_no_secrets_in_definition, + ) + + check = stepfunctions_statemachine_no_secrets_in_definition() + result = check.execute() + + assert len(result) == 0 + + def test_statemachine_with_no_definition(self): + stepfunctions_client = mock.MagicMock() + + from prowler.providers.aws.services.stepfunctions.stepfunctions_service import ( + StateMachine, + StateMachineStatus, + StateMachineType, + ) + + statemachine_arn = f"arn:aws:states:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:stateMachine:TestStateMachine" + stepfunctions_client.state_machines = { + statemachine_arn: StateMachine( + id="TestStateMachine", + arn=statemachine_arn, + name="TestStateMachine", + status=StateMachineStatus.ACTIVE, + definition=None, + region=AWS_REGION_US_EAST_1, + type=StateMachineType.STANDARD, + creation_date=datetime.now(), + ) + } + stepfunctions_client.audit_config = {"secrets_ignore_patterns": []} + + with ( + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_service.StepFunctions", + stepfunctions_client, + ), + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_client", + stepfunctions_client, + ), + ): + from prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition import ( + stepfunctions_statemachine_no_secrets_in_definition, + ) + + check = stepfunctions_statemachine_no_secrets_in_definition() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "No secrets found in Step Functions state machine TestStateMachine definition." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == "TestStateMachine" + assert result[0].resource_arn == statemachine_arn + + def test_statemachine_with_no_secrets_in_definition(self): + stepfunctions_client = mock.MagicMock() + + from prowler.providers.aws.services.stepfunctions.stepfunctions_service import ( + StateMachine, + StateMachineStatus, + StateMachineType, + ) + + statemachine_arn = f"arn:aws:states:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:stateMachine:TestStateMachine" + stepfunctions_client.state_machines = { + statemachine_arn: StateMachine( + id="TestStateMachine", + arn=statemachine_arn, + name="TestStateMachine", + status=StateMachineStatus.ACTIVE, + definition='{"Comment": "A simple example", "StartAt": "HelloWorld", "States": {"HelloWorld": {"Type": "Pass", "End": true}}}', + region=AWS_REGION_US_EAST_1, + type=StateMachineType.STANDARD, + creation_date=datetime.now(), + ) + } + stepfunctions_client.audit_config = {"secrets_ignore_patterns": []} + + with ( + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_service.StepFunctions", + stepfunctions_client, + ), + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_client", + stepfunctions_client, + ), + ): + from prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition import ( + stepfunctions_statemachine_no_secrets_in_definition, + ) + + check = stepfunctions_statemachine_no_secrets_in_definition() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "No secrets found in Step Functions state machine TestStateMachine definition." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == "TestStateMachine" + assert result[0].resource_arn == statemachine_arn + + def test_statemachine_with_secrets_in_definition(self): + stepfunctions_client = mock.MagicMock() + + from prowler.providers.aws.services.stepfunctions.stepfunctions_service import ( + StateMachine, + StateMachineStatus, + StateMachineType, + ) + + statemachine_arn = f"arn:aws:states:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:stateMachine:TestStateMachine" + stepfunctions_client.state_machines = { + statemachine_arn: StateMachine( + id="TestStateMachine", + arn=statemachine_arn, + name="TestStateMachine", + status=StateMachineStatus.ACTIVE, + definition='{"Comment": "Example with secret", "StartAt": "MyTask", "States": {"MyTask": {"Type": "Task", "Parameters": {"api_key": "AKIAIOSFODNN7EXAMPLE"}, "End": true}}}', + region=AWS_REGION_US_EAST_1, + type=StateMachineType.STANDARD, + creation_date=datetime.now(), + ) + } + stepfunctions_client.audit_config = {"secrets_ignore_patterns": []} + + with ( + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_service.StepFunctions", + stepfunctions_client, + ), + mock.patch( + "prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_client", + stepfunctions_client, + ), + ): + from prowler.providers.aws.services.stepfunctions.stepfunctions_statemachine_no_secrets_in_definition.stepfunctions_statemachine_no_secrets_in_definition import ( + stepfunctions_statemachine_no_secrets_in_definition, + ) + + check = stepfunctions_statemachine_no_secrets_in_definition() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "TestStateMachine" in result[0].status_extended + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == "TestStateMachine" + assert result[0].resource_arn == statemachine_arn diff --git a/tests/providers/googleworkspace/lib/service/googleworkspace_service_test.py b/tests/providers/googleworkspace/lib/service/googleworkspace_service_test.py new file mode 100644 index 0000000000..f76b848ffe --- /dev/null +++ b/tests/providers/googleworkspace/lib/service/googleworkspace_service_test.py @@ -0,0 +1,46 @@ +from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService + + +class TestIsCustomerLevelPolicy: + def test_no_policy_query(self): + """Policy without policyQuery is customer-level""" + assert GoogleWorkspaceService._is_customer_level_policy({}) is True + + def test_empty_policy_query(self): + """Policy with empty policyQuery is customer-level""" + assert ( + GoogleWorkspaceService._is_customer_level_policy({"policyQuery": {}}) + is True + ) + + def test_org_unit_targeted(self): + """Policy targeting a specific OU is not customer-level""" + assert ( + GoogleWorkspaceService._is_customer_level_policy( + {"policyQuery": {"orgUnit": "orgUnits/abc123"}} + ) + is False + ) + + def test_group_targeted(self): + """Policy targeting a specific group is not customer-level""" + assert ( + GoogleWorkspaceService._is_customer_level_policy( + {"policyQuery": {"group": "groups/xyz789"}} + ) + is False + ) + + def test_org_unit_and_group_targeted(self): + """Policy targeting both OU and group is not customer-level""" + assert ( + GoogleWorkspaceService._is_customer_level_policy( + { + "policyQuery": { + "orgUnit": "orgUnits/abc123", + "group": "groups/xyz789", + } + } + ) + is False + ) diff --git a/tests/providers/googleworkspace/services/calendar/calendar_service_test.py b/tests/providers/googleworkspace/services/calendar/calendar_service_test.py index 1491f839fb..760624e0ae 100644 --- a/tests/providers/googleworkspace/services/calendar/calendar_service_test.py +++ b/tests/providers/googleworkspace/services/calendar/calendar_service_test.py @@ -211,6 +211,86 @@ class TestCalendarService: assert calendar.policies.secondary_calendar_external_sharing is None assert calendar.policies.external_invitations_warning is None + def test_calendar_fetch_policies_ignores_ou_and_group_level(self): + """Test that OU-level and group-level policies are skipped, only customer-level used""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + # Response includes policies at different org levels: + # customer-level (no policyQuery), OU-level, and group-level + mock_policies_list.execute.return_value = { + "policies": [ + { + # Customer-level: no policyQuery → should be used + "setting": { + "type": "settings/calendar.primary_calendar_max_allowed_external_sharing", + "value": { + "maxAllowedExternalSharing": "EXTERNAL_FREE_BUSY_ONLY" + }, + } + }, + { + # OU-level: has policyQuery.orgUnit → should be skipped + "policyQuery": {"orgUnit": "orgUnits/sales_team"}, + "setting": { + "type": "settings/calendar.primary_calendar_max_allowed_external_sharing", + "value": { + "maxAllowedExternalSharing": "EXTERNAL_ALL_INFO_READ_WRITE" + }, + }, + }, + { + # Group-level: has policyQuery.group → should be skipped + "policyQuery": {"group": "groups/contractors"}, + "setting": { + "type": "settings/calendar.external_invitations", + "value": {"warnOnInvite": False}, + }, + }, + { + # Customer-level: no policyQuery → should be used + "setting": { + "type": "settings/calendar.external_invitations", + "value": {"warnOnInvite": True}, + } + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.calendar.calendar_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.calendar.calendar_service import ( + Calendar, + ) + + calendar = Calendar(mock_provider) + + assert calendar.policies_fetched is True + # Customer-level values should be stored + assert ( + calendar.policies.primary_calendar_external_sharing + == "EXTERNAL_FREE_BUSY_ONLY" + ) + assert calendar.policies.external_invitations_warning is True + # OU-level value (EXTERNAL_ALL_INFO_READ_WRITE) should NOT have overwritten + # Group-level value (warnOnInvite: False) should NOT have overwritten + def test_calendar_policies_model(self): """Test CalendarPolicies Pydantic model""" from prowler.providers.googleworkspace.services.calendar.calendar_service import ( diff --git a/tests/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only_test.py b/tests/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only_test.py new file mode 100644 index 0000000000..b439429e20 --- /dev/null +++ b/tests/providers/googleworkspace/services/drive/drive_access_checker_recipients_only/drive_access_checker_recipients_only_test.py @@ -0,0 +1,156 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + DOMAIN, + set_mocked_googleworkspace_provider, +) + + +class TestDriveAccessCheckerRecipientsOnly: + def test_pass_recipients_only(self): + """Test PASS when Access Checker is set to recipients only""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_access_checker_recipients_only.drive_access_checker_recipients_only.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_access_checker_recipients_only.drive_access_checker_recipients_only import ( + drive_access_checker_recipients_only, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + access_checker_suggestions="RECIPIENTS_ONLY" + ) + + check = drive_access_checker_recipients_only() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "recipients only" in findings[0].status_extended + assert findings[0].resource_name == DOMAIN + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_recipients_or_audience(self): + """Test FAIL when Access Checker allows audience suggestions""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_access_checker_recipients_only.drive_access_checker_recipients_only.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_access_checker_recipients_only.drive_access_checker_recipients_only import ( + drive_access_checker_recipients_only, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + access_checker_suggestions="RECIPIENTS_OR_AUDIENCE" + ) + + check = drive_access_checker_recipients_only() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "RECIPIENTS_OR_AUDIENCE" in findings[0].status_extended + + def test_fail_recipients_or_audience_or_public(self): + """Test FAIL when Access Checker allows public suggestions""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_access_checker_recipients_only.drive_access_checker_recipients_only.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_access_checker_recipients_only.drive_access_checker_recipients_only import ( + drive_access_checker_recipients_only, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + access_checker_suggestions="RECIPIENTS_OR_AUDIENCE_OR_PUBLIC" + ) + + check = drive_access_checker_recipients_only() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "RECIPIENTS_OR_AUDIENCE_OR_PUBLIC" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) but fetch succeeded""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_access_checker_recipients_only.drive_access_checker_recipients_only.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_access_checker_recipients_only.drive_access_checker_recipients_only import ( + drive_access_checker_recipients_only, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(access_checker_suggestions=None) + + check = drive_access_checker_recipients_only() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_access_checker_recipients_only.drive_access_checker_recipients_only.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_access_checker_recipients_only.drive_access_checker_recipients_only import ( + drive_access_checker_recipients_only, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = False + mock_drive_client.policies = DrivePolicies() + + check = drive_access_checker_recipients_only() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled_test.py b/tests/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled_test.py new file mode 100644 index 0000000000..00ed8dfa26 --- /dev/null +++ b/tests/providers/googleworkspace/services/drive/drive_desktop_access_disabled/drive_desktop_access_disabled_test.py @@ -0,0 +1,122 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + DOMAIN, + set_mocked_googleworkspace_provider, +) + + +class TestDriveDesktopAccessDisabled: + def test_pass_desktop_disabled(self): + """Test PASS when Drive for desktop is disabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_desktop_access_disabled.drive_desktop_access_disabled.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_desktop_access_disabled.drive_desktop_access_disabled import ( + drive_desktop_access_disabled, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(allow_drive_for_desktop=False) + + check = drive_desktop_access_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == DOMAIN + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_desktop_enabled(self): + """Test FAIL when Drive for desktop is enabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_desktop_access_disabled.drive_desktop_access_disabled.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_desktop_access_disabled.drive_desktop_access_disabled import ( + drive_desktop_access_disabled, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(allow_drive_for_desktop=True) + + check = drive_desktop_access_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enabled" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) but fetch succeeded""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_desktop_access_disabled.drive_desktop_access_disabled.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_desktop_access_disabled.drive_desktop_access_disabled import ( + drive_desktop_access_disabled, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(allow_drive_for_desktop=None) + + check = drive_desktop_access_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_desktop_access_disabled.drive_desktop_access_disabled.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_desktop_access_disabled.drive_desktop_access_disabled import ( + drive_desktop_access_disabled, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = False + mock_drive_client.policies = DrivePolicies() + + check = drive_desktop_access_disabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py b/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py new file mode 100644 index 0000000000..c9c7317f2b --- /dev/null +++ b/tests/providers/googleworkspace/services/drive/drive_external_sharing_warn_users/drive_external_sharing_warn_users_test.py @@ -0,0 +1,122 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + DOMAIN, + set_mocked_googleworkspace_provider, +) + + +class TestDriveExternalSharingWarnUsers: + def test_pass_warning_enabled(self): + """Test PASS when external sharing warning is enabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_external_sharing_warn_users.drive_external_sharing_warn_users.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_external_sharing_warn_users.drive_external_sharing_warn_users import ( + drive_external_sharing_warn_users, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(warn_for_external_sharing=True) + + check = drive_external_sharing_warn_users() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "enabled" in findings[0].status_extended + assert findings[0].resource_name == DOMAIN + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_warning_disabled(self): + """Test FAIL when external sharing warning is explicitly disabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_external_sharing_warn_users.drive_external_sharing_warn_users.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_external_sharing_warn_users.drive_external_sharing_warn_users import ( + drive_external_sharing_warn_users, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(warn_for_external_sharing=False) + + check = drive_external_sharing_warn_users() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "disabled" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) but fetch succeeded""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_external_sharing_warn_users.drive_external_sharing_warn_users.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_external_sharing_warn_users.drive_external_sharing_warn_users import ( + drive_external_sharing_warn_users, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(warn_for_external_sharing=None) + + check = drive_external_sharing_warn_users() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_external_sharing_warn_users.drive_external_sharing_warn_users.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_external_sharing_warn_users.drive_external_sharing_warn_users import ( + drive_external_sharing_warn_users, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = False + mock_drive_client.policies = DrivePolicies() + + check = drive_external_sharing_warn_users() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content_test.py b/tests/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content_test.py new file mode 100644 index 0000000000..5ac3067102 --- /dev/null +++ b/tests/providers/googleworkspace/services/drive/drive_internal_users_distribute_content/drive_internal_users_distribute_content_test.py @@ -0,0 +1,158 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + DOMAIN, + set_mocked_googleworkspace_provider, +) + + +class TestDriveInternalUsersDistributeContent: + def test_pass_eligible_internal_users(self): + """Test PASS when content distribution is restricted to eligible internal users""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_internal_users_distribute_content.drive_internal_users_distribute_content.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_internal_users_distribute_content.drive_internal_users_distribute_content import ( + drive_internal_users_distribute_content, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + allowed_parties_for_distributing_content="ELIGIBLE_INTERNAL_USERS" + ) + + check = drive_internal_users_distribute_content() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "ELIGIBLE_INTERNAL_USERS" in findings[0].status_extended + assert findings[0].resource_name == DOMAIN + assert findings[0].customer_id == CUSTOMER_ID + + def test_pass_none_allowed(self): + """Test PASS when content distribution is set to NONE (nobody can distribute)""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_internal_users_distribute_content.drive_internal_users_distribute_content.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_internal_users_distribute_content.drive_internal_users_distribute_content import ( + drive_internal_users_distribute_content, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + allowed_parties_for_distributing_content="NONE" + ) + + check = drive_internal_users_distribute_content() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "NONE" in findings[0].status_extended + + def test_fail_all_eligible_users(self): + """Test FAIL when all users (including external) can distribute content""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_internal_users_distribute_content.drive_internal_users_distribute_content.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_internal_users_distribute_content.drive_internal_users_distribute_content import ( + drive_internal_users_distribute_content, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + allowed_parties_for_distributing_content="ALL_ELIGIBLE_USERS" + ) + + check = drive_internal_users_distribute_content() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "ALL_ELIGIBLE_USERS" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) but fetch succeeded""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_internal_users_distribute_content.drive_internal_users_distribute_content.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_internal_users_distribute_content.drive_internal_users_distribute_content import ( + drive_internal_users_distribute_content, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + allowed_parties_for_distributing_content=None + ) + + check = drive_internal_users_distribute_content() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_internal_users_distribute_content.drive_internal_users_distribute_content.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_internal_users_distribute_content.drive_internal_users_distribute_content import ( + drive_internal_users_distribute_content, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = False + mock_drive_client.policies = DrivePolicies() + + check = drive_internal_users_distribute_content() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled_test.py b/tests/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled_test.py new file mode 100644 index 0000000000..cbcef4f900 --- /dev/null +++ b/tests/providers/googleworkspace/services/drive/drive_publishing_files_disabled/drive_publishing_files_disabled_test.py @@ -0,0 +1,122 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + DOMAIN, + set_mocked_googleworkspace_provider, +) + + +class TestDrivePublishingFilesDisabled: + def test_pass_publishing_disabled(self): + """Test PASS when publishing files to web is disabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_publishing_files_disabled.drive_publishing_files_disabled.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_publishing_files_disabled.drive_publishing_files_disabled import ( + drive_publishing_files_disabled, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(allow_publishing_files=False) + + check = drive_publishing_files_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "disabled" in findings[0].status_extended + assert findings[0].resource_name == DOMAIN + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_publishing_enabled(self): + """Test FAIL when publishing files to web is enabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_publishing_files_disabled.drive_publishing_files_disabled.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_publishing_files_disabled.drive_publishing_files_disabled import ( + drive_publishing_files_disabled, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(allow_publishing_files=True) + + check = drive_publishing_files_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "enabled" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) but fetch succeeded""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_publishing_files_disabled.drive_publishing_files_disabled.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_publishing_files_disabled.drive_publishing_files_disabled import ( + drive_publishing_files_disabled, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(allow_publishing_files=None) + + check = drive_publishing_files_disabled() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_publishing_files_disabled.drive_publishing_files_disabled.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_publishing_files_disabled.drive_publishing_files_disabled import ( + drive_publishing_files_disabled, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = False + mock_drive_client.policies = DrivePolicies() + + check = drive_publishing_files_disabled() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/drive/drive_service_test.py b/tests/providers/googleworkspace/services/drive/drive_service_test.py new file mode 100644 index 0000000000..cbdd56679c --- /dev/null +++ b/tests/providers/googleworkspace/services/drive/drive_service_test.py @@ -0,0 +1,349 @@ +from unittest.mock import MagicMock, patch + +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + set_mocked_googleworkspace_provider, +) + + +class TestDriveService: + def test_drive_fetch_policies_all_settings(self): + """Test fetching all 3 Drive and Docs policy settings from Cloud Identity API""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_credentials = MagicMock() + mock_session = MagicMock() + mock_session.credentials = mock_credentials + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + # Mock the actual Cloud Identity Policy API v1 response shape: + # - "type" (not "name"), prefixed with "settings/" + # - inner value field names are camelCase + mock_policies_list.execute.return_value = { + "policies": [ + { + "setting": { + "type": "settings/drive_and_docs.external_sharing", + "value": { + "externalSharingMode": "ALLOWLISTED_DOMAINS", + "warnForExternalSharing": True, + "warnForSharingOutsideAllowlistedDomains": True, + "allowPublishingFiles": False, + "accessCheckerSuggestions": "RECIPIENTS_ONLY", + "allowedPartiesForDistributingContent": "ELIGIBLE_INTERNAL_USERS", + }, + } + }, + { + "setting": { + "type": "settings/drive_and_docs.shared_drive_creation", + "value": { + "allowSharedDriveCreation": True, + "allowManagersToOverrideSettings": False, + "allowNonMemberAccess": False, + "allowedPartiesForDownloadPrintCopy": "EDITORS_ONLY", + }, + } + }, + { + "setting": { + "type": "settings/drive_and_docs.drive_for_desktop", + "value": {"allowDriveForDesktop": False}, + } + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.drive.drive_service import ( + Drive, + ) + + drive = Drive(mock_provider) + + assert drive.policies_fetched is True + assert drive.policies.external_sharing_mode == "ALLOWLISTED_DOMAINS" + assert drive.policies.warn_for_external_sharing is True + assert drive.policies.warn_for_sharing_outside_allowlisted_domains is True + assert drive.policies.allow_publishing_files is False + assert drive.policies.access_checker_suggestions == "RECIPIENTS_ONLY" + assert ( + drive.policies.allowed_parties_for_distributing_content + == "ELIGIBLE_INTERNAL_USERS" + ) + assert drive.policies.allow_shared_drive_creation is True + assert drive.policies.allow_managers_to_override_settings is False + assert drive.policies.allow_non_member_access is False + assert ( + drive.policies.allowed_parties_for_download_print_copy == "EDITORS_ONLY" + ) + assert drive.policies.allow_drive_for_desktop is False + + def test_drive_fetch_policies_empty_response(self): + """Test handling empty policies response""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + mock_policies_list.execute.return_value = {"policies": []} + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.drive.drive_service import ( + Drive, + ) + + drive = Drive(mock_provider) + + assert drive.policies_fetched is True + assert drive.policies.external_sharing_mode is None + assert drive.policies.warn_for_external_sharing is None + assert drive.policies.allow_publishing_files is None + assert drive.policies.allow_shared_drive_creation is None + assert drive.policies.allow_drive_for_desktop is None + + def test_drive_fetch_policies_api_error(self): + """Test handling of API errors during policy fetch""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_service.policies().list.side_effect = Exception("API Error") + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.drive.drive_service import ( + Drive, + ) + + drive = Drive(mock_provider) + + assert drive.policies_fetched is False + assert drive.policies.external_sharing_mode is None + assert drive.policies.allow_shared_drive_creation is None + assert drive.policies.allow_drive_for_desktop is None + + def test_drive_fetch_policies_build_service_returns_none(self): + """Test early return when _build_service fails to construct the client""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_service.GoogleWorkspaceService._build_service", + return_value=None, + ), + ): + from prowler.providers.googleworkspace.services.drive.drive_service import ( + Drive, + ) + + drive = Drive(mock_provider) + + assert drive.policies_fetched is False + assert drive.policies.external_sharing_mode is None + assert drive.policies.allow_shared_drive_creation is None + assert drive.policies.allow_drive_for_desktop is None + + def test_drive_fetch_policies_execute_raises(self): + """Test inner except handler when request.execute() raises during pagination""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_request = MagicMock() + mock_request.execute.side_effect = Exception("Execute failed") + mock_service.policies().list.return_value = mock_request + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.drive.drive_service import ( + Drive, + ) + + drive = Drive(mock_provider) + + assert drive.policies_fetched is False + assert drive.policies.external_sharing_mode is None + assert drive.policies.allow_shared_drive_creation is None + assert drive.policies.allow_drive_for_desktop is None + + def test_drive_policies_model(self): + """Test DrivePolicies Pydantic model""" + from prowler.providers.googleworkspace.services.drive.drive_service import ( + DrivePolicies, + ) + + policies = DrivePolicies( + external_sharing_mode="ALLOWLISTED_DOMAINS", + warn_for_external_sharing=True, + warn_for_sharing_outside_allowlisted_domains=True, + allow_publishing_files=False, + access_checker_suggestions="RECIPIENTS_ONLY", + allowed_parties_for_distributing_content="ELIGIBLE_INTERNAL_USERS", + allow_shared_drive_creation=True, + allow_managers_to_override_settings=False, + allow_non_member_access=False, + allowed_parties_for_download_print_copy="EDITORS_ONLY", + allow_drive_for_desktop=False, + ) + + assert policies.external_sharing_mode == "ALLOWLISTED_DOMAINS" + assert policies.warn_for_external_sharing is True + assert policies.allow_publishing_files is False + assert policies.access_checker_suggestions == "RECIPIENTS_ONLY" + assert policies.allow_shared_drive_creation is True + assert policies.allow_managers_to_override_settings is False + assert policies.allow_non_member_access is False + assert policies.allowed_parties_for_download_print_copy == "EDITORS_ONLY" + assert policies.allow_drive_for_desktop is False + + def test_drive_fetch_policies_ignores_ou_and_group_level(self): + """Test that OU-level and group-level policies are skipped, only customer-level used""" + mock_provider = set_mocked_googleworkspace_provider() + mock_provider.audit_config = {} + mock_provider.fixer_config = {} + mock_session = MagicMock() + mock_session.credentials = MagicMock() + mock_provider.session = mock_session + + mock_service = MagicMock() + mock_policies_list = MagicMock() + # Response includes 3 policies of the same type at different org levels: + # customer-level (no policyQuery), OU-level, and group-level + mock_policies_list.execute.return_value = { + "policies": [ + { + # Customer-level: no policyQuery → should be used + "setting": { + "type": "settings/drive_and_docs.external_sharing", + "value": { + "externalSharingMode": "ALLOWLISTED_DOMAINS", + "warnForExternalSharing": True, + }, + } + }, + { + # OU-level: has policyQuery.orgUnit → should be skipped + "policyQuery": {"orgUnit": "orgUnits/sales_team"}, + "setting": { + "type": "settings/drive_and_docs.external_sharing", + "value": { + "externalSharingMode": "ALLOWED", + "warnForExternalSharing": False, + }, + }, + }, + { + # Group-level: has policyQuery.group → should be skipped + "policyQuery": {"group": "groups/contractors"}, + "setting": { + "type": "settings/drive_and_docs.external_sharing", + "value": { + "externalSharingMode": "DISALLOWED", + "warnForExternalSharing": False, + }, + }, + }, + { + # Customer-level shared drive creation + "setting": { + "type": "settings/drive_and_docs.shared_drive_creation", + "value": {"allowSharedDriveCreation": True}, + } + }, + { + # OU-level shared drive creation → should be skipped + "policyQuery": {"orgUnit": "orgUnits/engineering"}, + "setting": { + "type": "settings/drive_and_docs.shared_drive_creation", + "value": {"allowSharedDriveCreation": False}, + }, + }, + ] + } + mock_service.policies().list.return_value = mock_policies_list + mock_service.policies().list_next.return_value = None + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_service.GoogleWorkspaceService._build_service", + return_value=mock_service, + ), + ): + from prowler.providers.googleworkspace.services.drive.drive_service import ( + Drive, + ) + + drive = Drive(mock_provider) + + assert drive.policies_fetched is True + # Customer-level values should be stored + assert drive.policies.external_sharing_mode == "ALLOWLISTED_DOMAINS" + assert drive.policies.warn_for_external_sharing is True + assert drive.policies.allow_shared_drive_creation is True + # OU/group-level values (ALLOWED, False, False) should NOT have overwritten diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py new file mode 100644 index 0000000000..f221e1ec75 --- /dev/null +++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_creation_allowed/drive_shared_drive_creation_allowed_test.py @@ -0,0 +1,124 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + DOMAIN, + set_mocked_googleworkspace_provider, +) + + +class TestDriveSharedDriveCreationAllowed: + def test_pass_creation_allowed(self): + """Test PASS when users are allowed to create shared drives""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_creation_allowed.drive_shared_drive_creation_allowed.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_creation_allowed.drive_shared_drive_creation_allowed import ( + drive_shared_drive_creation_allowed, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(allow_shared_drive_creation=True) + + check = drive_shared_drive_creation_allowed() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "allowed" in findings[0].status_extended + assert findings[0].resource_name == DOMAIN + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_creation_disabled(self): + """Test FAIL when users are prevented from creating shared drives""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_creation_allowed.drive_shared_drive_creation_allowed.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_creation_allowed.drive_shared_drive_creation_allowed import ( + drive_shared_drive_creation_allowed, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + allow_shared_drive_creation=False + ) + + check = drive_shared_drive_creation_allowed() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "prevented" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) but fetch succeeded""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_creation_allowed.drive_shared_drive_creation_allowed.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_creation_allowed.drive_shared_drive_creation_allowed import ( + drive_shared_drive_creation_allowed, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(allow_shared_drive_creation=None) + + check = drive_shared_drive_creation_allowed() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_creation_allowed.drive_shared_drive_creation_allowed.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_creation_allowed.drive_shared_drive_creation_allowed import ( + drive_shared_drive_creation_allowed, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = False + mock_drive_client.policies = DrivePolicies() + + check = drive_shared_drive_creation_allowed() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py new file mode 100644 index 0000000000..35ee7b1651 --- /dev/null +++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_disable_download_print_copy/drive_shared_drive_disable_download_print_copy_test.py @@ -0,0 +1,158 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + DOMAIN, + set_mocked_googleworkspace_provider, +) + + +class TestDriveSharedDriveDisableDownloadPrintCopy: + def test_pass_editors_only(self): + """Test PASS when download/print/copy is restricted to editors only""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_disable_download_print_copy.drive_shared_drive_disable_download_print_copy.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_disable_download_print_copy.drive_shared_drive_disable_download_print_copy import ( + drive_shared_drive_disable_download_print_copy, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + allowed_parties_for_download_print_copy="EDITORS_ONLY" + ) + + check = drive_shared_drive_disable_download_print_copy() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "EDITORS_ONLY" in findings[0].status_extended + assert findings[0].resource_name == DOMAIN + assert findings[0].customer_id == CUSTOMER_ID + + def test_pass_managers_only(self): + """Test PASS when download/print/copy is restricted to managers only""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_disable_download_print_copy.drive_shared_drive_disable_download_print_copy.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_disable_download_print_copy.drive_shared_drive_disable_download_print_copy import ( + drive_shared_drive_disable_download_print_copy, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + allowed_parties_for_download_print_copy="MANAGERS_ONLY" + ) + + check = drive_shared_drive_disable_download_print_copy() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "MANAGERS_ONLY" in findings[0].status_extended + + def test_fail_all_allowed(self): + """Test FAIL when all users (including viewers/commenters) can download/print/copy""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_disable_download_print_copy.drive_shared_drive_disable_download_print_copy.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_disable_download_print_copy.drive_shared_drive_disable_download_print_copy import ( + drive_shared_drive_disable_download_print_copy, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + allowed_parties_for_download_print_copy="ALL" + ) + + check = drive_shared_drive_disable_download_print_copy() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "ALL" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) but fetch succeeded""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_disable_download_print_copy.drive_shared_drive_disable_download_print_copy.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_disable_download_print_copy.drive_shared_drive_disable_download_print_copy import ( + drive_shared_drive_disable_download_print_copy, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + allowed_parties_for_download_print_copy=None + ) + + check = drive_shared_drive_disable_download_print_copy() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_disable_download_print_copy.drive_shared_drive_disable_download_print_copy.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_disable_download_print_copy.drive_shared_drive_disable_download_print_copy import ( + drive_shared_drive_disable_download_print_copy, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = False + mock_drive_client.policies = DrivePolicies() + + check = drive_shared_drive_disable_download_print_copy() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override_test.py new file mode 100644 index 0000000000..4b29aa5a0a --- /dev/null +++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_managers_cannot_override/drive_shared_drive_managers_cannot_override_test.py @@ -0,0 +1,128 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + DOMAIN, + set_mocked_googleworkspace_provider, +) + + +class TestDriveSharedDriveManagersCannotOverride: + def test_pass_override_disabled(self): + """Test PASS when managers cannot override shared drive settings""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_managers_cannot_override.drive_shared_drive_managers_cannot_override.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_managers_cannot_override.drive_shared_drive_managers_cannot_override import ( + drive_shared_drive_managers_cannot_override, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + allow_managers_to_override_settings=False + ) + + check = drive_shared_drive_managers_cannot_override() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "cannot override" in findings[0].status_extended + assert findings[0].resource_name == DOMAIN + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_override_allowed(self): + """Test FAIL when managers can override shared drive settings""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_managers_cannot_override.drive_shared_drive_managers_cannot_override.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_managers_cannot_override.drive_shared_drive_managers_cannot_override import ( + drive_shared_drive_managers_cannot_override, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + allow_managers_to_override_settings=True + ) + + check = drive_shared_drive_managers_cannot_override() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "allowed to override" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) but fetch succeeded""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_managers_cannot_override.drive_shared_drive_managers_cannot_override.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_managers_cannot_override.drive_shared_drive_managers_cannot_override import ( + drive_shared_drive_managers_cannot_override, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + allow_managers_to_override_settings=None + ) + + check = drive_shared_drive_managers_cannot_override() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_managers_cannot_override.drive_shared_drive_managers_cannot_override.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_managers_cannot_override.drive_shared_drive_managers_cannot_override import ( + drive_shared_drive_managers_cannot_override, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = False + mock_drive_client.policies = DrivePolicies() + + check = drive_shared_drive_managers_cannot_override() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access_test.py b/tests/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access_test.py new file mode 100644 index 0000000000..06b1877d52 --- /dev/null +++ b/tests/providers/googleworkspace/services/drive/drive_shared_drive_members_only_access/drive_shared_drive_members_only_access_test.py @@ -0,0 +1,122 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + DOMAIN, + set_mocked_googleworkspace_provider, +) + + +class TestDriveSharedDriveMembersOnlyAccess: + def test_pass_non_member_access_disabled(self): + """Test PASS when non-member access to shared drive files is disabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_members_only_access.drive_shared_drive_members_only_access.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_members_only_access.drive_shared_drive_members_only_access import ( + drive_shared_drive_members_only_access, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(allow_non_member_access=False) + + check = drive_shared_drive_members_only_access() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "members only" in findings[0].status_extended + assert findings[0].resource_name == DOMAIN + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_non_member_access_enabled(self): + """Test FAIL when non-members can be added to shared drive files""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_members_only_access.drive_shared_drive_members_only_access.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_members_only_access.drive_shared_drive_members_only_access import ( + drive_shared_drive_members_only_access, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(allow_non_member_access=True) + + check = drive_shared_drive_members_only_access() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "non-members" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) but fetch succeeded""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_members_only_access.drive_shared_drive_members_only_access.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_members_only_access.drive_shared_drive_members_only_access import ( + drive_shared_drive_members_only_access, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(allow_non_member_access=None) + + check = drive_shared_drive_members_only_access() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_shared_drive_members_only_access.drive_shared_drive_members_only_access.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_shared_drive_members_only_access.drive_shared_drive_members_only_access import ( + drive_shared_drive_members_only_access, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = False + mock_drive_client.policies = DrivePolicies() + + check = drive_shared_drive_members_only_access() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains_test.py b/tests/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains_test.py new file mode 100644 index 0000000000..0323e5e399 --- /dev/null +++ b/tests/providers/googleworkspace/services/drive/drive_sharing_allowlisted_domains/drive_sharing_allowlisted_domains_test.py @@ -0,0 +1,154 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + DOMAIN, + set_mocked_googleworkspace_provider, +) + + +class TestDriveSharingAllowlistedDomains: + def test_pass_allowlisted_domains(self): + """Test PASS when external sharing is restricted to allowlisted domains""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_sharing_allowlisted_domains.drive_sharing_allowlisted_domains.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_sharing_allowlisted_domains.drive_sharing_allowlisted_domains import ( + drive_sharing_allowlisted_domains, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + external_sharing_mode="ALLOWLISTED_DOMAINS" + ) + + check = drive_sharing_allowlisted_domains() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "allowlisted domains" in findings[0].status_extended + assert findings[0].resource_name == DOMAIN + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_allowed(self): + """Test FAIL when external sharing is set to ALLOWED (unrestricted)""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_sharing_allowlisted_domains.drive_sharing_allowlisted_domains.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_sharing_allowlisted_domains.drive_sharing_allowlisted_domains import ( + drive_sharing_allowlisted_domains, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(external_sharing_mode="ALLOWED") + + check = drive_sharing_allowlisted_domains() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "ALLOWED" in findings[0].status_extended + + def test_fail_disallowed(self): + """Test FAIL when external sharing is DISALLOWED (stricter than allowlist but not the expected value)""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_sharing_allowlisted_domains.drive_sharing_allowlisted_domains.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_sharing_allowlisted_domains.drive_sharing_allowlisted_domains import ( + drive_sharing_allowlisted_domains, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + external_sharing_mode="DISALLOWED" + ) + + check = drive_sharing_allowlisted_domains() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "DISALLOWED" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) but fetch succeeded""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_sharing_allowlisted_domains.drive_sharing_allowlisted_domains.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_sharing_allowlisted_domains.drive_sharing_allowlisted_domains import ( + drive_sharing_allowlisted_domains, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies(external_sharing_mode=None) + + check = drive_sharing_allowlisted_domains() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_sharing_allowlisted_domains.drive_sharing_allowlisted_domains.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_sharing_allowlisted_domains.drive_sharing_allowlisted_domains import ( + drive_sharing_allowlisted_domains, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = False + mock_drive_client.policies = DrivePolicies() + + check = drive_sharing_allowlisted_domains() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py b/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py new file mode 100644 index 0000000000..a494bbe2eb --- /dev/null +++ b/tests/providers/googleworkspace/services/drive/drive_warn_sharing_with_allowlisted_domains/drive_warn_sharing_with_allowlisted_domains_test.py @@ -0,0 +1,128 @@ +from unittest.mock import patch + +from prowler.providers.googleworkspace.services.drive.drive_service import DrivePolicies +from tests.providers.googleworkspace.googleworkspace_fixtures import ( + CUSTOMER_ID, + DOMAIN, + set_mocked_googleworkspace_provider, +) + + +class TestDriveWarnSharingWithAllowlistedDomains: + def test_pass_warning_enabled(self): + """Test PASS when warning for sharing with allowlisted domains is enabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_warn_sharing_with_allowlisted_domains.drive_warn_sharing_with_allowlisted_domains.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_warn_sharing_with_allowlisted_domains.drive_warn_sharing_with_allowlisted_domains import ( + drive_warn_sharing_with_allowlisted_domains, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + warn_for_sharing_outside_allowlisted_domains=True + ) + + check = drive_warn_sharing_with_allowlisted_domains() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "PASS" + assert "warned" in findings[0].status_extended + assert findings[0].resource_name == DOMAIN + assert findings[0].customer_id == CUSTOMER_ID + + def test_fail_warning_disabled(self): + """Test FAIL when warning for sharing with allowlisted domains is disabled""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_warn_sharing_with_allowlisted_domains.drive_warn_sharing_with_allowlisted_domains.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_warn_sharing_with_allowlisted_domains.drive_warn_sharing_with_allowlisted_domains import ( + drive_warn_sharing_with_allowlisted_domains, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + warn_for_sharing_outside_allowlisted_domains=False + ) + + check = drive_warn_sharing_with_allowlisted_domains() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "disabled" in findings[0].status_extended + + def test_fail_no_policy_set(self): + """Test FAIL when no explicit policy is set (None) but fetch succeeded""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_warn_sharing_with_allowlisted_domains.drive_warn_sharing_with_allowlisted_domains.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_warn_sharing_with_allowlisted_domains.drive_warn_sharing_with_allowlisted_domains import ( + drive_warn_sharing_with_allowlisted_domains, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = True + mock_drive_client.policies = DrivePolicies( + warn_for_sharing_outside_allowlisted_domains=None + ) + + check = drive_warn_sharing_with_allowlisted_domains() + findings = check.execute() + + assert len(findings) == 1 + assert findings[0].status == "FAIL" + assert "not explicitly configured" in findings[0].status_extended + + def test_no_findings_when_fetch_failed(self): + """Test no findings returned when the API fetch failed""" + mock_provider = set_mocked_googleworkspace_provider() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.googleworkspace.services.drive.drive_warn_sharing_with_allowlisted_domains.drive_warn_sharing_with_allowlisted_domains.drive_client" + ) as mock_drive_client, + ): + from prowler.providers.googleworkspace.services.drive.drive_warn_sharing_with_allowlisted_domains.drive_warn_sharing_with_allowlisted_domains import ( + drive_warn_sharing_with_allowlisted_domains, + ) + + mock_drive_client.provider = mock_provider + mock_drive_client.policies_fetched = False + mock_drive_client.policies = DrivePolicies() + + check = drive_warn_sharing_with_allowlisted_domains() + findings = check.execute() + + assert len(findings) == 0 diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users_test.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users_test.py new file mode 100644 index 0000000000..f9991b0e49 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users_test.py @@ -0,0 +1,858 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + ApplicationEnforcedRestrictions, + ApplicationsConditions, + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + Conditions, + GrantControlOperator, + GrantControls, + PersistentBrowser, + SessionControls, + SignInFrequency, + SignInFrequencyInterval, + UsersConditions, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + +CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users" + + +def _make_session_controls(): + """Return default session controls for test policies.""" + return SessionControls( + persistent_browser=PersistentBrowser(is_enabled=False, mode="always"), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ) + + +def _make_conditions( + included_applications=None, + included_users=None, + excluded_applications=None, + excluded_users=None, + excluded_groups=None, + excluded_roles=None, +): + """Return Conditions with the given application and user scopes.""" + return Conditions( + application_conditions=ApplicationsConditions( + included_applications=included_applications or ["All"], + excluded_applications=excluded_applications or [], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=excluded_groups or [], + included_users=included_users or ["All"], + excluded_users=excluded_users or [], + included_roles=[], + excluded_roles=excluded_roles or [], + ), + client_app_types=[], + user_risk_levels=[], + ) + + +def _make_grant_controls(built_in_controls=None): + """Return GrantControls with the given built-in controls.""" + return GrantControls( + built_in_controls=built_in_controls or [ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + authentication_strength=None, + ) + + +class Test_entra_conditional_access_policy_all_apps_all_users: + def test_no_conditional_access_policies(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + + entra_client.conditional_access_policies = {} + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_disabled(self): + policy_id = str(uuid4()) + display_name = "All Apps All Users Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.DISABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_excluding_roles(self): + policy_id = str(uuid4()) + display_name = "All Users Except Role Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions( + excluded_roles=["62e90394-69f5-4237-9190-012177145e10"], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} targets all cloud apps and all users but includes exclusions. Review excluded users/groups/roles/applications and verify compensating policies protect excluded identities and apps." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_not_targeting_all_apps(self): + policy_id = str(uuid4()) + display_name = "Specific App Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions( + included_applications=["some-app-id"], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_not_targeting_all_users(self): + policy_id = str(uuid4()) + display_name = "Specific Users Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions( + included_users=["some-user-id"], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_excluding_applications(self): + policy_id = str(uuid4()) + display_name = "All Apps Except One Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions( + excluded_applications=["excluded-app-id"], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} targets all cloud apps and all users but includes exclusions. Review excluded users/groups/roles/applications and verify compensating policies protect excluded identities and apps." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_excluding_users(self): + policy_id = str(uuid4()) + display_name = "All Users Except One Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions( + excluded_users=["excluded-user-id"], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} targets all cloud apps and all users but includes exclusions. Review excluded users/groups/roles/applications and verify compensating policies protect excluded identities and apps." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_only_password_change(self): + policy_id = str(uuid4()) + display_name = "Password Change Only Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls( + built_in_controls=[ + ConditionalAccessGrantControl.PASSWORD_CHANGE + ], + ), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_enabled_for_reporting(self): + policy_id = str(uuid4()) + display_name = "All Apps All Users - Report Only" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} covers all cloud apps and all users but is only in report-only mode." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_enabled_all_apps_all_users(self): + policy_id = str(uuid4()) + display_name = "All Apps All Users Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} covers all cloud apps and all users." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_multiple_policies_first_disabled_second_enabled(self): + disabled_id = str(uuid4()) + enabled_id = str(uuid4()) + enabled_name = "All Apps All Users - Enabled" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + disabled_id: ConditionalAccessPolicy( + id=disabled_id, + display_name="All Apps All Users - Disabled", + conditions=_make_conditions(), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.DISABLED, + ), + enabled_id: ConditionalAccessPolicy( + id=enabled_id, + display_name=enabled_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy {enabled_name} covers all cloud apps and all users." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[enabled_id].dict() + ) + assert result[0].resource_name == enabled_name + assert result[0].resource_id == enabled_id + assert result[0].location == "global" + + def test_policy_with_block_grant_control(self): + policy_id = str(uuid4()) + display_name = "Block All Apps All Users" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + ), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} covers all cloud apps and all users." + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_manual_policy_does_not_prevent_later_pass(self): + manual_policy_id = str(uuid4()) + pass_policy_id = str(uuid4()) + manual_display_name = "All Apps All Users With Exclusions" + pass_display_name = "All Apps All Users Enforced" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + manual_policy_id: ConditionalAccessPolicy( + id=manual_policy_id, + display_name=manual_display_name, + conditions=_make_conditions( + excluded_users=["excluded-user-id"], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ), + pass_policy_id: ConditionalAccessPolicy( + id=pass_policy_id, + display_name=pass_display_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy {pass_display_name} covers all cloud apps and all users." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[pass_policy_id].dict() + ) + assert result[0].resource_name == pass_display_name + assert result[0].resource_id == pass_policy_id + + def test_policy_no_application_conditions(self): + policy_id = str(uuid4()) + display_name = "No App Conditions Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=None, + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_no_user_conditions(self): + policy_id = str(uuid4()) + display_name = "No User Conditions Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=None, + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_approved_client_app_required_for_mobile/entra_conditional_access_policy_approved_client_app_required_for_mobile_test.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_approved_client_app_required_for_mobile/entra_conditional_access_policy_approved_client_app_required_for_mobile_test.py index 71856429f8..f74d6ae4e2 100644 --- a/tests/providers/m365/services/entra/entra_conditional_access_policy_approved_client_app_required_for_mobile/entra_conditional_access_policy_approved_client_app_required_for_mobile_test.py +++ b/tests/providers/m365/services/entra/entra_conditional_access_policy_approved_client_app_required_for_mobile/entra_conditional_access_policy_approved_client_app_required_for_mobile_test.py @@ -222,7 +222,7 @@ class Test_entra_conditional_access_policy_approved_client_app_required_for_mobi assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Conditional Access Policy '{display_name}' reports the requirement of approved client apps or app protection for mobile devices but does not enforce it." + == f"Conditional Access Policy {display_name} reports the requirement of approved client apps or app protection for mobile devices but does not enforce it." ) assert ( result[0].resource @@ -312,7 +312,7 @@ class Test_entra_conditional_access_policy_approved_client_app_required_for_mobi assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Conditional Access Policy '{display_name}' requires approved client apps or app protection for mobile devices." + == f"Conditional Access Policy {display_name} requires approved client apps or app protection for mobile devices." ) assert ( result[0].resource @@ -738,7 +738,7 @@ class Test_entra_conditional_access_policy_approved_client_app_required_for_mobi assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Conditional Access Policy '{display_name}' requires approved client apps or app protection for mobile devices." + == f"Conditional Access Policy {display_name} requires approved client apps or app protection for mobile devices." ) assert ( result[0].resource @@ -827,7 +827,7 @@ class Test_entra_conditional_access_policy_approved_client_app_required_for_mobi assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Conditional Access Policy '{display_name}' requires approved client apps or app protection for mobile devices." + == f"Conditional Access Policy {display_name} requires approved client apps or app protection for mobile devices." ) assert ( result[0].resource @@ -963,7 +963,7 @@ class Test_entra_conditional_access_policy_approved_client_app_required_for_mobi assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Conditional Access Policy '{enabled_name}' requires approved client apps or app protection for mobile devices." + == f"Conditional Access Policy {enabled_name} requires approved client apps or app protection for mobile devices." ) assert ( result[0].resource @@ -1052,7 +1052,7 @@ class Test_entra_conditional_access_policy_approved_client_app_required_for_mobi assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Conditional Access Policy '{display_name}' requires approved client apps or app protection for mobile devices." + == f"Conditional Access Policy {display_name} requires approved client apps or app protection for mobile devices." ) assert ( result[0].resource diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_block_unknown_device_platforms/entra_conditional_access_policy_block_unknown_device_platforms_test.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_block_unknown_device_platforms/entra_conditional_access_policy_block_unknown_device_platforms_test.py new file mode 100644 index 0000000000..f1ae073b6f --- /dev/null +++ b/tests/providers/m365/services/entra/entra_conditional_access_policy_block_unknown_device_platforms/entra_conditional_access_policy_block_unknown_device_platforms_test.py @@ -0,0 +1,702 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + ApplicationsConditions, + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + Conditions, + GrantControlOperator, + GrantControls, + PersistentBrowser, + PlatformConditions, + SessionControls, + SignInFrequency, + SignInFrequencyInterval, + UsersConditions, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + +CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms" + +KNOWN_PLATFORMS = ["android", "iOS", "windows", "macOS", "linux"] + + +def _make_session_controls() -> SessionControls: + """Return a minimal SessionControls instance for testing.""" + return SessionControls( + persistent_browser=PersistentBrowser(is_enabled=False, mode="always"), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ) + + +class Test_entra_conditional_access_policy_block_unknown_device_platforms: + """Tests for the entra_conditional_access_policy_block_unknown_device_platforms check.""" + + def test_no_conditional_access_policies(self): + """Test FAIL when there are no Conditional Access policies.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import ( + entra_conditional_access_policy_block_unknown_device_platforms, + ) + + entra_client.conditional_access_policies = {} + + check = entra_conditional_access_policy_block_unknown_device_platforms() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy blocks access from unknown or unsupported device platforms." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_disabled(self): + """Test FAIL when the only matching policy is disabled.""" + policy_id = str(uuid4()) + display_name = "Block Unknown Platforms" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import ( + entra_conditional_access_policy_block_unknown_device_platforms, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + platform_conditions=PlatformConditions( + include_platforms=["all"], + exclude_platforms=KNOWN_PLATFORMS, + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.OR, + authentication_strength=None, + ), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.DISABLED, + ) + } + + check = entra_conditional_access_policy_block_unknown_device_platforms() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy blocks access from unknown or unsupported device platforms." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_enabled_for_reporting_only(self): + """Test FAIL when the matching policy is only in report-only mode.""" + policy_id = str(uuid4()) + display_name = "Block Unknown Platforms" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import ( + entra_conditional_access_policy_block_unknown_device_platforms, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + platform_conditions=PlatformConditions( + include_platforms=["all"], + exclude_platforms=KNOWN_PLATFORMS, + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.OR, + authentication_strength=None, + ), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ) + } + + check = entra_conditional_access_policy_block_unknown_device_platforms() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Conditional Access Policy Block Unknown Platforms reports blocking unknown or unsupported device platforms but does not enforce it." + ) + assert result[0].resource_name == "Block Unknown Platforms" + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_no_platform_conditions(self): + """Test FAIL when the policy has no platform conditions configured.""" + policy_id = str(uuid4()) + display_name = "Block Unknown Platforms" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import ( + entra_conditional_access_policy_block_unknown_device_platforms, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + platform_conditions=None, + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.OR, + authentication_strength=None, + ), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_block_unknown_device_platforms() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy blocks access from unknown or unsupported device platforms." + ) + + def test_policy_does_not_include_all_platforms(self): + """Test FAIL when the policy includes specific platforms instead of all.""" + policy_id = str(uuid4()) + display_name = "Block Specific Platforms" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import ( + entra_conditional_access_policy_block_unknown_device_platforms, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + platform_conditions=PlatformConditions( + include_platforms=["android", "iOS"], + exclude_platforms=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.OR, + authentication_strength=None, + ), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_block_unknown_device_platforms() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_policy_missing_excluded_known_platforms(self): + """Test FAIL when the policy includes all platforms but does not exclude all known ones.""" + policy_id = str(uuid4()) + display_name = "Incomplete Platform Exclusion" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import ( + entra_conditional_access_policy_block_unknown_device_platforms, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + # Only exclude 3 of 5 known platforms + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + platform_conditions=PlatformConditions( + include_platforms=["all"], + exclude_platforms=["android", "iOS", "windows"], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.OR, + authentication_strength=None, + ), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_block_unknown_device_platforms() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_policy_no_block_grant_control(self): + """Test FAIL when the policy has correct platform conditions but does not block.""" + policy_id = str(uuid4()) + display_name = "MFA Unknown Platforms" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import ( + entra_conditional_access_policy_block_unknown_device_platforms, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + platform_conditions=PlatformConditions( + include_platforms=["all"], + exclude_platforms=KNOWN_PLATFORMS, + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.OR, + authentication_strength=None, + ), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_block_unknown_device_platforms() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_policy_limited_to_specific_users_and_apps(self): + """Test PASS when a scoped policy still blocks unknown device platforms.""" + policy_id = str(uuid4()) + display_name = "Scoped Unknown Platform Block" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import ( + entra_conditional_access_policy_block_unknown_device_platforms, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=[ + "00000000-0000-0000-0000-000000000001" + ], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=["11111111-1111-1111-1111-111111111111"], + excluded_groups=[], + included_users=[], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + platform_conditions=PlatformConditions( + include_platforms=["all"], + exclude_platforms=KNOWN_PLATFORMS, + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.OR, + authentication_strength=None, + ), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_block_unknown_device_platforms() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Conditional Access Policy Scoped Unknown Platform Block blocks access from unknown or unsupported device platforms." + ) + + def test_policy_enabled_and_compliant(self): + """Test PASS when an enabled policy blocks unknown device platforms correctly.""" + policy_id = str(uuid4()) + display_name = "Block Unknown Platforms" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import ( + entra_conditional_access_policy_block_unknown_device_platforms, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + platform_conditions=PlatformConditions( + include_platforms=["all"], + exclude_platforms=KNOWN_PLATFORMS, + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.OR, + authentication_strength=None, + ), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_block_unknown_device_platforms() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Conditional Access Policy Block Unknown Platforms blocks access from unknown or unsupported device platforms." + ) + assert result[0].resource_name == "Block Unknown Platforms" + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_mixed_policies_report_only_and_enabled(self): + """Test PASS when both report-only and enabled compliant policies exist.""" + report_policy_id = str(uuid4()) + enabled_policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import ( + entra_conditional_access_policy_block_unknown_device_platforms, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + base_conditions = Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + platform_conditions=PlatformConditions( + include_platforms=["all"], + exclude_platforms=KNOWN_PLATFORMS, + ), + ) + grant_controls = GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.OR, + authentication_strength=None, + ) + + entra_client.conditional_access_policies = { + report_policy_id: ConditionalAccessPolicy( + id=report_policy_id, + display_name="Report Only Policy", + conditions=base_conditions, + grant_controls=grant_controls, + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ), + enabled_policy_id: ConditionalAccessPolicy( + id=enabled_policy_id, + display_name="Enforced Block Policy", + conditions=base_conditions, + grant_controls=grant_controls, + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + check = entra_conditional_access_policy_block_unknown_device_platforms() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_name == "Enforced Block Policy" + assert result[0].resource_id == enabled_policy_id diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced_test.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced_test.py new file mode 100644 index 0000000000..52cef307f0 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced/entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced_test.py @@ -0,0 +1,697 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + ApplicationsConditions, + ConditionalAccessPolicyState, + Conditions, + DeviceConditions, + DeviceFilterMode, + GrantControlOperator, + GrantControls, + PersistentBrowser, + SessionControls, + SignInFrequency, + SignInFrequencyInterval, + SignInFrequencyType, + UsersConditions, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + +CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced" + + +def _make_policy( + policy_id, + display_name, + state=ConditionalAccessPolicyState.ENABLED, + included_users=None, + included_applications=None, + sign_in_frequency_enabled=True, + sign_in_frequency_interval=SignInFrequencyInterval.TIME_BASED, + sign_in_frequency_value=1, + sign_in_frequency_type=SignInFrequencyType.HOURS, + device_filter_mode=None, + device_filter_rule=None, +): + """Create a ConditionalAccessPolicy with the given parameters.""" + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + return ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=included_applications or ["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=included_users or ["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + device_conditions=DeviceConditions( + device_filter_mode=device_filter_mode, + device_filter_rule=device_filter_rule, + ), + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser(is_enabled=False, mode="always"), + sign_in_frequency=SignInFrequency( + is_enabled=sign_in_frequency_enabled, + frequency=sign_in_frequency_value, + type=sign_in_frequency_type, + interval=sign_in_frequency_interval, + ), + ), + state=state, + ) + + +class Test_entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced: + """Tests for sign-in frequency enforcement on non-corporate devices.""" + + def test_entra_no_conditional_access_policies(self): + """Test FAIL when no conditional access policies exist.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = {} + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_entra_policy_disabled(self): + """Test FAIL when a qualifying policy is disabled.""" + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name="Disabled Policy", + state=ConditionalAccessPolicyState.DISABLED, + device_filter_mode=DeviceFilterMode.INCLUDE, + device_filter_rule='device.isCompliant -ne True -or device.trustType -ne "ServerAD"', + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + + def test_entra_policy_enabled_for_reporting(self): + """Test FAIL when policy is enabled for reporting but not enforcing.""" + policy_id = str(uuid4()) + display_name = "Reporting Only Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name=display_name, + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + device_filter_mode=DeviceFilterMode.INCLUDE, + device_filter_rule='device.isCompliant -ne True -or device.trustType -ne "ServerAD"', + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} reports sign-in frequency for non-corporate devices but does not enforce it." + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + + def test_entra_policy_missing_all_users(self): + """Test FAIL when policy does not target all users.""" + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name="Limited Users Policy", + included_users=["user1@example.com"], + device_filter_mode=DeviceFilterMode.INCLUDE, + device_filter_rule="device.isCompliant -ne True", + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "Conditional Access Policies" + + def test_entra_policy_missing_all_applications(self): + """Test FAIL when policy does not target all applications.""" + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name="Limited Apps Policy", + included_applications=["Office365"], + device_filter_mode=DeviceFilterMode.INCLUDE, + device_filter_rule="device.isCompliant -ne True", + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "Conditional Access Policies" + + def test_entra_policy_sign_in_frequency_not_enabled(self): + """Test FAIL when sign-in frequency is not enabled.""" + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name="No Sign-In Freq Policy", + sign_in_frequency_enabled=False, + device_filter_mode=DeviceFilterMode.INCLUDE, + device_filter_rule="device.isCompliant -ne True", + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "Conditional Access Policies" + + def test_entra_policy_sign_in_frequency_not_time_based(self): + """Test FAIL when sign-in frequency interval is not time-based.""" + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name="EveryTime Policy", + sign_in_frequency_interval=SignInFrequencyInterval.EVERY_TIME, + device_filter_mode=DeviceFilterMode.INCLUDE, + device_filter_rule="device.isCompliant -ne True", + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "Conditional Access Policies" + + def test_entra_policy_no_device_filter(self): + """Test FAIL when policy has no device filter.""" + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name="No Device Filter Policy", + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "Conditional Access Policies" + + def test_entra_policy_device_filter_include_compliant(self): + """Test PASS with include mode device filter targeting non-compliant devices.""" + policy_id = str(uuid4()) + display_name = "Sign-In Freq Include Filter" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name=display_name, + device_filter_mode=DeviceFilterMode.INCLUDE, + device_filter_rule='device.isCompliant -ne True -or device.trustType -ne "ServerAD"', + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} enforces sign-in frequency for non-corporate devices." + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + + def test_entra_policy_device_filter_exclude_compliant(self): + """Test PASS with exclude mode device filter excluding corporate devices.""" + policy_id = str(uuid4()) + display_name = "Sign-In Freq Exclude Filter" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name=display_name, + device_filter_mode=DeviceFilterMode.EXCLUDE, + device_filter_rule='device.isCompliant -eq True -and device.trustType -eq "ServerAD"', + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + + def test_entra_policy_device_filter_unrelated_rule(self): + """Test FAIL when device filter rule does not target corporate device properties.""" + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name="Unrelated Filter Policy", + device_filter_mode=DeviceFilterMode.INCLUDE, + device_filter_rule='device.displayName -contains "kiosk"', + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "Conditional Access Policies" + + def test_entra_policy_device_filter_include_corporate_devices(self): + """Test FAIL when include mode targets only corporate devices.""" + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name="Corporate Devices Policy", + device_filter_mode=DeviceFilterMode.INCLUDE, + device_filter_rule="device.isCompliant -eq True", + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "Conditional Access Policies" + + def test_entra_policy_device_filter_exclude_non_corporate_devices(self): + """Test FAIL when exclude mode excludes non-corporate devices.""" + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name="Exclude Unmanaged Devices Policy", + device_filter_mode=DeviceFilterMode.EXCLUDE, + device_filter_rule="device.isCompliant -eq False", + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "Conditional Access Policies" + + def test_entra_multiple_policies_one_compliant(self): + """Test PASS when at least one policy among multiple is compliant.""" + policy_id_1 = str(uuid4()) + policy_id_2 = str(uuid4()) + display_name_2 = "Compliant Sign-In Freq Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id_1: _make_policy( + policy_id=policy_id_1, + display_name="Non-Compliant Policy", + sign_in_frequency_enabled=False, + ), + policy_id_2: _make_policy( + policy_id=policy_id_2, + display_name=display_name_2, + device_filter_mode=DeviceFilterMode.INCLUDE, + device_filter_rule='device.trustType -ne "ServerAD"', + ), + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_name == display_name_2 + assert result[0].resource_id == policy_id_2 + + def test_entra_policy_with_trust_type_only(self): + """Test PASS with device filter referencing only trustType.""" + policy_id = str(uuid4()) + display_name = "TrustType Filter Policy" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced.entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced import ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name=display_name, + device_filter_mode=DeviceFilterMode.EXCLUDE, + device_filter_rule='device.trustType -eq "ServerAD"', + ) + } + + check = ( + entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced() + ) + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_name == display_name diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_directory_sync_account_excluded/entra_conditional_access_policy_directory_sync_account_excluded_test.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_directory_sync_account_excluded/entra_conditional_access_policy_directory_sync_account_excluded_test.py new file mode 100644 index 0000000000..d072713f04 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_conditional_access_policy_directory_sync_account_excluded/entra_conditional_access_policy_directory_sync_account_excluded_test.py @@ -0,0 +1,711 @@ +"""Tests for the entra_conditional_access_policy_directory_sync_account_excluded check.""" + +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + ApplicationEnforcedRestrictions, + ApplicationsConditions, + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + Conditions, + GrantControlOperator, + GrantControls, + PersistentBrowser, + SessionControls, + SignInFrequency, + SignInFrequencyInterval, + UsersConditions, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + +DIRECTORY_SYNC_ROLE_TEMPLATE_ID = "d29b2b05-8046-44ba-8758-1e26182fcf32" + +CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded" + + +def _default_session_controls(): + """Return default session controls for test policies.""" + return SessionControls( + persistent_browser=PersistentBrowser(is_enabled=False, mode="always"), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ) + + +def _default_grant_controls(): + """Return default grant controls requiring MFA for test policies.""" + return GrantControls( + built_in_controls=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + authentication_strength=None, + ) + + +class Test_entra_conditional_access_policy_directory_sync_account_excluded: + """Test class for Directory Sync Account exclusion check.""" + + def test_no_conditional_access_policies(self): + """Test PASS when no Conditional Access policies exist.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import ( + entra_conditional_access_policy_directory_sync_account_excluded, + ) + + entra_client.conditional_access_policies = {} + + check = entra_conditional_access_policy_directory_sync_account_excluded() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "No Conditional Access Policy targets all users and all cloud apps, so no Directory Synchronization Accounts exclusion is needed." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_disabled(self): + """Test PASS when only a disabled policy exists targeting all users and apps.""" + policy_id = str(uuid4()) + display_name = "Require MFA for All Users" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import ( + entra_conditional_access_policy_directory_sync_account_excluded, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_default_grant_controls(), + session_controls=_default_session_controls(), + state=ConditionalAccessPolicyState.DISABLED, + ) + } + + check = entra_conditional_access_policy_directory_sync_account_excluded() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "No Conditional Access Policy targets all users and all cloud apps, so no Directory Synchronization Accounts exclusion is needed." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_targets_specific_users(self): + """Test PASS when the policy targets specific users, not all users.""" + policy_id = str(uuid4()) + display_name = "Require MFA for Admins" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import ( + entra_conditional_access_policy_directory_sync_account_excluded, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=["some-group-id"], + excluded_groups=[], + included_users=[], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_default_grant_controls(), + session_controls=_default_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_directory_sync_account_excluded() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "No Conditional Access Policy targets all users and all cloud apps, so no Directory Synchronization Accounts exclusion is needed." + ) + + def test_policy_targets_specific_apps(self): + """Test PASS when the policy targets specific apps, not all apps.""" + policy_id = str(uuid4()) + display_name = "Require MFA for Office 365" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import ( + entra_conditional_access_policy_directory_sync_account_excluded, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["some-app-id"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_default_grant_controls(), + session_controls=_default_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_directory_sync_account_excluded() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "No Conditional Access Policy targets all users and all cloud apps, so no Directory Synchronization Accounts exclusion is needed." + ) + + def test_policy_enabled_without_sync_exclusion(self): + """Test FAIL when an enabled policy targets all users and all apps but does not exclude the sync role.""" + policy_id = str(uuid4()) + display_name = "Require MFA for All Users" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import ( + entra_conditional_access_policy_directory_sync_account_excluded, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_default_grant_controls(), + session_controls=_default_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_directory_sync_account_excluded() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} does not exclude the Directory Synchronization Accounts role, which may break Entra Connect sync." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_report_only_without_sync_exclusion(self): + """Test FAIL when a report-only policy targets all users and apps without excluding the sync role.""" + policy_id = str(uuid4()) + display_name = "Report Only - Require MFA" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import ( + entra_conditional_access_policy_directory_sync_account_excluded, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_default_grant_controls(), + session_controls=_default_session_controls(), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ) + } + + check = entra_conditional_access_policy_directory_sync_account_excluded() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} reports excluding the Directory Synchronization Accounts role but does not enforce it." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_enabled_with_sync_exclusion(self): + """Test PASS when an enabled policy targets all users and apps and excludes the sync role.""" + policy_id = str(uuid4()) + display_name = "Require MFA for All Users" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import ( + entra_conditional_access_policy_directory_sync_account_excluded, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[ + DIRECTORY_SYNC_ROLE_TEMPLATE_ID, + ], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_default_grant_controls(), + session_controls=_default_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_directory_sync_account_excluded() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} excludes the Directory Synchronization Accounts role." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_with_sync_role_and_other_excluded_roles(self): + """Test PASS when the sync role is excluded alongside other roles.""" + policy_id = str(uuid4()) + display_name = "Require MFA for All Users" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import ( + entra_conditional_access_policy_directory_sync_account_excluded, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[ + "some-other-role-id", + DIRECTORY_SYNC_ROLE_TEMPLATE_ID, + ], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_default_grant_controls(), + session_controls=_default_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_directory_sync_account_excluded() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} excludes the Directory Synchronization Accounts role." + ) + + def test_multiple_policies_mixed_results(self): + """Test multiple policies where one excludes sync role and another does not.""" + policy_id_pass = str(uuid4()) + policy_id_fail = str(uuid4()) + display_name_pass = "MFA Policy - With Exclusion" + display_name_fail = "MFA Policy - Without Exclusion" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import ( + entra_conditional_access_policy_directory_sync_account_excluded, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id_pass: ConditionalAccessPolicy( + id=policy_id_pass, + display_name=display_name_pass, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[ + DIRECTORY_SYNC_ROLE_TEMPLATE_ID, + ], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_default_grant_controls(), + session_controls=_default_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ), + policy_id_fail: ConditionalAccessPolicy( + id=policy_id_fail, + display_name=display_name_fail, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_default_grant_controls(), + session_controls=_default_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + check = entra_conditional_access_policy_directory_sync_account_excluded() + result = check.execute() + assert len(result) == 2 + + pass_results = [r for r in result if r.status == "PASS"] + fail_results = [r for r in result if r.status == "FAIL"] + + assert len(pass_results) == 1 + assert len(fail_results) == 1 + + assert pass_results[0].resource_name == display_name_pass + assert pass_results[0].resource_id == policy_id_pass + assert ( + pass_results[0].status_extended + == f"Conditional Access Policy {display_name_pass} excludes the Directory Synchronization Accounts role." + ) + + assert fail_results[0].resource_name == display_name_fail + assert fail_results[0].resource_id == policy_id_fail + assert ( + fail_results[0].status_extended + == f"Conditional Access Policy {display_name_fail} does not exclude the Directory Synchronization Accounts role, which may break Entra Connect sync." + ) + + def test_policy_with_wrong_excluded_role(self): + """Test FAIL when the policy excludes a different role but not the sync role.""" + policy_id = str(uuid4()) + display_name = "Require MFA for All Users" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_directory_sync_account_excluded.entra_conditional_access_policy_directory_sync_account_excluded import ( + entra_conditional_access_policy_directory_sync_account_excluded, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=["some-other-role-id"], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_default_grant_controls(), + session_controls=_default_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_directory_sync_account_excluded() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} does not exclude the Directory Synchronization Accounts role, which may break Entra Connect sync." + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_mfa_enforced_for_guest_users/entra_conditional_access_policy_mfa_enforced_for_guest_users_test.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_mfa_enforced_for_guest_users/entra_conditional_access_policy_mfa_enforced_for_guest_users_test.py new file mode 100644 index 0000000000..5fb8f985c2 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_conditional_access_policy_mfa_enforced_for_guest_users/entra_conditional_access_policy_mfa_enforced_for_guest_users_test.py @@ -0,0 +1,705 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + ALL_GUEST_USER_TYPES, + ApplicationsConditions, + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + Conditions, + ExternalTenantsMembershipKind, + GrantControlOperator, + GrantControls, + GuestOrExternalUserType, + GuestsOrExternalUsers, + PersistentBrowser, + SessionControls, + SignInFrequency, + UsersConditions, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + +CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users" + + +def build_policy( + *, + display_name: str, + state: ConditionalAccessPolicyState, + included_applications: list[str] | None = None, + included_users: list[str] | None = None, + excluded_users: list[str] | None = None, + built_in_controls: list[ConditionalAccessGrantControl] | None = None, + authentication_strength: str | None = None, + included_guests_or_external_users: GuestsOrExternalUsers | None = None, + excluded_guests_or_external_users: GuestsOrExternalUsers | None = None, +): + """Build a ConditionalAccessPolicy for testing.""" + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + return ConditionalAccessPolicy( + id=str(uuid4()), + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=included_applications or ["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=included_users or [], + excluded_users=excluded_users or [], + included_roles=[], + excluded_roles=[], + included_guests_or_external_users=included_guests_or_external_users, + excluded_guests_or_external_users=excluded_guests_or_external_users, + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=GrantControls( + built_in_controls=built_in_controls or [], + operator=GrantControlOperator.OR, + authentication_strength=authentication_strength, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser(is_enabled=False, mode="always"), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=None, + ), + ), + state=state, + ) + + +class Test_entra_conditional_access_policy_mfa_enforced_for_guest_users: + """Tests for the entra_conditional_access_policy_mfa_enforced_for_guest_users check.""" + + def test_no_conditional_access_policies(self): + """Test FAIL when there are no Conditional Access policies.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + entra_client.conditional_access_policies = {} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for guest users." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + + def test_disabled_policy_is_skipped(self): + """Test FAIL when the only matching policy is disabled.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="MFA for Guests", + state=ConditionalAccessPolicyState.DISABLED, + included_users=["GuestsOrExternalUsers"], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for guest users." + ) + + def test_policy_enabled_targeting_all_users_with_mfa(self): + """Test PASS when an enabled policy targets all users with MFA for all apps.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="MFA for All Users", + state=ConditionalAccessPolicyState.ENABLED, + included_users=["All"], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Conditional Access Policy MFA for All Users requires MFA for guest users." + ) + assert result[0].resource_id == policy.id + assert result[0].resource_name == "MFA for All Users" + + def test_policy_enabled_targeting_guests_or_external_users(self): + """Test PASS when an enabled policy specifically targets GuestsOrExternalUsers.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="MFA for Guest Users", + state=ConditionalAccessPolicyState.ENABLED, + included_users=["GuestsOrExternalUsers"], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Conditional Access Policy MFA for Guest Users requires MFA for guest users." + ) + assert result[0].resource_id == policy.id + + def test_policy_enabled_targeting_all_guest_types_via_included_guests(self): + """Test PASS when policy targets all six guest types via included_guests_or_external_users.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="MFA for All Guest Types", + state=ConditionalAccessPolicyState.ENABLED, + built_in_controls=[ConditionalAccessGrantControl.MFA], + included_guests_or_external_users=GuestsOrExternalUsers( + guest_or_external_user_types=list(ALL_GUEST_USER_TYPES), + ), + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == policy.id + + def test_policy_with_authentication_strength_passes(self): + """Test PASS when policy uses authentication strength instead of MFA built-in control.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="Auth Strength for Guests", + state=ConditionalAccessPolicyState.ENABLED, + included_users=["GuestsOrExternalUsers"], + authentication_strength="Phishing-resistant MFA", + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == policy.id + + def test_policy_only_password_change_fails(self): + """Test FAIL when the policy only requires password change.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="Password Change for Guests", + state=ConditionalAccessPolicyState.ENABLED, + included_users=["GuestsOrExternalUsers"], + built_in_controls=[ConditionalAccessGrantControl.PASSWORD_CHANGE], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for guest users." + ) + + def test_policy_not_targeting_all_apps_fails(self): + """Test FAIL when the policy does not target all cloud applications.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="MFA for Guests Specific App", + state=ConditionalAccessPolicyState.ENABLED, + included_applications=["some-specific-app-id"], + included_users=["GuestsOrExternalUsers"], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for guest users." + ) + + def test_policy_not_targeting_guests_fails(self): + """Test FAIL when the policy does not target guest users.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="MFA for Specific Users", + state=ConditionalAccessPolicyState.ENABLED, + included_users=[str(uuid4())], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for guest users." + ) + + def test_policy_with_partial_guest_types_fails(self): + """Test FAIL when policy only targets some guest types but not all six.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="MFA for Some Guests", + state=ConditionalAccessPolicyState.ENABLED, + built_in_controls=[ConditionalAccessGrantControl.MFA], + included_guests_or_external_users=GuestsOrExternalUsers( + guest_or_external_user_types=[ + GuestOrExternalUserType.B2B_COLLABORATION_GUEST, + GuestOrExternalUserType.INTERNAL_GUEST, + ], + ), + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for guest users." + ) + + def test_policy_with_excluded_guest_types_fails(self): + """Test FAIL when the policy excludes guest/external user types.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="MFA for Guests with Exclusions", + state=ConditionalAccessPolicyState.ENABLED, + included_users=["All"], + built_in_controls=[ConditionalAccessGrantControl.MFA], + excluded_guests_or_external_users=GuestsOrExternalUsers( + guest_or_external_user_types=[ + GuestOrExternalUserType.SERVICE_PROVIDER, + ], + ), + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for guest users." + ) + + def test_policy_excluding_guests_via_excluded_users_fails(self): + """Test FAIL when the policy excludes GuestsOrExternalUsers.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="MFA for Users Except Guests", + state=ConditionalAccessPolicyState.ENABLED, + included_users=["All"], + excluded_users=["GuestsOrExternalUsers"], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for guest users." + ) + + def test_policy_with_selected_external_tenants_fails(self): + """Test FAIL when the policy only targets selected external tenants.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="MFA for Selected External Tenants", + state=ConditionalAccessPolicyState.ENABLED, + built_in_controls=[ConditionalAccessGrantControl.MFA], + included_guests_or_external_users=GuestsOrExternalUsers( + guest_or_external_user_types=list(ALL_GUEST_USER_TYPES), + external_tenants_membership_kind=ExternalTenantsMembershipKind.ENUMERATED, + ), + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for guest users." + ) + + def test_reporting_only_policy_fails_with_detail(self): + """Test FAIL with detail when the matching policy is in report-only mode.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="MFA for Guests Report Only", + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + included_users=["GuestsOrExternalUsers"], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Conditional Access Policy MFA for Guests Report Only targets guest users with MFA but is only in report-only mode." + ) + assert result[0].resource_id == policy.id + assert result[0].resource_name == "MFA for Guests Report Only" + + def test_no_application_conditions_fails(self): + """Test FAIL when the policy has no application conditions.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + policy_id = str(uuid4()) + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name="No App Conditions", + conditions=Conditions( + application_conditions=None, + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["GuestsOrExternalUsers"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.OR, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=None, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for guest users." + ) + + def test_no_mfa_grant_control_fails(self): + """Test FAIL when the policy does not require MFA as a grant control.""" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import ( + entra_conditional_access_policy_mfa_enforced_for_guest_users, + ) + + policy = build_policy( + display_name="Compliant Device for Guests", + state=ConditionalAccessPolicyState.ENABLED, + included_users=["GuestsOrExternalUsers"], + built_in_controls=[ConditionalAccessGrantControl.COMPLIANT_DEVICE], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_mfa_enforced_for_guest_users().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for guest users." + ) diff --git a/tests/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled_fixer_test.py b/tests/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled_fixer_test.py new file mode 100644 index 0000000000..672a5b1c73 --- /dev/null +++ b/tests/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled_fixer_test.py @@ -0,0 +1,61 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import set_mocked_m365_provider + + +class Test_exchange_organization_delicensing_resiliency_enabled_fixer: + def test_creates_new_powershell_session(self): + created_session = mock.MagicMock() + created_session.connect_exchange_online.return_value = True + created_session.execute.return_value = "" + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled_fixer.M365PowerShell", + return_value=created_session, + ) as mocked_powershell, + ): + from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled_fixer import ( + fixer, + ) + + assert fixer() + mocked_powershell.assert_called_once() + created_session.connect_exchange_online.assert_called_once() + created_session.execute.assert_any_call( + "Set-OrganizationConfig -DelayedDelicensingEnabled $true", + timeout=30, + ) + created_session.close.assert_called_once() + + def test_logs_power_shell_execution_error(self): + created_session = mock.MagicMock() + created_session.connect_exchange_online.return_value = True + created_session.execute.return_value = "Access is denied." + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled_fixer.M365PowerShell", + return_value=created_session, + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled_fixer.logger.error", + ) as mocked_logger_error, + ): + from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled_fixer import ( + fixer, + ) + + assert not fixer() + mocked_logger_error.assert_any_call( + 'PowerShell execution failed while running "Set-OrganizationConfig -DelayedDelicensingEnabled $true": Access is denied.' + ) + created_session.close.assert_called_once() diff --git a/tests/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled_test.py b/tests/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled_test.py new file mode 100644 index 0000000000..d9423afa4d --- /dev/null +++ b/tests/providers/m365/services/exchange/exchange_organization_delicensing_resiliency_enabled/exchange_organization_delicensing_resiliency_enabled_test.py @@ -0,0 +1,278 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + +ORGANIZATION_KWARGS = dict( + name="test-org", + guid="org-guid", + audit_disabled=False, + oauth_enabled=True, + mailtips_enabled=True, + mailtips_external_recipient_enabled=False, + mailtips_group_metrics_enabled=True, + mailtips_large_audience_threshold=25, +) + + +class Test_exchange_organization_delicensing_resiliency_enabled: + def test_no_organization(self): + exchange_client = mock.MagicMock() + exchange_client.audited_tenant = "audited_tenant" + exchange_client.audited_domain = DOMAIN + exchange_client.organization_config = None + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import ( + exchange_organization_delicensing_resiliency_enabled, + ) + + check = exchange_organization_delicensing_resiliency_enabled() + result = check.execute() + assert len(result) == 0 + + def test_delicensing_resiliency_disabled_above_threshold(self): + """Disabled + >= 5000 total licenses -> FAIL (fixer confirms eligibility).""" + exchange_client = mock.MagicMock() + exchange_client.audited_tenant = "audited_tenant" + exchange_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import ( + exchange_organization_delicensing_resiliency_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + Organization, + ) + + exchange_client.organization_config = Organization( + **ORGANIZATION_KWARGS, + delayed_delicensing_enabled=False, + total_paid_licenses=6000, + ) + + check = exchange_organization_delicensing_resiliency_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "preventive FAIL" in result[0].status_extended + assert result[0].resource_name == "test-org" + assert result[0].resource_id == "org-guid" + assert result[0].location == "global" + + def test_delicensing_resiliency_disabled_at_threshold(self): + """Disabled + exactly 5000 total licenses -> FAIL.""" + exchange_client = mock.MagicMock() + exchange_client.audited_tenant = "audited_tenant" + exchange_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import ( + exchange_organization_delicensing_resiliency_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + Organization, + ) + + exchange_client.organization_config = Organization( + **ORGANIZATION_KWARGS, + delayed_delicensing_enabled=False, + total_paid_licenses=5000, + ) + + check = exchange_organization_delicensing_resiliency_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_delicensing_resiliency_disabled_below_threshold(self): + """Disabled + < 5000 total licenses -> PASS (not applicable).""" + exchange_client = mock.MagicMock() + exchange_client.audited_tenant = "audited_tenant" + exchange_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import ( + exchange_organization_delicensing_resiliency_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + Organization, + ) + + exchange_client.organization_config = Organization( + **ORGANIZATION_KWARGS, + delayed_delicensing_enabled=False, + total_paid_licenses=4999, + ) + + check = exchange_organization_delicensing_resiliency_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert "not applicable" in result[0].status_extended + assert "4999 total licenses" in result[0].status_extended + + def test_delicensing_resiliency_disabled_licenses_unknown(self): + """Disabled + unknown license count -> FAIL.""" + exchange_client = mock.MagicMock() + exchange_client.audited_tenant = "audited_tenant" + exchange_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import ( + exchange_organization_delicensing_resiliency_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + Organization, + ) + + exchange_client.organization_config = Organization( + **ORGANIZATION_KWARGS, + delayed_delicensing_enabled=False, + total_paid_licenses=None, + ) + + check = exchange_organization_delicensing_resiliency_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "preventive FAIL" in result[0].status_extended + + def test_delicensing_resiliency_enabled(self): + """Enabled -> PASS regardless of license count.""" + exchange_client = mock.MagicMock() + exchange_client.audited_tenant = "audited_tenant" + exchange_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import ( + exchange_organization_delicensing_resiliency_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + Organization, + ) + + exchange_client.organization_config = Organization( + **ORGANIZATION_KWARGS, + delayed_delicensing_enabled=True, + total_paid_licenses=6000, + ) + + check = exchange_organization_delicensing_resiliency_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert "is enabled" in result[0].status_extended + assert result[0].resource == exchange_client.organization_config.dict() + assert result[0].resource_name == "test-org" + assert result[0].resource_id == "org-guid" + assert result[0].location == "global" + + def test_delicensing_resiliency_enabled_below_threshold(self): + """Enabled + below threshold -> still PASS (enabled always wins).""" + exchange_client = mock.MagicMock() + exchange_client.audited_tenant = "audited_tenant" + exchange_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import ( + exchange_organization_delicensing_resiliency_enabled, + ) + from prowler.providers.m365.services.exchange.exchange_service import ( + Organization, + ) + + exchange_client.organization_config = Organization( + **ORGANIZATION_KWARGS, + delayed_delicensing_enabled=True, + total_paid_licenses=100, + ) + + check = exchange_organization_delicensing_resiliency_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert "is enabled" in result[0].status_extended diff --git a/tests/providers/m365/services/exchange/exchange_service_test.py b/tests/providers/m365/services/exchange/exchange_service_test.py index 3f0e62cc3b..8eed77ca99 100644 --- a/tests/providers/m365/services/exchange/exchange_service_test.py +++ b/tests/providers/m365/services/exchange/exchange_service_test.py @@ -161,6 +161,18 @@ def mock_exchange_get_shared_mailboxes(_): ] +async def mock_exchange_get_total_paid_licenses(_): + return 6000 + + +async def mock_exchange_get_total_paid_licenses_none(_): + return None + + +@patch( + "prowler.providers.m365.services.exchange.exchange_service.Exchange._get_total_paid_licenses", + new=mock_exchange_get_total_paid_licenses, +) class Test_Exchange_Service: def test_get_client(self): with ( @@ -201,6 +213,7 @@ class Test_Exchange_Service: assert organization_config.mailtips_external_recipient_enabled is False assert organization_config.mailtips_group_metrics_enabled is True assert organization_config.mailtips_large_audience_threshold == 25 + assert organization_config.total_paid_licenses == 6000 exchange_client.powershell.close() @@ -481,3 +494,27 @@ class Test_Exchange_Service: assert shared_mailboxes[1].identity == "info@contoso.com" exchange_client.powershell.close() + + def test_get_total_paid_licenses_none(self): + with ( + mock.patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online" + ), + mock.patch.object( + Exchange, + "_get_organization_config", + mock_exchange_get_organization_config, + ), + mock.patch.object( + Exchange, + "_get_total_paid_licenses", + mock_exchange_get_total_paid_licenses_none, + ), + ): + exchange_client = Exchange( + set_mocked_m365_provider( + identity=M365IdentityInfo(tenant_domain=DOMAIN) + ) + ) + assert exchange_client.organization_config.total_paid_licenses is None + exchange_client.powershell.close() diff --git a/tests/providers/m365/services/intune/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default_test.py b/tests/providers/m365/services/intune/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default_test.py new file mode 100644 index 0000000000..3dc51076eb --- /dev/null +++ b/tests/providers/m365/services/intune/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default_test.py @@ -0,0 +1,163 @@ +from unittest import mock + +from prowler.providers.m365.services.intune.intune_service import IntuneSettings +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + +CHECK_MODULE_PATH = "prowler.providers.m365.services.intune.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default" + + +class Test_intune_device_compliance_policy_unassigned_devices_not_compliant_by_default: + def test_secure_by_default_true(self): + intune_client = mock.MagicMock() + intune_client.audited_tenant = "audited_tenant" + intune_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.intune_client", new=intune_client), + ): + from prowler.providers.m365.services.intune.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default import ( + intune_device_compliance_policy_unassigned_devices_not_compliant_by_default, + ) + + intune_client.settings = IntuneSettings(secure_by_default=True) + intune_client.verification_error = None + + result = ( + intune_device_compliance_policy_unassigned_devices_not_compliant_by_default().execute() + ) + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == ( + "Intune built-in Device Compliance Policy marks devices " + "with no compliance policy assigned as Not compliant." + ) + + def test_secure_by_default_false(self): + intune_client = mock.MagicMock() + intune_client.audited_tenant = "audited_tenant" + intune_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.intune_client", new=intune_client), + ): + from prowler.providers.m365.services.intune.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default import ( + intune_device_compliance_policy_unassigned_devices_not_compliant_by_default, + ) + + intune_client.settings = IntuneSettings(secure_by_default=False) + intune_client.verification_error = None + + result = ( + intune_device_compliance_policy_unassigned_devices_not_compliant_by_default().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == ( + "Intune built-in Device Compliance Policy marks devices " + "with no compliance policy assigned as Compliant. " + "Change the default to Not compliant in Intune settings." + ) + + def test_secure_by_default_none(self): + intune_client = mock.MagicMock() + intune_client.audited_tenant = "audited_tenant" + intune_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.intune_client", new=intune_client), + ): + from prowler.providers.m365.services.intune.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default import ( + intune_device_compliance_policy_unassigned_devices_not_compliant_by_default, + ) + + intune_client.settings = IntuneSettings(secure_by_default=None) + intune_client.verification_error = None + + result = ( + intune_device_compliance_policy_unassigned_devices_not_compliant_by_default().execute() + ) + + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert result[0].status_extended == ( + "Intune built-in Device Compliance Policy could not be verified " + "because Microsoft Graph did not return the secure-by-default " + "compliance setting." + ) + + def test_settings_is_none(self): + intune_client = mock.MagicMock() + intune_client.audited_tenant = "audited_tenant" + intune_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.intune_client", new=intune_client), + ): + from prowler.providers.m365.services.intune.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default import ( + intune_device_compliance_policy_unassigned_devices_not_compliant_by_default, + ) + + intune_client.settings = None + intune_client.verification_error = None + + result = ( + intune_device_compliance_policy_unassigned_devices_not_compliant_by_default().execute() + ) + + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert result[0].status_extended == ( + "Intune built-in Device Compliance Policy could not be verified " + "because Microsoft Graph did not return the secure-by-default " + "compliance setting." + ) + + def test_verification_error_returns_manual(self): + intune_client = mock.MagicMock() + intune_client.audited_tenant = "audited_tenant" + intune_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch(f"{CHECK_MODULE_PATH}.intune_client", new=intune_client), + ): + from prowler.providers.m365.services.intune.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default import ( + intune_device_compliance_policy_unassigned_devices_not_compliant_by_default, + ) + + intune_client.settings = None + intune_client.verification_error = ( + "Could not read Microsoft Intune device management settings." + ) + + result = ( + intune_device_compliance_policy_unassigned_devices_not_compliant_by_default().execute() + ) + + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert result[0].status_extended == ( + "Intune built-in Device Compliance Policy could not be verified. " + "Could not read Microsoft Intune device management settings." + ) diff --git a/tests/providers/m365/services/intune/intune_service_test.py b/tests/providers/m365/services/intune/intune_service_test.py index 3e6762391f..1e273d04e9 100644 --- a/tests/providers/m365/services/intune/intune_service_test.py +++ b/tests/providers/m365/services/intune/intune_service_test.py @@ -349,6 +349,60 @@ def test_intune_get_settings_null_settings(): assert settings.secure_by_default is None +def test_intune_get_settings_retries_without_select_when_settings_missing(): + """Test _get_settings retries without $select when settings are omitted.""" + intune = Intune.__new__(Intune) + + selected_response = SimpleNamespace(settings=None) + full_response = SimpleNamespace(settings=SimpleNamespace(secure_by_default=True)) + + mock_client = mock.MagicMock() + mock_client.device_management.get = AsyncMock( + side_effect=[selected_response, full_response] + ) + + intune.client = mock_client + + loop = asyncio.new_event_loop() + try: + settings, error = loop.run_until_complete(intune._get_settings()) + finally: + loop.close() + + assert error is None + assert settings is not None + assert settings.secure_by_default is True + assert mock_client.device_management.get.await_count == 2 + + +def test_intune_get_settings_retries_without_select_when_value_missing(): + """Test _get_settings retries without $select when secure_by_default is omitted.""" + intune = Intune.__new__(Intune) + + selected_response = SimpleNamespace( + settings=SimpleNamespace(secure_by_default=None) + ) + full_response = SimpleNamespace(settings=SimpleNamespace(secure_by_default=False)) + + mock_client = mock.MagicMock() + mock_client.device_management.get = AsyncMock( + side_effect=[selected_response, full_response] + ) + + intune.client = mock_client + + loop = asyncio.new_event_loop() + try: + settings, error = loop.run_until_complete(intune._get_settings()) + finally: + loop.close() + + assert error is None + assert settings is not None + assert settings.secure_by_default is False + assert mock_client.device_management.get.await_count == 2 + + def test_intune_get_settings_exception(): """Test _get_settings handles exceptions gracefully.""" intune = Intune.__new__(Intune) diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 941e7c5ff4..ded143758e 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -2,10 +2,26 @@ All notable changes to the **Prowler UI** are documented in this file. -## [1.23.0] (Prowler UNRELEASED) +## [1.24.0] (Prowler UNRELEASED) ### 🚀 Added +- Resources side drawer with redesigned detail panel [(#10673)](https://github.com/prowler-cloud/prowler/pull/10673) + +### 🐞 Fixed + +- Findings group resource filters now strip unsupported scan parameters, display scan name instead of provider alias in filter badges, migrate mute modal from HeroUI to shadcn, and add searchable accounts/provider type selectors [(#10662)](https://github.com/prowler-cloud/prowler/pull/10662) +- Compliance detail page header now reflects the actual provider, alias and UID of the selected scan instead of always defaulting to AWS [(#10674)](https://github.com/prowler-cloud/prowler/pull/10674) + +--- + +## [1.23.0] (Prowler v5.23.0) + +### 🚀 Added + +- Invitation accept smart router for handling invitation flow routing [(#10573)](https://github.com/prowler-cloud/prowler/pull/10573) +- Invitation link backward compatibility [(#10583)](https://github.com/prowler-cloud/prowler/pull/10583) +- Updated invitation link to use smart router [(#10575)](https://github.com/prowler-cloud/prowler/pull/10575) - Multi-tenant organization management: create, switch, edit, and delete organizations from the profile page [(#10491)](https://github.com/prowler-cloud/prowler/pull/10491) - Findings grouped view with drill-down table showing resources per check, resource detail drawer, infinite scroll pagination, and bulk mute support [(#10425)](https://github.com/prowler-cloud/prowler/pull/10425) - Resource events tool to Lighthouse AI [(#10412)](https://github.com/prowler-cloud/prowler/pull/10412) @@ -14,13 +30,20 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🔄 Changed - Attack Paths custom openCypher queries now use a code editor with syntax highlighting and line numbers [(#10445)](https://github.com/prowler-cloud/prowler/pull/10445) +- Attack Paths custom openCypher queries now link to the Prowler documentation with examples and how-to guidance instead of the upstream Cartography schema URL - Filter summary strip: removed redundant "Clear all" link next to pills (use top-bar Clear Filters instead) and switched chip variant from `outline` to `tag` for consistency [(#10481)](https://github.com/prowler-cloud/prowler/pull/10481) +### 🔐 Security + +- Bump `serialize-javascript` override from 7.0.3 to 7.0.5 to restore pnpm trusted publishing and patch CVE-2026-34043 [(#10653)](https://github.com/prowler-cloud/prowler/pull/10653) + ### 🐞 Fixed +- Preserve query parameters in callbackUrl during invitation flow [(#10571)](https://github.com/prowler-cloud/prowler/pull/10571) - Deleting the active organization now switches to the target org before deleting, preventing JWT rejection from the backend [(#10491)](https://github.com/prowler-cloud/prowler/pull/10491) - Clear Filters now resets all filters including muted findings and auto-applies, Clear all in pills only removes pill-visible sub-filters, and the discard icon is now an Undo text button [(#10446)](https://github.com/prowler-cloud/prowler/pull/10446) - Send to Jira modal now dynamically fetches and displays available issue types per project instead of hardcoding `"Task"`, fixing failures on non-English Jira instances [(#10534)](https://github.com/prowler-cloud/prowler/pull/10534) +- Exclude service filter from finding group resources endpoint to prevent empty results when a service filter is active [(#10652)](https://github.com/prowler-cloud/prowler/pull/10652) --- diff --git a/ui/README.md b/ui/README.md index e5d76ff1e3..148ef61d5b 100644 --- a/ui/README.md +++ b/ui/README.md @@ -142,12 +142,12 @@ Or remove the variable from your `.env` file. ### Troubleshooting -If hooks aren't running after commits: +If hooks aren't running after commits, verify prek is installed and hooks are set up: ```bash -# Verify hooks are configured -git config --get core.hooksPath # Should output: ui/.husky +# Check prek is available +prek --version -# Reconfigure if needed -git config core.hooksPath "ui/.husky" +# Re-install hooks if needed +prek install --overwrite ``` diff --git a/ui/actions/attack-paths/queries.adapter.test.ts b/ui/actions/attack-paths/queries.adapter.test.ts index cd6bafd206..77298991f3 100644 --- a/ui/actions/attack-paths/queries.adapter.test.ts +++ b/ui/actions/attack-paths/queries.adapter.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; +import { DOCS_URLS } from "@/lib/external-urls"; import { ATTACK_PATH_QUERY_IDS, - type AttackPathCartographySchemaAttributes, type AttackPathQuery, } from "@/types/attack-paths"; @@ -22,20 +22,9 @@ const presetQuery: AttackPathQuery = { }; describe("buildAttackPathQueries", () => { - it("prepends a custom query with a schema documentation link", () => { - // Given - const schema: AttackPathCartographySchemaAttributes = { - id: "aws-0.129.0", - provider: "aws", - cartography_version: "0.129.0", - schema_url: - "https://github.com/cartography-cncf/cartography/blob/0.129.0/docs/root/modules/aws/schema.md", - raw_schema_url: - "https://raw.githubusercontent.com/cartography-cncf/cartography/refs/tags/0.129.0/docs/root/modules/aws/schema.md", - }; - + it("prepends a custom query that links to the Prowler documentation", () => { // When - const result = buildAttackPathQueries([presetQuery], schema); + const result = buildAttackPathQueries([presetQuery]); // Then expect(result[0]).toMatchObject({ @@ -44,8 +33,8 @@ describe("buildAttackPathQueries", () => { name: "Custom openCypher query", short_description: "Write and run your own read-only query", documentation_link: { - text: "Cartography schema used by Prowler for AWS graphs", - link: schema.schema_url, + text: "Learn how to write custom openCypher queries", + link: DOCS_URLS.ATTACK_PATHS_CUSTOM_QUERIES, }, }, }); diff --git a/ui/actions/attack-paths/queries.adapter.ts b/ui/actions/attack-paths/queries.adapter.ts index 0b3120bb0a..b4635ed333 100644 --- a/ui/actions/attack-paths/queries.adapter.ts +++ b/ui/actions/attack-paths/queries.adapter.ts @@ -1,7 +1,7 @@ +import { DOCS_URLS } from "@/lib/external-urls"; import { MetaDataProps } from "@/types"; import { ATTACK_PATH_QUERY_IDS, - type AttackPathCartographySchemaAttributes, AttackPathQueriesResponse, AttackPathQuery, QUERY_PARAMETER_INPUT_TYPES, @@ -61,15 +61,12 @@ const CUSTOM_QUERY_PLACEHOLDER = `MATCH (n) RETURN n LIMIT 25`; -const formatSchemaDocumentationLinkText = ( - schema: AttackPathCartographySchemaAttributes, -): string => { - return `Cartography schema used by Prowler for ${schema.provider.toUpperCase()} graphs`; -}; +const CUSTOM_QUERY_DOCUMENTATION_LINK = { + text: "Learn how to write custom openCypher queries", + link: DOCS_URLS.ATTACK_PATHS_CUSTOM_QUERIES, +} as const; -const createCustomQuery = ( - schema?: AttackPathCartographySchemaAttributes, -): AttackPathQuery => ({ +const createCustomQuery = (): AttackPathQuery => ({ type: "attack-paths-scans", id: ATTACK_PATH_QUERY_IDS.CUSTOM, attributes: { @@ -79,12 +76,7 @@ const createCustomQuery = ( "Run a read-only openCypher query against the selected Attack Paths scan. Results are automatically scoped to the selected provider.", provider: "custom", attribution: null, - documentation_link: schema - ? { - text: formatSchemaDocumentationLinkText(schema), - link: schema.schema_url, - } - : null, + documentation_link: { ...CUSTOM_QUERY_DOCUMENTATION_LINK }, parameters: [ { name: "query", @@ -103,7 +95,6 @@ const createCustomQuery = ( export const buildAttackPathQueries = ( queries: AttackPathQuery[], - schema?: AttackPathCartographySchemaAttributes, ): AttackPathQuery[] => { - return [createCustomQuery(schema), ...queries]; + return [createCustomQuery(), ...queries]; }; diff --git a/ui/actions/finding-groups/finding-groups.adapter.test.ts b/ui/actions/finding-groups/finding-groups.adapter.test.ts index 5874ddf7e5..c9b4a0314c 100644 --- a/ui/actions/finding-groups/finding-groups.adapter.test.ts +++ b/ui/actions/finding-groups/finding-groups.adapter.test.ts @@ -78,14 +78,31 @@ describe("adaptFindingGroupsResponse — malformed input", () => { check_description: null, severity: "critical", status: "FAIL", + muted: true, impacted_providers: ["aws"], resources_total: 5, resources_fail: 3, pass_count: 2, fail_count: 3, + manual_count: 1, + pass_muted_count: 0, + fail_muted_count: 3, + manual_muted_count: 0, muted_count: 0, new_count: 1, changed_count: 0, + new_fail_count: 0, + new_fail_muted_count: 1, + new_pass_count: 0, + new_pass_muted_count: 0, + new_manual_count: 0, + new_manual_muted_count: 0, + changed_fail_count: 0, + changed_fail_muted_count: 0, + changed_pass_count: 0, + changed_pass_muted_count: 0, + changed_manual_count: 0, + changed_manual_muted_count: 0, first_seen_at: null, last_seen_at: "2024-01-01T00:00:00Z", failing_since: null, @@ -101,6 +118,9 @@ describe("adaptFindingGroupsResponse — malformed input", () => { expect(result).toHaveLength(1); expect(result[0].checkId).toBe("s3_bucket_public_access"); expect(result[0].checkTitle).toBe("S3 Bucket Public Access"); + expect(result[0].muted).toBe(true); + expect(result[0].manualCount).toBe(1); + expect(result[0].newFailMutedCount).toBe(1); }); }); @@ -149,6 +169,7 @@ describe("adaptFindingGroupResourcesResponse — malformed input", () => { id: "resource-row-1", type: "finding-group-resources", attributes: { + finding_id: "real-finding-uuid", resource: { uid: "arn:aws:s3:::my-bucket", name: "my-bucket", @@ -163,6 +184,8 @@ describe("adaptFindingGroupResourcesResponse — malformed input", () => { alias: "production", }, status: "FAIL", + muted: true, + delta: "new", severity: "critical", first_seen_at: null, last_seen_at: "2024-01-01T00:00:00Z", @@ -176,7 +199,10 @@ describe("adaptFindingGroupResourcesResponse — malformed input", () => { // Then expect(result).toHaveLength(1); + expect(result[0].findingId).toBe("real-finding-uuid"); expect(result[0].checkId).toBe("s3_check"); expect(result[0].resourceName).toBe("my-bucket"); + expect(result[0].delta).toBe("new"); + expect(result[0].isMuted).toBe(true); }); }); diff --git a/ui/actions/finding-groups/finding-groups.adapter.ts b/ui/actions/finding-groups/finding-groups.adapter.ts index 593171078d..7f260c8aa6 100644 --- a/ui/actions/finding-groups/finding-groups.adapter.ts +++ b/ui/actions/finding-groups/finding-groups.adapter.ts @@ -19,15 +19,32 @@ interface FindingGroupAttributes { check_title: string | null; check_description: string | null; severity: string; - status: string; // "FAIL" | "PASS" | "MUTED" (already uppercase) + status: string; // "FAIL" | "PASS" | "MANUAL" (already uppercase) + muted?: boolean; impacted_providers: string[]; resources_total: number; resources_fail: number; pass_count: number; fail_count: number; + manual_count?: number; + pass_muted_count?: number; + fail_muted_count?: number; + manual_muted_count?: number; muted_count: number; new_count: number; changed_count: number; + new_fail_count?: number; + new_fail_muted_count?: number; + new_pass_count?: number; + new_pass_muted_count?: number; + new_manual_count?: number; + new_manual_muted_count?: number; + changed_fail_count?: number; + changed_fail_muted_count?: number; + changed_pass_count?: number; + changed_pass_muted_count?: number; + changed_manual_count?: number; + changed_manual_muted_count?: number; first_seen_at: string | null; last_seen_at: string | null; failing_since: string | null; @@ -62,10 +79,33 @@ export function adaptFindingGroupsResponse( checkTitle: item.attributes.check_title || item.attributes.check_id, severity: item.attributes.severity as Severity, status: item.attributes.status as FindingStatus, + muted: + item.attributes.muted ?? + (item.attributes.muted_count > 0 && + (item.attributes.muted_count === item.attributes.resources_fail || + item.attributes.muted_count === item.attributes.resources_total)), resourcesTotal: item.attributes.resources_total, resourcesFail: item.attributes.resources_fail, + passCount: item.attributes.pass_count, + failCount: item.attributes.fail_count, + manualCount: item.attributes.manual_count ?? 0, + passMutedCount: item.attributes.pass_muted_count ?? 0, + failMutedCount: item.attributes.fail_muted_count ?? 0, + manualMutedCount: item.attributes.manual_muted_count ?? 0, newCount: item.attributes.new_count, changedCount: item.attributes.changed_count, + newFailCount: item.attributes.new_fail_count ?? 0, + newFailMutedCount: item.attributes.new_fail_muted_count ?? 0, + newPassCount: item.attributes.new_pass_count ?? 0, + newPassMutedCount: item.attributes.new_pass_muted_count ?? 0, + newManualCount: item.attributes.new_manual_count ?? 0, + newManualMutedCount: item.attributes.new_manual_muted_count ?? 0, + changedFailCount: item.attributes.changed_fail_count ?? 0, + changedFailMutedCount: item.attributes.changed_fail_muted_count ?? 0, + changedPassCount: item.attributes.changed_pass_count ?? 0, + changedPassMutedCount: item.attributes.changed_pass_muted_count ?? 0, + changedManualCount: item.attributes.changed_manual_count ?? 0, + changedManualMutedCount: item.attributes.changed_manual_muted_count ?? 0, mutedCount: item.attributes.muted_count, providers: (item.attributes.impacted_providers || []) as ProviderType[], updatedAt: item.attributes.last_seen_at || "", @@ -95,9 +135,12 @@ interface ProviderInfo { } interface FindingGroupResourceAttributes { + finding_id: string; resource: ResourceInfo; provider: ProviderInfo; status: string; + muted?: boolean; + delta?: string | null; severity: string; first_seen_at: string | null; last_seen_at: string | null; @@ -131,20 +174,21 @@ export function adaptFindingGroupResourcesResponse( return data.map((item) => ({ id: item.id, rowType: FINDINGS_ROW_TYPE.RESOURCE, - findingId: item.id, + findingId: item.attributes.finding_id || item.id, checkId, providerType: (item.attributes.provider?.type || "aws") as ProviderType, providerAlias: item.attributes.provider?.alias || "", providerUid: item.attributes.provider?.uid || "", resourceName: item.attributes.resource?.name || "-", + resourceType: item.attributes.resource?.type || "-", resourceGroup: item.attributes.resource?.resource_group || "-", resourceUid: item.attributes.resource?.uid || "-", service: item.attributes.resource?.service || "-", region: item.attributes.resource?.region || "-", severity: (item.attributes.severity || "informational") as Severity, status: item.attributes.status, - isMuted: item.attributes.status === "MUTED", - // TODO: remove fallback once the API returns muted_reason in finding-group-resources + delta: item.attributes.delta || null, + isMuted: item.attributes.muted ?? item.attributes.status === "MUTED", mutedReason: item.attributes.muted_reason || undefined, firstSeenAt: item.attributes.first_seen_at, lastSeenAt: item.attributes.last_seen_at, diff --git a/ui/actions/finding-groups/finding-groups.test.ts b/ui/actions/finding-groups/finding-groups.test.ts index 0d8c2df53a..f71fe989fb 100644 --- a/ui/actions/finding-groups/finding-groups.test.ts +++ b/ui/actions/finding-groups/finding-groups.test.ts @@ -44,17 +44,87 @@ vi.mock("next/navigation", () => ({ import { getFindingGroupResources, + getFindingGroups, getLatestFindingGroupResources, + getLatestFindingGroups, } from "./finding-groups"; -// --------------------------------------------------------------------------- -// Blocker 1 + 2: FAIL-first sort and FAIL-only filter for drill-down resources -// --------------------------------------------------------------------------- - // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- +describe("getFindingGroups — default sort for muted and non-muted rows", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal("fetch", fetchMock); + getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" }); + handleApiResponseMock.mockResolvedValue({ data: [] }); + fetchMock.mockResolvedValue(new Response("", { status: 200 })); + }); + + it("should prefer non-muted fail counters when include muted is not active", async () => { + // When + await getFindingGroups(); + + // Then + const calledUrl = fetchMock.mock.calls[0][0] as string; + const url = new URL(calledUrl); + expect(url.searchParams.get("sort")).toBe( + "-status,-new_fail_count,-changed_fail_count,-severity,-fail_count,-last_seen_at", + ); + }); + + it("should include muted counters when filter[muted]=include is active", async () => { + // When + await getFindingGroups({ + filters: { "filter[muted]": "include" }, + }); + + // Then + const calledUrl = fetchMock.mock.calls[0][0] as string; + const url = new URL(calledUrl); + expect(url.searchParams.get("sort")).toBe( + "-status,-new_fail_count,-changed_fail_count,-severity,-new_fail_muted_count,-changed_fail_muted_count,-fail_count,-fail_muted_count,-last_seen_at", + ); + }); +}); + +describe("getLatestFindingGroups — default sort for muted and non-muted rows", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal("fetch", fetchMock); + getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" }); + handleApiResponseMock.mockResolvedValue({ data: [] }); + fetchMock.mockResolvedValue(new Response("", { status: 200 })); + }); + + it("should prefer non-muted fail counters when include muted is not active", async () => { + // When + await getLatestFindingGroups(); + + // Then + const calledUrl = fetchMock.mock.calls[0][0] as string; + const url = new URL(calledUrl); + expect(url.searchParams.get("sort")).toBe( + "-status,-new_fail_count,-changed_fail_count,-severity,-fail_count,-last_seen_at", + ); + }); + + it("should include muted counters when filter[muted]=include is active", async () => { + // When + await getLatestFindingGroups({ + filters: { "filter[muted]": "include" }, + }); + + // Then + const calledUrl = fetchMock.mock.calls[0][0] as string; + const url = new URL(calledUrl); + expect(url.searchParams.get("sort")).toBe( + "-status,-new_fail_count,-changed_fail_count,-severity,-new_fail_muted_count,-changed_fail_muted_count,-fail_count,-fail_muted_count,-last_seen_at", + ); + }); +}); + describe("getFindingGroupResources — SSRF path traversal protection", () => { beforeEach(() => { vi.clearAllMocks(); @@ -169,7 +239,7 @@ describe("getLatestFindingGroupResources — SSRF path traversal protection", () }); // --------------------------------------------------------------------------- -// Blocker 1: Resources list must show FAIL first (sort=-status) +// Resources list keeps FAIL-first sort but no longer forces FAIL-only filtering // --------------------------------------------------------------------------- describe("getFindingGroupResources — Blocker 1: FAIL-first sort", () => { @@ -181,30 +251,32 @@ describe("getFindingGroupResources — Blocker 1: FAIL-first sort", () => { fetchMock.mockResolvedValue(new Response("", { status: 200 })); }); - it("should include sort=-status in the API call so FAIL resources appear first", async () => { + it("should include the composite sort so FAIL resources appear first, then severity", async () => { // Given const checkId = "s3_bucket_public_access"; // When await getFindingGroupResources({ checkId }); - // Then — the URL must contain sort=-status + // Then — the URL must contain the composite sort const calledUrl = fetchMock.mock.calls[0][0] as string; const url = new URL(calledUrl); - expect(url.searchParams.get("sort")).toBe("-status"); + expect(url.searchParams.get("sort")).toBe( + "-status,-delta,-severity,-last_seen_at", + ); }); - it("should include filter[status]=FAIL in the API call so only impacted resources are shown", async () => { + it("should not force filter[status]=FAIL so PASS resources can also be shown", async () => { // Given const checkId = "s3_bucket_public_access"; // When await getFindingGroupResources({ checkId }); - // Then — the URL must contain filter[status]=FAIL + // Then — the URL should not add a hardcoded status filter const calledUrl = fetchMock.mock.calls[0][0] as string; const url = new URL(calledUrl); - expect(url.searchParams.get("filter[status]")).toBe("FAIL"); + expect(url.searchParams.get("filter[status]")).toBeNull(); }); }); @@ -217,7 +289,7 @@ describe("getLatestFindingGroupResources — Blocker 1: FAIL-first sort", () => fetchMock.mockResolvedValue(new Response("", { status: 200 })); }); - it("should include sort=-status in the API call so FAIL resources appear first", async () => { + it("should include the composite sort so FAIL resources appear first, then severity", async () => { // Given const checkId = "iam_user_mfa_enabled"; @@ -227,10 +299,12 @@ describe("getLatestFindingGroupResources — Blocker 1: FAIL-first sort", () => // Then const calledUrl = fetchMock.mock.calls[0][0] as string; const url = new URL(calledUrl); - expect(url.searchParams.get("sort")).toBe("-status"); + expect(url.searchParams.get("sort")).toBe( + "-status,-delta,-severity,-last_seen_at", + ); }); - it("should include filter[status]=FAIL in the API call so only impacted resources are shown", async () => { + it("should not force filter[status]=FAIL so PASS resources can also be shown", async () => { // Given const checkId = "iam_user_mfa_enabled"; @@ -240,7 +314,7 @@ describe("getLatestFindingGroupResources — Blocker 1: FAIL-first sort", () => // Then const calledUrl = fetchMock.mock.calls[0][0] as string; const url = new URL(calledUrl); - expect(url.searchParams.get("filter[status]")).toBe("FAIL"); + expect(url.searchParams.get("filter[status]")).toBeNull(); }); }); @@ -257,7 +331,7 @@ describe("getFindingGroupResources — triangulation: params coexist", () => { fetchMock.mockResolvedValue(new Response("", { status: 200 })); }); - it("should send sort=-status AND filter[status]=FAIL alongside pagination params", async () => { + it("should send the composite sort alongside pagination params without forcing filter[status]", async () => { // Given const checkId = "s3_bucket_versioning"; @@ -269,8 +343,10 @@ describe("getFindingGroupResources — triangulation: params coexist", () => { const url = new URL(calledUrl); expect(url.searchParams.get("page[number]")).toBe("2"); expect(url.searchParams.get("page[size]")).toBe("50"); - expect(url.searchParams.get("sort")).toBe("-status"); - expect(url.searchParams.get("filter[status]")).toBe("FAIL"); + expect(url.searchParams.get("sort")).toBe( + "-status,-delta,-severity,-last_seen_at", + ); + expect(url.searchParams.get("filter[status]")).toBeNull(); }); }); @@ -283,7 +359,7 @@ describe("getLatestFindingGroupResources — triangulation: params coexist", () fetchMock.mockResolvedValue(new Response("", { status: 200 })); }); - it("should send sort=-status AND filter[status]=FAIL alongside pagination params", async () => { + it("should send the composite sort alongside pagination params without forcing filter[status]", async () => { // Given const checkId = "iam_root_mfa_enabled"; @@ -295,16 +371,18 @@ describe("getLatestFindingGroupResources — triangulation: params coexist", () const url = new URL(calledUrl); expect(url.searchParams.get("page[number]")).toBe("3"); expect(url.searchParams.get("page[size]")).toBe("20"); - expect(url.searchParams.get("sort")).toBe("-status"); - expect(url.searchParams.get("filter[status]")).toBe("FAIL"); + expect(url.searchParams.get("sort")).toBe( + "-status,-delta,-severity,-last_seen_at", + ); + expect(url.searchParams.get("filter[status]")).toBeNull(); }); }); // --------------------------------------------------------------------------- -// Blocker: Duplicate filter[status] — caller-supplied status must be stripped +// Caller filters should propagate unchanged to the drill-down resources endpoint // --------------------------------------------------------------------------- -describe("getFindingGroupResources — Blocker: caller filter[status] is always overridden to FAIL", () => { +describe("getFindingGroupResources — caller filters are preserved", () => { beforeEach(() => { vi.clearAllMocks(); vi.stubGlobal("fetch", fetchMock); @@ -313,23 +391,7 @@ describe("getFindingGroupResources — Blocker: caller filter[status] is always fetchMock.mockResolvedValue(new Response("", { status: 200 })); }); - it("should use filter[status]=FAIL even when caller passes filter[status]=PASS", async () => { - // Given — caller explicitly passes PASS, which must be ignored - const checkId = "s3_bucket_public_access"; - const filters = { "filter[status]": "PASS" }; - - // When - await getFindingGroupResources({ checkId, filters }); - - // Then — the final URL must have exactly one filter[status]=FAIL, not PASS - const calledUrl = fetchMock.mock.calls[0][0] as string; - const url = new URL(calledUrl); - const allStatusValues = url.searchParams.getAll("filter[status]"); - expect(allStatusValues).toHaveLength(1); - expect(allStatusValues[0]).toBe("FAIL"); - }); - - it("should not have duplicate filter[status] params when caller passes filter[status]", async () => { + it("should preserve caller filter[status] when explicitly provided", async () => { // Given const checkId = "s3_bucket_public_access"; const filters = { "filter[status]": "PASS" }; @@ -337,14 +399,80 @@ describe("getFindingGroupResources — Blocker: caller filter[status] is always // When await getFindingGroupResources({ checkId, filters }); - // Then — no duplicates + // Then const calledUrl = fetchMock.mock.calls[0][0] as string; const url = new URL(calledUrl); - expect(url.searchParams.getAll("filter[status]")).toHaveLength(1); + const allStatusValues = url.searchParams.getAll("filter[status]"); + expect(allStatusValues).toHaveLength(1); + expect(allStatusValues[0]).toBe("PASS"); + }); + + it("should translate a single group status__in filter into filter[status] for resources", async () => { + // Given + const checkId = "s3_bucket_public_access"; + const filters = { + "filter[status__in]": "PASS", + "filter[severity__in]": "medium", + "filter[provider_type__in]": "aws", + }; + + // When + await getFindingGroupResources({ checkId, filters }); + + // Then + const calledUrl = fetchMock.mock.calls[0][0] as string; + const url = new URL(calledUrl); + expect(url.searchParams.get("filter[status]")).toBe("PASS"); + expect(url.searchParams.get("filter[status__in]")).toBeNull(); + expect(url.searchParams.get("filter[severity__in]")).toBe("medium"); + expect(url.searchParams.get("filter[provider_type__in]")).toBe("aws"); + }); + + it("should keep the composite sort when the resource search filter is applied", async () => { + // Given + const checkId = "s3_bucket_public_access"; + const filters = { + "filter[name__icontains]": "bucket-prod", + "filter[severity__in]": "high", + }; + + // When + await getFindingGroupResources({ checkId, filters }); + + // Then + const calledUrl = fetchMock.mock.calls[0][0] as string; + const url = new URL(calledUrl); + expect(url.searchParams.get("sort")).toBe( + "-status,-delta,-severity,-last_seen_at", + ); + expect(url.searchParams.get("filter[name__icontains]")).toBe("bucket-prod"); + expect(url.searchParams.get("filter[severity__in]")).toBe("high"); + }); + + it("should strip scan filters that the group resources endpoint does not accept", async () => { + // Given + const checkId = "s3_bucket_public_access"; + const filters = { + "filter[scan__in]": "scan-1", + "filter[scan_id]": "scan-1", + "filter[scan_id__in]": "scan-1,scan-2", + "filter[region__in]": "eu-west-1", + }; + + // When + await getFindingGroupResources({ checkId, filters }); + + // Then + const calledUrl = fetchMock.mock.calls[0][0] as string; + const url = new URL(calledUrl); + expect(url.searchParams.get("filter[scan__in]")).toBeNull(); + expect(url.searchParams.get("filter[scan_id]")).toBeNull(); + expect(url.searchParams.get("filter[scan_id__in]")).toBeNull(); + expect(url.searchParams.get("filter[region__in]")).toBe("eu-west-1"); }); }); -describe("getLatestFindingGroupResources — Blocker: caller filter[status] is always overridden to FAIL", () => { +describe("getLatestFindingGroupResources — caller filters are preserved", () => { beforeEach(() => { vi.clearAllMocks(); vi.stubGlobal("fetch", fetchMock); @@ -353,23 +481,7 @@ describe("getLatestFindingGroupResources — Blocker: caller filter[status] is a fetchMock.mockResolvedValue(new Response("", { status: 200 })); }); - it("should use filter[status]=FAIL even when caller passes filter[status]=PASS", async () => { - // Given — caller explicitly passes PASS, which must be ignored - const checkId = "iam_user_mfa_enabled"; - const filters = { "filter[status]": "PASS" }; - - // When - await getLatestFindingGroupResources({ checkId, filters }); - - // Then — the final URL must have exactly one filter[status]=FAIL, not PASS - const calledUrl = fetchMock.mock.calls[0][0] as string; - const url = new URL(calledUrl); - const allStatusValues = url.searchParams.getAll("filter[status]"); - expect(allStatusValues).toHaveLength(1); - expect(allStatusValues[0]).toBe("FAIL"); - }); - - it("should not have duplicate filter[status] params when caller passes filter[status]", async () => { + it("should preserve caller filter[status] when explicitly provided", async () => { // Given const checkId = "iam_user_mfa_enabled"; const filters = { "filter[status]": "PASS" }; @@ -377,9 +489,75 @@ describe("getLatestFindingGroupResources — Blocker: caller filter[status] is a // When await getLatestFindingGroupResources({ checkId, filters }); - // Then — no duplicates + // Then const calledUrl = fetchMock.mock.calls[0][0] as string; const url = new URL(calledUrl); - expect(url.searchParams.getAll("filter[status]")).toHaveLength(1); + const allStatusValues = url.searchParams.getAll("filter[status]"); + expect(allStatusValues).toHaveLength(1); + expect(allStatusValues[0]).toBe("PASS"); + }); + + it("should translate a single group status__in filter into filter[status] for latest resources", async () => { + // Given + const checkId = "iam_user_mfa_enabled"; + const filters = { + "filter[status__in]": "PASS", + "filter[severity__in]": "low", + "filter[provider_type__in]": "aws", + }; + + // When + await getLatestFindingGroupResources({ checkId, filters }); + + // Then + const calledUrl = fetchMock.mock.calls[0][0] as string; + const url = new URL(calledUrl); + expect(url.searchParams.get("filter[status]")).toBe("PASS"); + expect(url.searchParams.get("filter[status__in]")).toBeNull(); + expect(url.searchParams.get("filter[severity__in]")).toBe("low"); + expect(url.searchParams.get("filter[provider_type__in]")).toBe("aws"); + }); + + it("should keep the composite sort when the resource search filter is applied", async () => { + // Given + const checkId = "iam_user_mfa_enabled"; + const filters = { + "filter[name__icontains]": "instance-prod", + "filter[status__in]": "PASS,FAIL", + }; + + // When + await getLatestFindingGroupResources({ checkId, filters }); + + // Then + const calledUrl = fetchMock.mock.calls[0][0] as string; + const url = new URL(calledUrl); + expect(url.searchParams.get("sort")).toBe( + "-status,-delta,-severity,-last_seen_at", + ); + expect(url.searchParams.get("filter[name__icontains]")).toBe( + "instance-prod", + ); + expect(url.searchParams.get("filter[status__in]")).toBe("PASS,FAIL"); + }); + + it("should strip scan filters before calling latest group resources", async () => { + // Given + const checkId = "iam_user_mfa_enabled"; + const filters = { + "filter[scan__in]": "scan-1", + "filter[scan_id]": "scan-1", + "filter[region__in]": "us-east-1", + }; + + // When + await getLatestFindingGroupResources({ checkId, filters }); + + // Then + const calledUrl = fetchMock.mock.calls[0][0] as string; + const url = new URL(calledUrl); + expect(url.searchParams.get("filter[scan__in]")).toBeNull(); + expect(url.searchParams.get("filter[scan_id]")).toBeNull(); + expect(url.searchParams.get("filter[region__in]")).toBe("us-east-1"); }); }); diff --git a/ui/actions/finding-groups/finding-groups.ts b/ui/actions/finding-groups/finding-groups.ts index b31d7a5bfc..c409d804b7 100644 --- a/ui/actions/finding-groups/finding-groups.ts +++ b/ui/actions/finding-groups/finding-groups.ts @@ -5,6 +5,7 @@ import { redirect } from "next/navigation"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; import { appendSanitizedProviderFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; +import { FilterParam } from "@/types/filters"; /** * Maps filter[search] to filter[check_title__icontains] for finding-groups. @@ -23,21 +24,114 @@ function mapSearchFilter( return mapped; } -export const getFindingGroups = async ({ - page = 1, - pageSize = 10, - sort = "", - filters = {}, -}) => { +function splitCsvFilterValues(value: string | string[] | undefined): string[] { + if (Array.isArray(value)) { + return value + .flatMap((item) => item.split(",")) + .map((item) => item.trim()) + .filter(Boolean); + } + + if (typeof value === "string") { + return value + .split(",") + .map((item) => item.trim()) + .filter(Boolean); + } + + return []; +} + +/** + * Filters that belong to finding-groups but are NOT valid for the + * finding-group resources sub-endpoint. These must be stripped before + * calling the resources API to avoid empty results. + */ +const FINDING_GROUP_RESOURCE_UNSUPPORTED_FILTERS: FilterParam[] = [ + "filter[service__in]", + "filter[scan__in]", + "filter[scan_id]", + "filter[scan_id__in]", +]; + +function normalizeFindingGroupResourceFilters( + filters: Record, +): Record { + const normalized = Object.fromEntries( + Object.entries(filters).filter( + ([key]) => + !FINDING_GROUP_RESOURCE_UNSUPPORTED_FILTERS.includes( + key as FilterParam, + ), + ), + ); + + const exactStatusFilter = normalized["filter[status]"]; + + if (exactStatusFilter !== undefined) { + delete normalized["filter[status__in]"]; + return normalized; + } + + const statusValues = splitCsvFilterValues(normalized["filter[status__in]"]); + if (statusValues.length === 1) { + normalized["filter[status]"] = statusValues[0]; + delete normalized["filter[status__in]"]; + } + + return normalized; +} + +const DEFAULT_FINDING_GROUPS_SORT = + "-status,-new_fail_count,-changed_fail_count,-severity,-fail_count,-last_seen_at"; + +const DEFAULT_FINDING_GROUPS_SORT_WITH_MUTED = + "-status,-new_fail_count,-changed_fail_count,-severity,-new_fail_muted_count,-changed_fail_muted_count,-fail_count,-fail_muted_count,-last_seen_at"; + +const DEFAULT_FINDING_GROUP_RESOURCES_SORT = + "-status,-delta,-severity,-last_seen_at"; + +interface FetchFindingGroupsParams { + page?: number; + pageSize?: number; + sort?: string; + filters?: Record; +} + +function includesMutedFindings( + filters: Record, +): boolean { + const mutedFilter = filters["filter[muted]"]; + + if (Array.isArray(mutedFilter)) { + return mutedFilter.includes("include"); + } + + return mutedFilter === "include"; +} + +function getDefaultFindingGroupsSort( + filters: Record, +): string { + return includesMutedFindings(filters) + ? DEFAULT_FINDING_GROUPS_SORT_WITH_MUTED + : DEFAULT_FINDING_GROUPS_SORT; +} + +async function fetchFindingGroupsEndpoint( + endpoint: string, + { page = 1, pageSize = 10, sort, filters = {} }: FetchFindingGroupsParams, +) { const headers = await getAuthHeaders({ contentType: false }); + const resolvedSort = sort ?? getDefaultFindingGroupsSort(filters); if (isNaN(Number(page)) || page < 1) redirect("/findings"); - const url = new URL(`${apiBaseUrl}/finding-groups`); + const url = new URL(`${apiBaseUrl}/${endpoint}`); if (page) url.searchParams.append("page[number]", page.toString()); if (pageSize) url.searchParams.append("page[size]", pageSize.toString()); - if (sort) url.searchParams.append("sort", sort); + if (resolvedSort) url.searchParams.append("sort", resolvedSort); appendSanitizedProviderFilters(url, mapSearchFilter(filters)); @@ -45,120 +139,60 @@ export const getFindingGroups = async ({ const response = await fetch(url.toString(), { headers }); return handleApiResponse(response); } catch (error) { - console.error("Error fetching finding groups:", error); + console.error(`Error fetching ${endpoint}:`, error); return undefined; } -}; +} -export const getLatestFindingGroups = async ({ - page = 1, - pageSize = 10, - sort = "", - filters = {}, -}) => { +export const getFindingGroups = async (params: FetchFindingGroupsParams = {}) => + fetchFindingGroupsEndpoint("finding-groups", params); + +export const getLatestFindingGroups = async ( + params: FetchFindingGroupsParams = {}, +) => fetchFindingGroupsEndpoint("finding-groups/latest", params); + +interface FetchFindingGroupResourcesParams { + checkId: string; + page?: number; + pageSize?: number; + filters?: Record; +} + +async function fetchFindingGroupResourcesEndpoint( + endpointPrefix: string, + { + checkId, + page = 1, + pageSize = 20, + filters = {}, + }: FetchFindingGroupResourcesParams, +) { const headers = await getAuthHeaders({ contentType: false }); + const normalizedFilters = normalizeFindingGroupResourceFilters(filters); - if (isNaN(Number(page)) || page < 1) redirect("/findings"); - - const url = new URL(`${apiBaseUrl}/finding-groups/latest`); + const url = new URL( + `${apiBaseUrl}/${endpointPrefix}/${encodeURIComponent(checkId)}/resources`, + ); if (page) url.searchParams.append("page[number]", page.toString()); if (pageSize) url.searchParams.append("page[size]", pageSize.toString()); - if (sort) url.searchParams.append("sort", sort); + url.searchParams.append("sort", DEFAULT_FINDING_GROUP_RESOURCES_SORT); - appendSanitizedProviderFilters(url, mapSearchFilter(filters)); + appendSanitizedProviderFilters(url, normalizedFilters); try { const response = await fetch(url.toString(), { headers }); return handleApiResponse(response); } catch (error) { - console.error("Error fetching latest finding groups:", error); + console.error(`Error fetching ${endpointPrefix} resources:`, error); return undefined; } -}; +} -export const getFindingGroupResources = async ({ - checkId, - page = 1, - pageSize = 20, - filters = {}, -}: { - checkId: string; - page?: number; - pageSize?: number; - filters?: Record; -}) => { - const headers = await getAuthHeaders({ contentType: false }); +export const getFindingGroupResources = async ( + params: FetchFindingGroupResourcesParams, +) => fetchFindingGroupResourcesEndpoint("finding-groups", params); - const url = new URL( - `${apiBaseUrl}/finding-groups/${encodeURIComponent(checkId)}/resources`, - ); - - if (page) url.searchParams.append("page[number]", page.toString()); - if (pageSize) url.searchParams.append("page[size]", pageSize.toString()); - // sort=-status is kept for future-proofing: if the filter[status]=FAIL - // constraint is ever relaxed to allow multiple statuses, the sort ensures - // FAIL resources still appear first in the result set. - url.searchParams.append("sort", "-status"); - - appendSanitizedProviderFilters(url, filters); - - // Use .set() AFTER appendSanitizedProviderFilters so our hardcoded FAIL - // always wins, even if the caller passed a different filter[status] value. - // Using .set() instead of .append() prevents duplicate filter[status] params. - url.searchParams.set("filter[status]", "FAIL"); - - try { - const response = await fetch(url.toString(), { - headers, - }); - - return handleApiResponse(response); - } catch (error) { - console.error("Error fetching finding group resources:", error); - return undefined; - } -}; - -export const getLatestFindingGroupResources = async ({ - checkId, - page = 1, - pageSize = 20, - filters = {}, -}: { - checkId: string; - page?: number; - pageSize?: number; - filters?: Record; -}) => { - const headers = await getAuthHeaders({ contentType: false }); - - const url = new URL( - `${apiBaseUrl}/finding-groups/latest/${encodeURIComponent(checkId)}/resources`, - ); - - if (page) url.searchParams.append("page[number]", page.toString()); - if (pageSize) url.searchParams.append("page[size]", pageSize.toString()); - // sort=-status is kept for future-proofing: if the filter[status]=FAIL - // constraint is ever relaxed to allow multiple statuses, the sort ensures - // FAIL resources still appear first in the result set. - url.searchParams.append("sort", "-status"); - - appendSanitizedProviderFilters(url, filters); - - // Use .set() AFTER appendSanitizedProviderFilters so our hardcoded FAIL - // always wins, even if the caller passed a different filter[status] value. - // Using .set() instead of .append() prevents duplicate filter[status] params. - url.searchParams.set("filter[status]", "FAIL"); - - try { - const response = await fetch(url.toString(), { - headers, - }); - - return handleApiResponse(response); - } catch (error) { - console.error("Error fetching latest finding group resources:", error); - return undefined; - } -}; +export const getLatestFindingGroupResources = async ( + params: FetchFindingGroupResourcesParams, +) => fetchFindingGroupResourcesEndpoint("finding-groups/latest", params); diff --git a/ui/actions/findings/findings-by-resource.test.ts b/ui/actions/findings/findings-by-resource.test.ts index c637e5f707..4aff44a2cf 100644 --- a/ui/actions/findings/findings-by-resource.test.ts +++ b/ui/actions/findings/findings-by-resource.test.ts @@ -43,7 +43,6 @@ vi.mock("@/actions/finding-groups", () => ({ })); import { - resolveFindingIds, resolveFindingIdsByCheckIds, resolveFindingIdsByVisibleGroupResources, } from "./findings-by-resource"; @@ -142,47 +141,6 @@ describe("resolveFindingIdsByCheckIds", () => { }); }); -describe("resolveFindingIds", () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.stubGlobal("fetch", fetchMock); - getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" }); - }); - - it("should use the dated findings endpoint when date or scan filters are active", async () => { - // Given - fetchMock.mockResolvedValue(new Response("", { status: 200 })); - handleApiResponseMock.mockResolvedValue({ - data: [{ id: "finding-1" }, { id: "finding-2" }], - }); - - // When - const result = await resolveFindingIds({ - checkId: "check-1", - resourceUids: ["resource-1", "resource-2"], - hasDateOrScanFilter: true, - filters: { - "filter[scan__in]": "scan-1", - "filter[inserted_at__gte]": "2026-03-01", - }, - }); - - // Then - expect(result).toEqual(["finding-1", "finding-2"]); - - const calledUrl = new URL(fetchMock.mock.calls[0][0]); - expect(calledUrl.pathname).toBe("/api/v1/findings"); - expect(calledUrl.searchParams.get("filter[check_id]")).toBe("check-1"); - expect(calledUrl.searchParams.get("filter[resource_uid__in]")).toBe( - "resource-1,resource-2", - ); - expect(calledUrl.searchParams.get("filter[scan__in]")).toBe("scan-1"); - expect(calledUrl.searchParams.get("filter[inserted_at__gte]")).toBe( - "2026-03-01", - ); - }); -}); - describe("resolveFindingIdsByVisibleGroupResources", () => { beforeEach(() => { vi.clearAllMocks(); @@ -190,22 +148,18 @@ describe("resolveFindingIdsByVisibleGroupResources", () => { getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" }); }); - it("should resolve finding IDs from the group's visible resource UIDs instead of muting the whole check", async () => { - // Given + it("extracts finding_id directly from group resources without a second resolution round-trip", async () => { + // Given — the group resources endpoint returns finding_id in each resource getLatestFindingGroupResourcesMock .mockResolvedValueOnce({ data: [ { id: "resource-row-1", - attributes: { - resource: { uid: "resource-1" }, - }, + attributes: { finding_id: "finding-1" }, }, { id: "resource-row-2", - attributes: { - resource: { uid: "resource-2" }, - }, + attributes: { finding_id: "finding-2" }, }, ], meta: { pagination: { pages: 2 } }, @@ -214,19 +168,12 @@ describe("resolveFindingIdsByVisibleGroupResources", () => { data: [ { id: "resource-row-3", - attributes: { - resource: { uid: "resource-3" }, - }, + attributes: { finding_id: "finding-3" }, }, ], meta: { pagination: { pages: 2 } }, }); - fetchMock.mockResolvedValue(new Response("", { status: 200 })); - handleApiResponseMock.mockResolvedValue({ - data: [{ id: "finding-1" }, { id: "finding-2" }, { id: "finding-3" }], - }); - // When const result = await resolveFindingIdsByVisibleGroupResources({ checkId: "check-1", @@ -236,8 +183,13 @@ describe("resolveFindingIdsByVisibleGroupResources", () => { resourceSearch: "visible subset", }); - // Then + // Then — finding IDs come directly from the group resources response expect(result).toEqual(["finding-1", "finding-2", "finding-3"]); + + // No second round-trip to /findings/latest + expect(fetchMock).not.toHaveBeenCalled(); + + // Group resources endpoint was paginated with correct filters expect(getLatestFindingGroupResourcesMock).toHaveBeenCalledTimes(2); expect(getLatestFindingGroupResourcesMock).toHaveBeenNthCalledWith(1, { checkId: "check-1", @@ -246,6 +198,8 @@ describe("resolveFindingIdsByVisibleGroupResources", () => { filters: { "filter[provider_type__in]": "aws", "filter[name__icontains]": "visible subset", + "filter[status]": "FAIL", + "filter[muted]": "false", }, }); expect(getLatestFindingGroupResourcesMock).toHaveBeenNthCalledWith(2, { @@ -255,168 +209,56 @@ describe("resolveFindingIdsByVisibleGroupResources", () => { filters: { "filter[provider_type__in]": "aws", "filter[name__icontains]": "visible subset", + "filter[status]": "FAIL", + "filter[muted]": "false", }, }); - - const calledUrl = new URL(fetchMock.mock.calls[0][0]); - expect(calledUrl.pathname).toBe("/api/v1/findings/latest"); - expect(calledUrl.searchParams.get("filter[check_id]")).toBe("check-1"); - expect(calledUrl.searchParams.get("filter[check_id__in]")).toBeNull(); - expect(calledUrl.searchParams.get("filter[resource_uid__in]")).toBe( - "resource-1,resource-2,resource-3", - ); - }); -}); - -// --------------------------------------------------------------------------- -// Blocker 3: Muting a group mutes ALL historical findings, not just FAIL ones -// -// The fix: resolveFindingIds must include filter[status]=FAIL so only active -// (failing) findings are resolved for mute, not historical/passing ones. -// --------------------------------------------------------------------------- - -describe("resolveFindingIds — Blocker 3: only resolve FAIL findings for mute", () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.stubGlobal("fetch", fetchMock); - getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" }); }); - it("should include filter[status]=FAIL in the findings resolution URL for mute", async () => { + it("deduplicates finding IDs across pages", async () => { + // Given — same finding_id appears on both pages + getLatestFindingGroupResourcesMock + .mockResolvedValueOnce({ + data: [ + { id: "r-1", attributes: { finding_id: "finding-1" } }, + { id: "r-2", attributes: { finding_id: "finding-2" } }, + ], + meta: { pagination: { pages: 2 } }, + }) + .mockResolvedValueOnce({ + data: [{ id: "r-3", attributes: { finding_id: "finding-2" } }], + meta: { pagination: { pages: 2 } }, + }); + + // When + const result = await resolveFindingIdsByVisibleGroupResources({ + checkId: "check-1", + }); + + // Then — no duplicates + expect(result).toEqual(["finding-1", "finding-2"]); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("uses the dated endpoint when date or scan filters are active", async () => { // Given - fetchMock.mockResolvedValue(new Response("", { status: 200 })); - handleApiResponseMock.mockResolvedValue({ - data: [{ id: "finding-1" }, { id: "finding-2" }], + getFindingGroupResourcesMock.mockResolvedValueOnce({ + data: [{ id: "r-1", attributes: { finding_id: "finding-1" } }], + meta: { pagination: { pages: 1 } }, }); // When - await resolveFindingIds({ + await resolveFindingIdsByVisibleGroupResources({ checkId: "check-1", - resourceUids: ["resource-1", "resource-2"], - }); - - // Then — the URL must filter to only FAIL status findings - const calledUrl = new URL(fetchMock.mock.calls[0][0]); - expect(calledUrl.searchParams.get("filter[status]")).toBe("FAIL"); - }); - - it("should include filter[status]=FAIL even when date or scan filters are active", async () => { - // Given - fetchMock.mockResolvedValue(new Response("", { status: 200 })); - handleApiResponseMock.mockResolvedValue({ - data: [{ id: "finding-1" }], - }); - - // When - await resolveFindingIds({ - checkId: "check-1", - resourceUids: ["resource-1"], hasDateOrScanFilter: true, filters: { - "filter[inserted_at__gte]": "2026-01-01", + "filter[scan__in]": "scan-1", }, }); - // Then - const calledUrl = new URL(fetchMock.mock.calls[0][0]); - expect(calledUrl.pathname).toBe("/api/v1/findings"); - expect(calledUrl.searchParams.get("filter[status]")).toBe("FAIL"); - }); - - it("should override caller filter[status] with FAIL — no duplicate params", async () => { - // Given — caller passes filter[status]=PASS via filters dict - fetchMock.mockResolvedValue(new Response("", { status: 200 })); - handleApiResponseMock.mockResolvedValue({ - data: [{ id: "finding-1" }], - }); - - // When - await resolveFindingIds({ - checkId: "check-1", - resourceUids: ["resource-1"], - filters: { - "filter[status]": "PASS", - }, - }); - - // Then — hardcoded FAIL must win, exactly 1 value - const calledUrl = new URL(fetchMock.mock.calls[0][0] as string); - const statusValues = calledUrl.searchParams.getAll("filter[status]"); - expect(statusValues).toHaveLength(1); - expect(statusValues[0]).toBe("FAIL"); - }); -}); - -// --------------------------------------------------------------------------- -// Fix 4: Unbounded page[size] cap -// -// The bug: createResourceFindingResolutionUrl sets page[size]=resourceUids.length -// with no upper bound guard. The production fix adds Math.min(resourceUids.length, MAX_PAGE_SIZE) -// with MAX_PAGE_SIZE=500 as an explicit defensive cap. -// --------------------------------------------------------------------------- - -describe("resolveFindingIds — Fix 4: page[size] explicit cap at MAX_PAGE_SIZE=500", () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.stubGlobal("fetch", fetchMock); - getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" }); - }); - - it("should use resourceUids.length as page[size] for a small batch (under 500)", async () => { - // Given — 3 resources, well under the cap - fetchMock.mockResolvedValue(new Response("", { status: 200 })); - handleApiResponseMock.mockResolvedValue({ - data: [{ id: "finding-1" }, { id: "finding-2" }, { id: "finding-3" }], - }); - - // When - await resolveFindingIds({ - checkId: "check-1", - resourceUids: ["resource-1", "resource-2", "resource-3"], - }); - - // Then — page[size] should equal the number of resourceUids (3) - const calledUrl = new URL(fetchMock.mock.calls[0][0]); - expect(calledUrl.searchParams.get("page[size]")).toBe("3"); - }); - - it("should cap page[size] at 500 when the chunk has exactly 500 UIDs (boundary value)", async () => { - // Given — exactly 500 unique UIDs (at the cap boundary) - const resourceUids = Array.from({ length: 500 }, (_, i) => `resource-${i}`); - fetchMock.mockResolvedValue(new Response("", { status: 200 })); - handleApiResponseMock.mockResolvedValue({ data: [] }); - - // When - await resolveFindingIds({ - checkId: "check-1", - resourceUids, - }); - - // Then — page[size] must be exactly 500 (not capped lower) - const firstUrl = new URL(fetchMock.mock.calls[0][0] as string); - expect(firstUrl.searchParams.get("page[size]")).toBe("500"); - }); - - it("should cap page[size] at 500 even when a chunk would exceed 500 — Math.min guard in URL builder", async () => { - // Given — 501 UIDs. The chunker splits into [500, 1]. - // The FIRST chunk has 500 UIDs → page[size] should be 500 (Math.min(500, 500)). - // The SECOND chunk has 1 UID → page[size] should be 1 (Math.min(1, 500)). - // This proves the Math.min cap fires correctly on every chunk. - const resourceUids = Array.from({ length: 501 }, (_, i) => `resource-${i}`); - fetchMock.mockResolvedValue(new Response("", { status: 200 })); - handleApiResponseMock.mockResolvedValue({ data: [] }); - - // When - await resolveFindingIds({ - checkId: "check-1", - resourceUids, - }); - - // Then — two fetch calls: one for 500 UIDs, one for 1 UID - expect(fetchMock).toHaveBeenCalledTimes(2); - const firstUrl = new URL(fetchMock.mock.calls[0][0] as string); - const secondUrl = new URL(fetchMock.mock.calls[1][0] as string); - expect(firstUrl.searchParams.get("page[size]")).toBe("500"); - expect(secondUrl.searchParams.get("page[size]")).toBe("1"); + // Then — uses getFindingGroupResources (dated), not getLatestFindingGroupResources + expect(getFindingGroupResourcesMock).toHaveBeenCalledTimes(1); + expect(getLatestFindingGroupResourcesMock).not.toHaveBeenCalled(); + expect(fetchMock).not.toHaveBeenCalled(); }); }); diff --git a/ui/actions/findings/findings-by-resource.ts b/ui/actions/findings/findings-by-resource.ts index 12900ffdca..344c5607a0 100644 --- a/ui/actions/findings/findings-by-resource.ts +++ b/ui/actions/findings/findings-by-resource.ts @@ -14,22 +14,12 @@ const FINDING_IDS_RESOLUTION_CONCURRENCY = 4; const FINDING_GROUP_RESOURCES_RESOLUTION_PAGE_SIZE = 500; const FINDING_FIELDS = "uid"; -/** Explicit upper bound for page[size] in resource-finding resolution requests. */ -const MAX_RESOURCE_FINDING_PAGE_SIZE = 500; - interface ResolveFindingIdsByCheckIdsParams { checkIds: string[]; filters?: Record; hasDateOrScanFilter?: boolean; } -interface ResolveFindingIdsParams { - checkId: string; - resourceUids: string[]; - filters?: Record; - hasDateOrScanFilter?: boolean; -} - interface ResolveFindingIdsByVisibleGroupResourcesParams { checkId: string; filters?: Record; @@ -42,8 +32,8 @@ interface FindingIdsPageResponse { totalPages: number; } -interface FindingGroupResourceUidsPageResponse { - resourceUids: string[]; +interface FindingGroupResourceFindingIdsPageResponse { + findingIds: string[]; totalPages: number; } @@ -100,78 +90,7 @@ async function fetchFindingIdsPage({ }; } -function chunkValues(values: T[], chunkSize: number): T[][] { - const chunks: T[][] = []; - for (let index = 0; index < values.length; index += chunkSize) { - chunks.push(values.slice(index, index + chunkSize)); - } - return chunks; -} - -function createResourceFindingResolutionUrl({ - checkId, - resourceUids, - filters = {}, - hasDateOrScanFilter = false, -}: ResolveFindingIdsParams): URL { - const endpoint = hasDateOrScanFilter ? "findings" : "findings/latest"; - const url = new URL(`${apiBaseUrl}/${endpoint}`); - - url.searchParams.append("filter[check_id]", checkId); - url.searchParams.append("filter[resource_uid__in]", resourceUids.join(",")); - url.searchParams.append("filter[muted]", "false"); - url.searchParams.append( - "page[size]", - Math.min(resourceUids.length, MAX_RESOURCE_FINDING_PAGE_SIZE).toString(), - ); - - appendSanitizedProviderTypeFilters(url, filters); - - // Hardcoded FAIL filter AFTER appendSanitizedProviderTypeFilters — .set() - // guarantees this wins even if the caller passes filter[status] in filters. - url.searchParams.set("filter[status]", "FAIL"); - - return url; -} - -async function fetchFindingIdsForResourceUids({ - headers, - ...params -}: ResolveFindingIdsParams & { - headers: HeadersInit; -}): Promise { - const response = await fetch( - createResourceFindingResolutionUrl(params).toString(), - { - headers, - }, - ); - const data = await handleApiResponse(response); - - if (!data?.data || !Array.isArray(data.data)) { - return []; - } - - return data.data - .map((item: { id?: string }) => item.id) - .filter((id: string | undefined): id is string => Boolean(id)); -} - -function buildFindingGroupResourceFilters({ - filters = {}, - resourceSearch, -}: Pick< - ResolveFindingIdsByVisibleGroupResourcesParams, - "filters" | "resourceSearch" ->): Record { - const nextFilters = { ...filters }; - if (resourceSearch) { - nextFilters["filter[name__icontains]"] = resourceSearch; - } - return nextFilters; -} - -async function fetchFindingGroupResourceUidsPage({ +async function fetchFindingGroupResourceFindingIdsPage({ checkId, filters = {}, hasDateOrScanFilter = false, @@ -179,77 +98,44 @@ async function fetchFindingGroupResourceUidsPage({ resourceSearch, }: ResolveFindingIdsByVisibleGroupResourcesParams & { page: number; -}): Promise { +}): Promise { const fetchFn = hasDateOrScanFilter ? getFindingGroupResources : getLatestFindingGroupResources; + const resolvedFilters: Record = { + ...filters, + "filter[status]": "FAIL", + "filter[muted]": "false", + }; + if (resourceSearch) { + resolvedFilters["filter[name__icontains]"] = resourceSearch; + } + const response = await fetchFn({ checkId, page, pageSize: FINDING_GROUP_RESOURCES_RESOLUTION_PAGE_SIZE, - filters: buildFindingGroupResourceFilters({ filters, resourceSearch }), + filters: resolvedFilters, }); const data = response?.data; if (!data || !Array.isArray(data)) { - return { resourceUids: [], totalPages: 1 }; + return { findingIds: [], totalPages: 1 }; } return { - resourceUids: data + findingIds: data .map( - (item: { attributes?: { resource?: { uid?: string } } }) => - item.attributes?.resource?.uid, + (item: { attributes?: { finding_id?: string } }) => + item.attributes?.finding_id, ) - .filter((uid: string | undefined): uid is string => Boolean(uid)), + .filter((id: string | undefined): id is string => Boolean(id)), totalPages: response?.meta?.pagination?.pages ?? 1, }; } -/** - * Resolves resource UIDs + check ID into actual finding UUIDs. - * Uses /findings/latest (or /findings when date/scan filters are active) - * with check_id and resource_uid__in filters to batch-resolve actual finding IDs. - */ -export const resolveFindingIds = async ({ - checkId, - resourceUids, - filters = {}, - hasDateOrScanFilter = false, -}: ResolveFindingIdsParams): Promise => { - if (resourceUids.length === 0) { - return []; - } - - const headers = await getAuthHeaders({ contentType: false }); - const resourceUidChunks = chunkValues( - Array.from(new Set(resourceUids)), - FINDING_IDS_RESOLUTION_PAGE_SIZE, - ); - - try { - const results = await runWithConcurrencyLimit( - resourceUidChunks, - FINDING_IDS_RESOLUTION_CONCURRENCY, - (resourceUidChunk) => - fetchFindingIdsForResourceUids({ - checkId, - resourceUids: resourceUidChunk, - filters, - hasDateOrScanFilter, - headers, - }), - ); - - return Array.from(new Set(results.flat())); - } catch (error) { - console.error("Error resolving finding IDs:", error); - return []; - } -}; - /** * Resolves check IDs into actual finding UUIDs. * Used at the group level where each row represents a check_id. @@ -305,8 +191,12 @@ export const resolveFindingIdsByCheckIds = async ({ }; /** - * Resolves a finding-group row to the actual findings for the resources + * Resolves a finding-group row to the actual finding UUIDs for the resources * currently visible in that group. + * + * Extracts finding_id directly from the group resources endpoint response, + * filtering server-side by status=FAIL and muted=false. No second resolution + * round-trip to /findings/latest is needed. */ export const resolveFindingIdsByVisibleGroupResources = async ({ checkId, @@ -315,7 +205,7 @@ export const resolveFindingIdsByVisibleGroupResources = async ({ resourceSearch, }: ResolveFindingIdsByVisibleGroupResourcesParams): Promise => { try { - const firstPage = await fetchFindingGroupResourceUidsPage({ + const firstPage = await fetchFindingGroupResourceFindingIdsPage({ checkId, filters, hasDateOrScanFilter, @@ -332,7 +222,7 @@ export const resolveFindingIdsByVisibleGroupResources = async ({ remainingPages, FINDING_IDS_RESOLUTION_CONCURRENCY, (page) => - fetchFindingGroupResourceUidsPage({ + fetchFindingGroupResourceFindingIdsPage({ checkId, filters, hasDateOrScanFilter, @@ -341,19 +231,12 @@ export const resolveFindingIdsByVisibleGroupResources = async ({ }), ); - const resourceUids = Array.from( + return Array.from( new Set([ - ...firstPage.resourceUids, - ...remainingResults.flatMap((result) => result.resourceUids), + ...firstPage.findingIds, + ...remainingResults.flatMap((result) => result.findingIds), ]), ); - - return resolveFindingIds({ - checkId, - resourceUids, - filters, - hasDateOrScanFilter, - }); } catch (error) { console.error( "Error resolving finding IDs from visible group resources:", @@ -379,6 +262,9 @@ export const getLatestFindingsByResourceUid = async ({ ); url.searchParams.append("filter[resource_uid]", resourceUid); + url.searchParams.append("filter[status]", "FAIL"); + url.searchParams.append("filter[muted]", "include"); + url.searchParams.append("sort", "-severity,-updated_at"); if (page) url.searchParams.append("page[number]", page.toString()); if (pageSize) url.searchParams.append("page[size]", pageSize.toString()); diff --git a/ui/actions/invitations/invitation.ts b/ui/actions/invitations/invitation.ts index 8ad037cbbe..72f591705a 100644 --- a/ui/actions/invitations/invitation.ts +++ b/ui/actions/invitations/invitation.ts @@ -2,10 +2,13 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; +import { z } from "zod"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper"; +const invitationTokenSchema = z.string().min(1).max(500); + export const getInvitations = async ({ page = 1, query = "", @@ -195,3 +198,35 @@ export const revokeInvite = async (formData: FormData) => { handleApiError(error); } }; + +export const acceptInvitation = async (token: string) => { + const parsed = invitationTokenSchema.safeParse(token); + if (!parsed.success) { + return { error: "Invalid invitation token" }; + } + + const headers = await getAuthHeaders({ contentType: true }); + + const url = new URL(`${apiBaseUrl}/invitations/accept`); + + const body = JSON.stringify({ + data: { + type: "invitations", + attributes: { + invitation_token: parsed.data, + }, + }, + }); + + try { + const response = await fetch(url.toString(), { + method: "POST", + headers, + body, + }); + + return handleApiResponse(response); + } catch (error) { + return handleApiError(error); + } +}; diff --git a/ui/actions/mute-rules/mute-rules.ts b/ui/actions/mute-rules/mute-rules.ts index cc31bd90bd..5b571e2bba 100644 --- a/ui/actions/mute-rules/mute-rules.ts +++ b/ui/actions/mute-rules/mute-rules.ts @@ -144,22 +144,36 @@ export const createMuteRule = async ( }, }, }; + const requestBody = JSON.stringify(bodyData); const response = await fetch(url.toString(), { method: "POST", headers, - body: JSON.stringify(bodyData), + body: requestBody, }); if (!response.ok) { let errorMessage = `Failed to create mute rule: ${response.statusText}`; + const responseContentType = response.headers.get("content-type"); try { - const errorData = await response.json(); - errorMessage = - errorData?.errors?.[0]?.detail || errorData?.message || errorMessage; + if (responseContentType?.includes("application/json")) { + const errorData = await response.json(); + errorMessage = + ( + errorData as { + errors?: Array<{ detail?: string }>; + message?: string; + } + )?.errors?.[0]?.detail || + (errorData as { message?: string })?.message || + errorMessage; + } else { + await response.text(); + } } catch { // JSON parsing failed, use default error message } + throw new Error(errorMessage); } diff --git a/ui/actions/scans/scans.ts b/ui/actions/scans/scans.ts index 8716c5c1c6..7f8c2c8479 100644 --- a/ui/actions/scans/scans.ts +++ b/ui/actions/scans/scans.ts @@ -76,11 +76,18 @@ export const getScansByState = async () => { } }; -export const getScan = async (scanId: string) => { +export const getScan = async ( + scanId: string, + { include }: { include?: string } = {}, +) => { const headers = await getAuthHeaders({ contentType: false }); const url = new URL(`${apiBaseUrl}/scans/${scanId}`); + if (include) { + url.searchParams.set("include", include); + } + try { const response = await fetch(url.toString(), { headers, diff --git a/ui/app/(auth)/(guest-only)/layout.tsx b/ui/app/(auth)/(guest-only)/layout.tsx new file mode 100644 index 0000000000..3ff93d836b --- /dev/null +++ b/ui/app/(auth)/(guest-only)/layout.tsx @@ -0,0 +1,18 @@ +import { redirect } from "next/navigation"; +import { ReactNode } from "react"; + +import { auth } from "@/auth.config"; + +export default async function GuestOnlyLayout({ + children, +}: { + children: ReactNode; +}) { + const session = await auth(); + + if (session?.user) { + redirect("/"); + } + + return <>{children}; +} diff --git a/ui/app/(auth)/sign-in/page.tsx b/ui/app/(auth)/(guest-only)/sign-in/page.tsx similarity index 100% rename from ui/app/(auth)/sign-in/page.tsx rename to ui/app/(auth)/(guest-only)/sign-in/page.tsx diff --git a/ui/app/(auth)/sign-up/page.tsx b/ui/app/(auth)/(guest-only)/sign-up/page.tsx similarity index 100% rename from ui/app/(auth)/sign-up/page.tsx rename to ui/app/(auth)/(guest-only)/sign-up/page.tsx diff --git a/ui/app/(auth)/invitation/accept/accept-invitation-client.tsx b/ui/app/(auth)/invitation/accept/accept-invitation-client.tsx new file mode 100644 index 0000000000..73e6dbe8fd --- /dev/null +++ b/ui/app/(auth)/invitation/accept/accept-invitation-client.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { Icon } from "@iconify/react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { signOut } from "next-auth/react"; +import { useEffect, useRef, useState } from "react"; + +import { acceptInvitation } from "@/actions/invitations"; +import { Button } from "@/components/shadcn"; +import { + INVITATION_ACTION_PARAM, + INVITATION_SIGNUP_ACTION, +} from "@/lib/invitation-routing"; + +type AcceptState = + | { kind: "no-token" } + | { kind: "accepting" } + | { kind: "error"; message: string; canRetry: boolean; needsSignOut: boolean } + | { kind: "choose" }; + +function mapApiError(status: number | undefined): { + message: string; + canRetry: boolean; + needsSignOut: boolean; +} { + switch (status) { + case 410: + return { + message: + "This invitation has expired. Please contact your administrator for a new one.", + canRetry: false, + needsSignOut: false, + }; + case 400: + return { + message: "This invitation has already been used.", + canRetry: false, + needsSignOut: false, + }; + case 404: + return { + message: + "This invitation was sent to a different email address. Please sign in with the correct account.", + canRetry: false, + needsSignOut: true, + }; + default: + return { + message: "Something went wrong while accepting the invitation.", + canRetry: true, + needsSignOut: false, + }; + } +} + +export function AcceptInvitationClient({ + isAuthenticated, + token, +}: { + isAuthenticated: boolean; + token: string | null; +}) { + const router = useRouter(); + const [state, setState] = useState(() => { + if (!token) return { kind: "no-token" }; + if (!isAuthenticated) return { kind: "choose" }; + return { kind: "accepting" }; + }); + const hasStartedRef = useRef(false); + + async function doAccept() { + if (!token) return; + setState({ kind: "accepting" }); + + const result = await acceptInvitation(token); + + if (result?.error) { + const { message, canRetry, needsSignOut } = mapApiError(result.status); + setState({ kind: "error", message, canRetry, needsSignOut }); + } else { + router.push("/"); + } + } + + async function handleSignOutAndRedirect() { + if (!token) return; + const callbackPath = `/invitation/accept?invitation_token=${encodeURIComponent(token)}`; + await signOut({ redirect: false }); + router.push(`/sign-in?callbackUrl=${encodeURIComponent(callbackPath)}`); + } + + useEffect(() => { + if (hasStartedRef.current) return; + hasStartedRef.current = true; + + if (!token) { + setState({ kind: "no-token" }); + return; + } + + if (isAuthenticated) { + doAccept(); + } else { + setState({ kind: "choose" }); + } + }, [token, isAuthenticated]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( +
+
+ {/* No token */} + {state.kind === "no-token" && ( +
+ +

Invalid Invitation Link

+

+ No invitation token was provided. Please check the link you + received. +

+ +
+ )} + + {/* Accepting */} + {state.kind === "accepting" && ( +
+ +

Accepting Invitation...

+

+ Please wait while we process your invitation. +

+
+ )} + + {/* Error */} + {state.kind === "error" && ( +
+ +

+ Could Not Accept Invitation +

+

{state.message}

+
+ {state.canRetry && } + {state.needsSignOut ? ( + + ) : ( + + )} +
+
+ )} + + {/* Choice page for unauthenticated users */} + {state.kind === "choose" && ( +
+ +
+

+ You've Been Invited +

+

+ You've been invited to join a tenant on Prowler. How would + you like to continue? +

+
+
+ + +
+
+ )} +
+
+ ); +} diff --git a/ui/app/(auth)/invitation/accept/page.tsx b/ui/app/(auth)/invitation/accept/page.tsx new file mode 100644 index 0000000000..9bfc5e0110 --- /dev/null +++ b/ui/app/(auth)/invitation/accept/page.tsx @@ -0,0 +1,22 @@ +import { auth } from "@/auth.config"; +import { SearchParamsProps } from "@/types"; + +import { AcceptInvitationClient } from "./accept-invitation-client"; + +export default async function AcceptInvitationPage({ + searchParams, +}: { + searchParams: Promise; +}) { + const session = await auth(); + const resolvedSearchParams = await searchParams; + + const token = + typeof resolvedSearchParams?.invitation_token === "string" + ? resolvedSearchParams.invitation_token + : null; + + return ( + + ); +} diff --git a/ui/app/(auth)/layout.tsx b/ui/app/(auth)/layout.tsx index ab6b1e9bfa..07fe3a60c3 100644 --- a/ui/app/(auth)/layout.tsx +++ b/ui/app/(auth)/layout.tsx @@ -2,10 +2,8 @@ import "@/styles/globals.css"; import { GoogleTagManager } from "@next/third-parties/google"; import { Metadata, Viewport } from "next"; -import { redirect } from "next/navigation"; -import { ReactNode } from "react"; +import { ReactNode, Suspense } from "react"; -import { auth } from "@/auth.config"; import { NavigationProgress, Toaster } from "@/components/ui"; import { fontSans } from "@/config/fonts"; import { siteConfig } from "@/config/site"; @@ -31,17 +29,7 @@ export const viewport: Viewport = { ], }; -export default async function RootLayout({ - children, -}: { - children: ReactNode; -}) { - const session = await auth(); - - if (session?.user) { - redirect("/"); - } - +export default function AuthLayout({ children }: { children: ReactNode }) { return ( @@ -53,7 +41,9 @@ export default async function RootLayout({ )} > - + + + {children} ({ + useSearchParams: () => new URLSearchParams(), +})); + +vi.mock("@/hooks/use-url-filters", () => ({ + useUrlFilters: () => ({ + navigateWithParams: vi.fn(), + }), +})); + +vi.mock("@/components/icons/providers-badge", () => ({ + AWSProviderBadge: () => AWS, + AzureProviderBadge: () => Azure, + GCPProviderBadge: () => GCP, + CloudflareProviderBadge: () => Cloudflare, + GitHubProviderBadge: () => GitHub, + GoogleWorkspaceProviderBadge: () => Google Workspace, + IacProviderBadge: () => IaC, + ImageProviderBadge: () => Image, + KS8ProviderBadge: () => Kubernetes, + M365ProviderBadge: () => M365, + MongoDBAtlasProviderBadge: () => MongoDB Atlas, + OpenStackProviderBadge: () => OpenStack, + OracleCloudProviderBadge: () => Oracle Cloud, + AlibabaCloudProviderBadge: () => Alibaba Cloud, + VercelProviderBadge: () => Vercel, +})); + +vi.mock("@/components/shadcn/select/multiselect", () => ({ + MultiSelect: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + MultiSelectTrigger: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + MultiSelectValue: ({ placeholder }: { placeholder: string }) => ( + {placeholder} + ), + MultiSelectContent: ({ + children, + search, + }: { + children: React.ReactNode; + search?: unknown; + }) => { + multiSelectContentSpy(search); + return
{children}
; + }, + MultiSelectItem: ({ + children, + value, + keywords, + }: { + children: React.ReactNode; + value: string; + keywords?: string[]; + }) => ( +
+ {children} +
+ ), +})); + +const providers = [ + { + id: "provider-1", + type: "providers" as const, + attributes: { + provider: "aws" as const, + uid: "123456789012", + alias: "Production AWS", + status: "completed" as const, + resources: 0, + connection: { + connected: true, + last_checked_at: "2026-04-13T00:00:00Z", + }, + scanner_args: { + only_logs: false, + excluded_checks: [], + aws_retries_max_attempts: 3, + }, + inserted_at: "2026-04-13T00:00:00Z", + updated_at: "2026-04-13T00:00:00Z", + created_by: { + object: "user", + id: "user-1", + }, + }, + relationships: { + secret: { + data: null, + }, + provider_groups: { + meta: { + count: 0, + }, + data: [], + }, + }, + }, +]; + +describe("AccountsSelector", () => { + it("passes searchable dropdown defaults to MultiSelectContent", () => { + render(); + + expect(multiSelectContentSpy).toHaveBeenCalledWith({ + placeholder: "Search accounts...", + emptyMessage: "No accounts found.", + }); + expect(screen.getByText("Production AWS")).toBeInTheDocument(); + }); + + it("allows disabling search explicitly", () => { + render(); + + expect(multiSelectContentSpy).toHaveBeenLastCalledWith(false); + }); + + it("passes visible account labels as search keywords instead of only the internal id", () => { + render(); + + expect( + screen.getByText("Production AWS").closest("[data-value]"), + ).toHaveAttribute( + "data-keywords", + expect.stringContaining("Production AWS"), + ); + expect( + screen.getByText("Production AWS").closest("[data-value]"), + ).toHaveAttribute("data-keywords", expect.stringContaining("123456789012")); + }); +}); diff --git a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx index 441ad28697..e134625d98 100644 --- a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx @@ -24,6 +24,7 @@ import { MultiSelect, MultiSelectContent, MultiSelectItem, + type MultiSelectSearchProp, MultiSelectTrigger, MultiSelectValue, } from "@/components/shadcn/select/multiselect"; @@ -55,6 +56,7 @@ const PROVIDER_ICON: Record = { /** Common props shared by both batch and instant modes. */ interface AccountsSelectorBaseProps { providers: ProviderProps[]; + search?: MultiSelectSearchProp; /** * Currently selected provider types (from the pending ProviderTypeSelector state). * Used only for contextual description/empty-state messaging — does NOT narrow @@ -95,6 +97,10 @@ export function AccountsSelector({ onBatchChange, selectedValues, selectedProviderTypes, + search = { + placeholder: "Search accounts...", + emptyMessage: "No accounts found.", + }, }: AccountsSelectorProps) { const searchParams = useSearchParams(); const { navigateWithParams } = useUrlFilters(); @@ -159,7 +165,7 @@ export function AccountsSelector({ > {selectedLabel() || } - + {visibleProviders.length > 0 ? ( <>
diff --git a/ui/app/(prowler)/_overview/_components/provider-type-selector.test.tsx b/ui/app/(prowler)/_overview/_components/provider-type-selector.test.tsx new file mode 100644 index 0000000000..a60766394f --- /dev/null +++ b/ui/app/(prowler)/_overview/_components/provider-type-selector.test.tsx @@ -0,0 +1,138 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { ProviderTypeSelector } from "./provider-type-selector"; + +const multiSelectContentSpy = vi.fn(); + +vi.mock("next/navigation", () => ({ + useSearchParams: () => new URLSearchParams(), +})); + +vi.mock("@/hooks/use-url-filters", () => ({ + useUrlFilters: () => ({ + navigateWithParams: vi.fn(), + }), +})); + +vi.mock("@/components/icons/providers-badge", () => ({ + AWSProviderBadge: () => AWS, + AzureProviderBadge: () => Azure, + GCPProviderBadge: () => GCP, + KS8ProviderBadge: () => Kubernetes, + M365ProviderBadge: () => M365, + GitHubProviderBadge: () => GitHub, + GoogleWorkspaceProviderBadge: () => Google Workspace, + IacProviderBadge: () => IaC, + ImageProviderBadge: () => Image, + OracleCloudProviderBadge: () => Oracle Cloud, + MongoDBAtlasProviderBadge: () => MongoDB Atlas, + AlibabaCloudProviderBadge: () => Alibaba Cloud, + CloudflareProviderBadge: () => Cloudflare, + OpenStackProviderBadge: () => OpenStack, + VercelProviderBadge: () => Vercel, +})); + +vi.mock("@/components/shadcn/select/multiselect", () => ({ + MultiSelect: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + MultiSelectTrigger: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + MultiSelectValue: ({ placeholder }: { placeholder: string }) => ( + {placeholder} + ), + MultiSelectContent: ({ + children, + search, + }: { + children: React.ReactNode; + search?: unknown; + }) => { + multiSelectContentSpy(search); + return
{children}
; + }, + MultiSelectItem: ({ + children, + value, + keywords, + }: { + children: React.ReactNode; + value: string; + keywords?: string[]; + }) => ( +
+ {children} +
+ ), +})); + +const providers = [ + { + id: "provider-1", + type: "providers" as const, + attributes: { + provider: "aws" as const, + uid: "123456789012", + alias: "Production AWS", + status: "completed" as const, + resources: 0, + connection: { + connected: true, + last_checked_at: "2026-04-13T00:00:00Z", + }, + scanner_args: { + only_logs: false, + excluded_checks: [], + aws_retries_max_attempts: 3, + }, + inserted_at: "2026-04-13T00:00:00Z", + updated_at: "2026-04-13T00:00:00Z", + created_by: { + object: "user", + id: "user-1", + }, + }, + relationships: { + secret: { + data: null, + }, + provider_groups: { + meta: { + count: 0, + }, + data: [], + }, + }, + }, +]; + +describe("ProviderTypeSelector", () => { + it("passes searchable dropdown defaults to MultiSelectContent", () => { + render(); + + expect(multiSelectContentSpy).toHaveBeenCalledWith({ + placeholder: "Search providers...", + emptyMessage: "No providers found.", + }); + expect(screen.getByText("Amazon Web Services")).toBeInTheDocument(); + }); + + it("allows disabling search explicitly", () => { + render(); + + expect(multiSelectContentSpy).toHaveBeenLastCalledWith(false); + }); + + it("passes provider label as search keywords", () => { + render(); + + expect( + screen.getByText("Amazon Web Services").closest("[data-value]"), + ).toHaveAttribute( + "data-keywords", + expect.stringContaining("Amazon Web Services"), + ); + }); +}); diff --git a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx index 849d9e4d5e..819bc49000 100644 --- a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx @@ -7,6 +7,7 @@ import { MultiSelect, MultiSelectContent, MultiSelectItem, + type MultiSelectSearchProp, MultiSelectTrigger, MultiSelectValue, } from "@/components/shadcn/select/multiselect"; @@ -164,6 +165,7 @@ const PROVIDER_DATA: Record< /** Common props shared by both batch and instant modes. */ interface ProviderTypeSelectorBaseProps { providers: ProviderProps[]; + search?: MultiSelectSearchProp; } /** Batch mode: caller controls both pending state and notification callback (all-or-nothing). */ @@ -198,6 +200,10 @@ export const ProviderTypeSelector = ({ providers, onBatchChange, selectedValues, + search = { + placeholder: "Search providers...", + emptyMessage: "No providers found.", + }, }: ProviderTypeSelectorProps) => { const searchParams = useSearchParams(); const { navigateWithParams } = useUrlFilters(); @@ -284,7 +290,7 @@ export const ProviderTypeSelector = ({ > {selectedLabel() || } - + {availableTypes.length > 0 ? ( <>
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-overview.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-overview.tsx index e614ce521a..2e294cf069 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-overview.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-overview.tsx @@ -1,7 +1,7 @@ "use client"; +import { InfoField } from "@/components/shadcn/info-field/info-field"; import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; -import { InfoField } from "@/components/ui/entities"; import { DateWithTime } from "@/components/ui/entities/date-with-time"; import type { GraphNode, GraphNodePropertyValue } from "@/types/attack-paths"; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-code-editor.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-code-editor.tsx index 0f0866c1a2..c0c2283cb0 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-code-editor.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-code-editor.tsx @@ -1,399 +1 @@ -"use client"; - -import { - HighlightStyle, - StreamLanguage, - syntaxHighlighting, -} from "@codemirror/language"; -import { tags } from "@lezer/highlight"; -import CodeMirror, { - EditorView, - highlightActiveLineGutter, - lineNumbers, - placeholder as codeEditorPlaceholder, -} from "@uiw/react-codemirror"; -import { useTheme } from "next-themes"; -import { type HTMLAttributes } from "react"; - -import { Badge } from "@/components/shadcn"; -import { cn } from "@/lib/utils"; - -const OPEN_CYPHER_KEYWORDS = new Set([ - "all", - "and", - "as", - "asc", - "ascending", - "by", - "call", - "case", - "contains", - "create", - "delete", - "desc", - "descending", - "detach", - "distinct", - "else", - "end", - "exists", - "false", - "in", - "is", - "limit", - "match", - "merge", - "not", - "null", - "optional", - "or", - "order", - "remove", - "return", - "set", - "skip", - "then", - "true", - "unwind", - "where", - "with", - "xor", - "yield", -]); - -const OPEN_CYPHER_FUNCTIONS = new Set([ - "collect", - "coalesce", - "count", - "exists", - "head", - "id", - "keys", - "labels", - "last", - "length", - "nodes", - "properties", - "range", - "reduce", - "relationships", - "size", - "startnode", - "sum", - "tail", - "timestamp", - "tolower", - "toupper", - "trim", - "type", -]); - -interface OpenCypherParserState { - inBlockComment: boolean; - inString: "'" | '"' | null; -} - -const openCypherLanguage = StreamLanguage.define({ - startState() { - return { - inBlockComment: false, - inString: null, - }; - }, - token(stream, state) { - if (state.inBlockComment) { - while (!stream.eol()) { - if (stream.match("*/")) { - state.inBlockComment = false; - break; - } - stream.next(); - } - - return "comment"; - } - - if (state.inString) { - let escaped = false; - - while (!stream.eol()) { - const next = stream.next(); - - if (escaped) { - escaped = false; - continue; - } - - if (next === "\\") { - escaped = true; - continue; - } - - if (next === state.inString) { - state.inString = null; - break; - } - } - - // OpenCypher only supports single-line strings — reset at EOL so an - // unclosed quote does not bleed into subsequent lines. - if (stream.eol()) { - state.inString = null; - } - - return "string"; - } - - if (stream.eatSpace()) { - return null; - } - - if (stream.match("//")) { - stream.skipToEnd(); - return "comment"; - } - - if (stream.match("/*")) { - state.inBlockComment = true; - return "comment"; - } - - const quote = stream.peek(); - if (quote === "'" || quote === '"') { - state.inString = quote; - stream.next(); - return "string"; - } - - if (stream.match(/\$[A-Za-z_][\w]*/)) { - return "variableName"; - } - - if (stream.match(/:[A-Za-z_][\w]*/)) { - return "typeName"; - } - - if (stream.match(/[()[\]{},.;]/)) { - return "punctuation"; - } - - if (stream.match(/[<>!=~|&+\-/*%^]+/)) { - return "operator"; - } - - if (stream.match(/\d+(?:\.\d+)?/)) { - return "number"; - } - - if (stream.match(/[A-Za-z_][\w]*/)) { - const currentValue = stream.current(); - const normalizedValue = currentValue.toLowerCase(); - - if (OPEN_CYPHER_KEYWORDS.has(normalizedValue)) { - return "keyword"; - } - - if ( - OPEN_CYPHER_FUNCTIONS.has(normalizedValue) && - stream.match(/\s*(?=\()/, false) - ) { - return "function"; - } - - return "variableName"; - } - - stream.next(); - return null; - }, -}); - -const lightHighlightStyle = HighlightStyle.define([ - { tag: tags.keyword, color: "#0550ae", fontWeight: "600" }, - { tag: tags.string, color: "#0a3069" }, - { tag: tags.number, color: "#8250df" }, - { tag: [tags.typeName, tags.className], color: "#953800" }, - { tag: [tags.variableName, tags.propertyName], color: "#24292f" }, - { tag: tags.function(tags.variableName), color: "#8250df" }, - { tag: tags.operator, color: "#57606a" }, - { tag: tags.comment, color: "#6e7781", fontStyle: "italic" }, - { tag: tags.punctuation, color: "#57606a" }, -]); - -const darkHighlightStyle = HighlightStyle.define([ - { tag: tags.keyword, color: "#79c0ff", fontWeight: "600" }, - { tag: tags.string, color: "#a5d6ff" }, - { tag: tags.number, color: "#d2a8ff" }, - { tag: [tags.typeName, tags.className], color: "#ffa657" }, - { tag: [tags.variableName, tags.propertyName], color: "#e6edf3" }, - { tag: tags.function(tags.variableName), color: "#d2a8ff" }, - { tag: tags.operator, color: "#8b949e" }, - { tag: tags.comment, color: "#8b949e", fontStyle: "italic" }, - { tag: tags.punctuation, color: "#8b949e" }, -]); - -const MONO_FONT = - 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace'; - -const baseThemeRules: Record> = { - "&": { - backgroundColor: "transparent", - color: "var(--text-neutral-primary)", - fontFamily: MONO_FONT, - fontSize: "12px", - }, - "&.cm-focused": { - outline: "none", - }, - ".cm-scroller": { - minHeight: "320px", - overflow: "auto", - fontFamily: MONO_FONT, - lineHeight: "1.5rem", - }, - ".cm-content": { - padding: "16px", - caretColor: "var(--text-neutral-primary)", - }, - ".cm-line": { - padding: "0 0 0 8px", - }, - ".cm-gutters": { - backgroundColor: "var(--bg-neutral-secondary)", - color: "var(--text-neutral-tertiary)", - borderRight: "1px solid var(--border-neutral-secondary)", - minWidth: "44px", - }, - ".cm-lineNumbers .cm-gutterElement": { - padding: "0 10px 0 12px", - }, - ".cm-activeLineGutter": { - backgroundColor: "var(--bg-neutral-secondary)", - color: "var(--text-neutral-secondary)", - }, - ".cm-activeLine": { - backgroundColor: "transparent", - }, - ".cm-cursor, .cm-dropCursor": { - borderLeftColor: "var(--text-neutral-primary)", - }, - ".cm-placeholder": { - color: "var(--text-neutral-tertiary)", - }, -}; - -const LIGHT_SELECTION_BG = "rgba(9, 105, 218, 0.18)"; -const DARK_SELECTION_BG = "rgba(121, 192, 255, 0.18)"; - -const lightTheme = EditorView.theme( - { - ...baseThemeRules, - ".cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection": - { backgroundColor: LIGHT_SELECTION_BG }, - }, - { dark: false }, -); - -const darkTheme = EditorView.theme( - { - ...baseThemeRules, - ".cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection": - { backgroundColor: DARK_SELECTION_BG }, - }, - { dark: true }, -); - -interface QueryCodeEditorProps - extends Omit, "onChange"> { - ariaLabel: string; - language?: "openCypher"; - value: string; - placeholder?: string; - invalid?: boolean; - requirementBadge?: string; - onChange: (value: string) => void; - onBlur?: () => void; -} - -export const QueryCodeEditor = ({ - id, - className, - ariaLabel, - language = "openCypher", - value, - placeholder, - invalid = false, - requirementBadge, - onChange, - onBlur, - ...props -}: QueryCodeEditorProps) => { - const { resolvedTheme } = useTheme(); - const isDarkMode = resolvedTheme === "dark"; - const editorTheme = isDarkMode ? darkTheme : lightTheme; - const editorHighlightStyle = isDarkMode - ? darkHighlightStyle - : lightHighlightStyle; - - return ( -
-
- - {ariaLabel} - - {requirementBadge ? ( - - {requirementBadge} - - ) : null} -
- - { - onBlur?.(); - return false; - }, - }), - ]} - /> -
- ); -}; +export { QueryCodeEditor } from "@/components/shared/query-code-editor"; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-description.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-description.test.tsx index cc61c16a4d..88ec6f139d 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-description.test.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-description.test.tsx @@ -16,27 +16,27 @@ const customQuery: AttackPathQuery = { provider: "aws", attribution: null, documentation_link: { - text: "Cartography schema used by Prowler for AWS graphs", - link: "https://example.com/schema", + text: "Learn how to write custom openCypher queries", + link: "https://example.com/docs", }, parameters: [], }, }; describe("QueryDescription", () => { - it("renders the schema documentation link inside an info alert", () => { + it("renders the documentation link inside an info alert", () => { // Given render(); // When const alert = screen.getByRole("alert"); const link = screen.getByRole("link", { - name: /cartography schema used by prowler for aws graphs/i, + name: /learn how to write custom opencypher queries/i, }); // Then expect(alert).toBeInTheDocument(); - expect(link).toHaveAttribute("href", "https://example.com/schema"); + expect(link).toHaveAttribute("href", "https://example.com/docs"); }); it("does not render unsafe documentation or attribution URLs as clickable links", () => { @@ -46,7 +46,7 @@ describe("QueryDescription", () => { attributes: { ...customQuery.attributes, documentation_link: { - text: "Cartography schema used by Prowler for AWS graphs", + text: "Learn how to write custom openCypher queries", link: "javascript:alert('xss')", }, attribution: { @@ -62,14 +62,14 @@ describe("QueryDescription", () => { // Then expect( screen.queryByRole("link", { - name: /cartography schema used by prowler for aws graphs/i, + name: /learn how to write custom opencypher queries/i, }), ).not.toBeInTheDocument(); expect( screen.queryByRole("link", { name: /unsafe source/i }), ).not.toBeInTheDocument(); expect( - screen.getByText(/cartography schema used by prowler for aws graphs/i), + screen.getByText(/learn how to write custom opencypher queries/i), ).toBeInTheDocument(); expect(screen.getByText(/unsafe source/i)).toBeInTheDocument(); }); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx index 86d87808f9..f6b1007f5a 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx @@ -12,7 +12,6 @@ import { executeQuery, getAttackPathScans, getAvailableQueries, - getCartographySchema, } from "@/actions/attack-paths"; import { adaptQueryResultToGraphData } from "@/actions/attack-paths/query-result.adapter"; import { AutoRefresh } from "@/components/scans"; @@ -144,14 +143,10 @@ export default function AttackPathsPage() { setQueriesLoading(true); try { - const [queriesData, schemaData] = await Promise.all([ - getAvailableQueries(scanId), - getCartographySchema(scanId), - ]); + const queriesData = await getAvailableQueries(scanId); const availableQueries = buildAttackPathQueries( queriesData?.data ?? [], - schemaData?.data.attributes, ); if (availableQueries.length > 0) { diff --git a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx index f8a1122dfd..dd86be1f8a 100644 --- a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx +++ b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx @@ -7,6 +7,7 @@ import { getComplianceRequirements, } from "@/actions/compliances"; import { getThreatScore } from "@/actions/overview"; +import { getScan } from "@/actions/scans"; import { ClientAccordionWrapper, ComplianceDownloadContainer, @@ -37,7 +38,6 @@ interface ComplianceDetailSearchParams { complianceId: string; version?: string; scanId?: string; - scanData?: string; "filter[region__in]"?: string; "filter[cis_profile_level]"?: string; page?: string; @@ -53,7 +53,7 @@ export default async function ComplianceDetail({ }) { const { compliancetitle } = await params; const resolvedSearchParams = await searchParams; - const { complianceId, version, scanId, scanData } = resolvedSearchParams; + const { complianceId, version, scanId } = resolvedSearchParams; const regionFilter = resolvedSearchParams["filter[region__in]"]; const cisProfileFilter = resolvedSearchParams["filter[cis_profile_level]"]; const logoPath = getComplianceIcon(compliancetitle); @@ -72,22 +72,47 @@ export default async function ComplianceDetail({ : `${formattedTitle}`; let selectedScan: ScanEntity | null = null; + const selectedScanId = scanId || null; - if (scanData) { - selectedScan = JSON.parse(decodeURIComponent(scanData)); + const [metadataInfoData, attributesData, selectedScanResponse] = + await Promise.all([ + getComplianceOverviewMetadataInfo({ + filters: { + "filter[scan_id]": selectedScanId, + }, + }), + getComplianceAttributes(complianceId), + selectedScanId + ? getScan(selectedScanId, { include: "provider" }) + : Promise.resolve(null), + ]); + + if (selectedScanResponse?.data) { + const scan = selectedScanResponse.data; + const providerId = scan.relationships?.provider?.data?.id; + const providerData = providerId + ? selectedScanResponse.included?.find( + (item: { type: string; id: string }) => + item.type === "providers" && item.id === providerId, + ) + : undefined; + + if (providerData) { + selectedScan = { + id: scan.id, + providerInfo: { + provider: providerData.attributes.provider, + alias: providerData.attributes.alias, + uid: providerData.attributes.uid, + }, + attributes: { + name: scan.attributes.name, + completed_at: scan.attributes.completed_at, + }, + }; + } } - const selectedScanId = scanId || selectedScan?.id || null; - - const [metadataInfoData, attributesData] = await Promise.all([ - getComplianceOverviewMetadataInfo({ - filters: { - "filter[scan_id]": selectedScanId, - }, - }), - getComplianceAttributes(complianceId), - ]); - const uniqueRegions = metadataInfoData?.data?.attributes?.regions || []; // Detect if this is a ThreatScore compliance view diff --git a/ui/app/(prowler)/findings/page.test.ts b/ui/app/(prowler)/findings/page.test.ts new file mode 100644 index 0000000000..76462dff99 --- /dev/null +++ b/ui/app/(prowler)/findings/page.test.ts @@ -0,0 +1,37 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { describe, expect, it } from "vitest"; + +/** + * Source-level assertions for the findings page. + * + * Directly importing page.tsx triggers deep transitive imports + * (next-auth → next/server) that vitest cannot resolve without the + * full Next.js build pipeline. These tests verify key architectural + * invariants via source analysis instead. + */ +describe("findings page", () => { + const currentDir = path.dirname(fileURLToPath(import.meta.url)); + const pagePath = path.join(currentDir, "page.tsx"); + const source = readFileSync(pagePath, "utf8"); + + it("only passes sort to fetchFindingGroups when the user has an explicit sort param", () => { + expect(source).toContain("...(encodedSort && { sort: encodedSort })"); + }); + + it("normalizes scan filters with the required inserted_at params before fetching historical finding groups", () => { + expect(source).toContain("resolveFindingScanDateFilters"); + }); + + it("uses getLatestFindingGroups for non-date/scan queries and getFindingGroups for historical", () => { + expect(source).toContain("hasDateOrScan"); + expect(source).toContain("getFindingGroups"); + expect(source).toContain("getLatestFindingGroups"); + }); + + it("guards errors array access with a length check", () => { + expect(source).toContain("errors?.length > 0"); + }); +}); diff --git a/ui/app/(prowler)/findings/page.tsx b/ui/app/(prowler)/findings/page.tsx index 576d34ca1e..2211800451 100644 --- a/ui/app/(prowler)/findings/page.tsx +++ b/ui/app/(prowler)/findings/page.tsx @@ -7,7 +7,7 @@ import { } from "@/actions/finding-groups"; import { getLatestMetadataInfo, getMetadataInfo } from "@/actions/findings"; import { getProviders } from "@/actions/providers"; -import { getScans } from "@/actions/scans"; +import { getScan, getScans } from "@/actions/scans"; import { FindingsFilters } from "@/components/findings/findings-filters"; import { FindingsGroupTable, @@ -21,6 +21,7 @@ import { extractSortAndKey, hasDateOrScanFilter, } from "@/lib"; +import { resolveFindingScanDateFilters } from "@/lib/findings-scan-filters"; import { ScanEntity, ScanProps } from "@/types"; import { SearchParamsProps } from "@/types/components"; @@ -39,16 +40,28 @@ export default async function Findings({ // TODO: Re-implement deep link support (/findings?id=) using the grouped view's resource detail drawer // once the legacy FindingDetailsSheet is fully deprecated (still used by /resources and overview dashboard). - const [metadataInfoData, providersData, scansData] = await Promise.all([ - (hasDateOrScan ? getMetadataInfo : getLatestMetadataInfo)({ - query, - sort: encodedSort, - filters, - }), + const [providersData, scansData] = await Promise.all([ getProviders({ pageSize: 50 }), getScans({ pageSize: 50 }), ]); + const filtersWithScanDates = await resolveFindingScanDateFilters({ + filters, + scans: scansData?.data || [], + loadScan: async (scanId: string) => { + const response = await getScan(scanId); + return response?.data; + }, + }); + + const metadataInfoData = await ( + hasDateOrScan ? getMetadataInfo : getLatestMetadataInfo + )({ + query, + sort: encodedSort, + filters: filtersWithScanDates, + }); + // Extract unique regions, services, categories, groups from the new endpoint const uniqueRegions = metadataInfoData?.data?.attributes?.regions || []; const uniqueServices = metadataInfoData?.data?.attributes?.services || []; @@ -88,7 +101,10 @@ export default async function Findings({ />
}> - + @@ -97,19 +113,15 @@ export default async function Findings({ const SSRDataTable = async ({ searchParams, + filters, }: { searchParams: SearchParamsProps; + filters: Record; }) => { const page = parseInt(searchParams.page?.toString() || "1", 10); const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10); - const defaultSort = "-severity,-fail_count,-last_seen_at"; - const { encodedSort } = extractSortAndKey({ - ...searchParams, - sort: searchParams.sort ?? defaultSort, - }); - - const { filters } = extractFiltersAndQuery(searchParams); + const { encodedSort } = extractSortAndKey(searchParams); // Check if the searchParams contain any date or scan filter const hasDateOrScan = hasDateOrScanFilter(searchParams); @@ -119,7 +131,7 @@ const SSRDataTable = async ({ const findingGroupsData = await fetchFindingGroups({ page, - sort: encodedSort, + ...(encodedSort && { sort: encodedSort }), filters, pageSize, }); @@ -131,7 +143,7 @@ const SSRDataTable = async ({ return ( <> - {findingGroupsData?.errors && ( + {findingGroupsData?.errors?.length > 0 && (

Error:

{findingGroupsData.errors[0].detail}

@@ -141,6 +153,8 @@ const SSRDataTable = async ({ key={groupKey} data={groups} metadata={findingGroupsData?.meta} + resolvedFilters={filters} + hasHistoricalData={hasDateOrScan} /> ); diff --git a/ui/app/(prowler)/resources/page.test.ts b/ui/app/(prowler)/resources/page.test.ts new file mode 100644 index 0000000000..72766d17b0 --- /dev/null +++ b/ui/app/(prowler)/resources/page.test.ts @@ -0,0 +1,31 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { describe, expect, it } from "vitest"; + +describe("resources page", () => { + const currentDir = path.dirname(fileURLToPath(import.meta.url)); + const pagePath = path.join(currentDir, "page.tsx"); + const tablePath = path.join( + currentDir, + "../../../components/resources/table/resources-table-with-selection.tsx", + ); + + const pageSource = readFileSync(pagePath, "utf8"); + const tableSource = readFileSync(tablePath, "utf8"); + + it("fetches the deep-linked resource on the server in parallel with the rest of the page data", () => { + expect(pageSource).toContain("getResourceById(initialResourceId"); + expect(pageSource).toContain("await Promise.all"); + expect(pageSource).toContain("initialResource={processedResource}"); + }); + + it("keeps the client table free of deep-link fetch effects", () => { + expect(tableSource).not.toContain("useEffect"); + expect(tableSource).not.toContain("useRef"); + expect(tableSource).not.toContain("getResourceById"); + expect(tableSource).not.toContain("initialResourceId"); + expect(tableSource).toContain("initialResource?: ResourceProps | null"); + }); +}); diff --git a/ui/app/(prowler)/resources/page.tsx b/ui/app/(prowler)/resources/page.tsx index e390b98682..426778ddb5 100644 --- a/ui/app/(prowler)/resources/page.tsx +++ b/ui/app/(prowler)/resources/page.tsx @@ -8,7 +8,6 @@ import { getResourceById, getResources, } from "@/actions/resources"; -import { ResourceDetailsSheet } from "@/components/resources/resource-details-sheet"; import { ResourcesFilters } from "@/components/resources/resources-filters"; import { SkeletonTableResources } from "@/components/resources/skeleton/skeleton-table-resources"; import { ResourcesTableWithSelection } from "@/components/resources/table"; @@ -36,8 +35,7 @@ export default async function Resources({ // Check if the searchParams contain any date or scan filter const hasDateOrScan = hasDateOrScanFilter(resolvedSearchParams); - // Check if there's a specific resource ID to fetch - const resourceId = resolvedSearchParams.resourceId?.toString(); + const initialResourceId = resolvedSearchParams.resourceId?.toString(); const [metadataInfoData, providersData, resourceByIdData] = await Promise.all( [ @@ -47,35 +45,33 @@ export default async function Resources({ sort: encodedSort, }), getProviders({ pageSize: 50 }), - resourceId - ? getResourceById(resourceId, { include: ["provider"] }) - : Promise.resolve(null), + initialResourceId + ? getResourceById(initialResourceId, { include: ["provider"] }) + : Promise.resolve(undefined), ], ); - // Process the resource data to match the expected structure const processedResource = resourceByIdData?.data ? (() => { const resource = resourceByIdData.data; const providerDict = createDict("providers", resourceByIdData); - const provider = { - data: providerDict[resource.relationships?.provider?.data?.id], - }; - return { ...resource, relationships: { ...resource.relationships, - provider, + provider: { + data: providerDict[resource.relationships.provider.data.id], + }, }, - } as ResourceProps; + } satisfies ResourceProps; })() : null; // Extract unique regions, services, groups from the metadata endpoint const uniqueRegions = metadataInfoData?.data?.attributes?.regions || []; const uniqueServices = metadataInfoData?.data?.attributes?.services || []; + const uniqueResourceTypes = metadataInfoData?.data?.attributes?.types || []; const uniqueGroups = metadataInfoData?.data?.attributes?.groups || []; return ( @@ -86,24 +82,27 @@ export default async function Resources({ providers={providersData?.data || []} uniqueRegions={uniqueRegions} uniqueServices={uniqueServices} + uniqueResourceTypes={uniqueResourceTypes} uniqueGroups={uniqueGroups} />
}> - + - {processedResource && ( - - )} ); } const SSRDataTable = async ({ searchParams, + initialResource, }: { searchParams: SearchParamsProps; + initialResource?: ResourceProps | null; }) => { const page = parseInt(searchParams.page?.toString() || "1", 10); const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10); @@ -175,6 +174,7 @@ const SSRDataTable = async ({ ); diff --git a/ui/app/(prowler)/scans/page.tsx b/ui/app/(prowler)/scans/page.tsx index 2d0416f37c..55dbf7eb8a 100644 --- a/ui/app/(prowler)/scans/page.tsx +++ b/ui/app/(prowler)/scans/page.tsx @@ -18,7 +18,12 @@ import { createProviderDetailsMapping, extractProviderUIDs, } from "@/lib/provider-helpers"; -import { ProviderProps, ScanProps, SearchParamsProps } from "@/types"; +import { + ExpandedScanData, + ProviderProps, + ScanProps, + SearchParamsProps, +} from "@/types"; export default async function Scans({ searchParams, @@ -30,7 +35,34 @@ export default async function Scans({ const filteredParams = { ...resolvedSearchParams }; delete filteredParams.scanId; - const providersData = await getAllProviders(); + const [providersData, completedScansData] = await Promise.all([ + getAllProviders(), + getScans({ + filters: { "filter[state]": "completed" }, + pageSize: 50, + fields: { scans: "name,completed_at,provider" }, + include: "provider", + }), + ]); + + const completedScans: ExpandedScanData[] = (completedScansData?.data ?? []) + .map((scan: ScanProps) => { + const providerId = scan.relationships?.provider?.data?.id; + const providerData = completedScansData?.included?.find( + (item: { type: string; id: string }) => + item.type === "providers" && item.id === providerId, + ); + if (!providerData) return null; + return { + ...scan, + providerInfo: { + provider: providerData.attributes.provider, + uid: providerData.attributes.uid, + alias: providerData.attributes.alias, + }, + }; + }) + .filter(Boolean) as ExpandedScanData[]; const providerInfo = providersData?.data @@ -90,6 +122,7 @@ export default async function Scans({
diff --git a/ui/auth.config.ts b/ui/auth.config.ts index 4bf7a660cb..c43ff55cc6 100644 --- a/ui/auth.config.ts +++ b/ui/auth.config.ts @@ -281,15 +281,20 @@ export const authConfig = { const sessionError = auth?.error; const isSignUpPage = nextUrl.pathname === "/sign-up"; const isSignInPage = nextUrl.pathname === "/sign-in"; + const isInvitationPage = + nextUrl.pathname.startsWith("/invitation/accept"); - // Allow access to sign-up and sign-in pages - if (isSignUpPage || isSignInPage) return true; + // Allow access to sign-up, sign-in, and invitation pages + if (isSignUpPage || isSignInPage || isInvitationPage) return true; // For all other routes, require authentication // Return NextResponse.redirect to preserve callbackUrl for post-login redirect if (!isLoggedIn) { const signInUrl = new URL("/sign-in", nextUrl.origin); - signInUrl.searchParams.set("callbackUrl", nextUrl.pathname); + signInUrl.searchParams.set( + "callbackUrl", + nextUrl.pathname + nextUrl.search, + ); // Include session error if present (e.g., RefreshAccessTokenError) if (sessionError) { signInUrl.searchParams.set("error", sessionError); diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx index d700821ed6..5d1b0425ba 100644 --- a/ui/components/compliance/compliance-card.tsx +++ b/ui/components/compliance/compliance-card.tsx @@ -32,7 +32,6 @@ export const ComplianceCard: React.FC = ({ scanId, complianceId, id, - selectedScan, }) => { const searchParams = useSearchParams(); const router = useRouter(); @@ -65,17 +64,6 @@ export const ComplianceCard: React.FC = ({ params.set("version", version); params.set("scanId", scanId); - if (selectedScan) { - params.set( - "scanData", - JSON.stringify({ - id: selectedScan.id, - providerInfo: selectedScan.providerInfo, - attributes: selectedScan.attributes, - }), - ); - } - const regionFilter = searchParams.get("filter[region__in]"); if (regionFilter) { params.set("filter[region__in]", regionFilter); diff --git a/ui/components/compliance/threatscore-badge.tsx b/ui/components/compliance/threatscore-badge.tsx index a047f91bdd..feb2aec91b 100644 --- a/ui/components/compliance/threatscore-badge.tsx +++ b/ui/components/compliance/threatscore-badge.tsx @@ -40,7 +40,6 @@ export const ThreatScoreBadge = ({ score, scanId, provider, - selectedScan, sectionScores, }: ThreatScoreBadgeProps) => { const router = useRouter(); @@ -62,17 +61,6 @@ export const ThreatScoreBadge = ({ params.set("version", version); params.set("scanId", scanId); - if (selectedScan) { - params.set( - "scanData", - JSON.stringify({ - id: selectedScan.id, - providerInfo: selectedScan.providerInfo, - attributes: selectedScan.attributes, - }), - ); - } - const regionFilter = searchParams.get("filter[region__in]"); if (regionFilter) { params.set("filter[region__in]", regionFilter); diff --git a/ui/components/findings/findings-filters.tsx b/ui/components/findings/findings-filters.tsx index af169bfcc1..6316e49203 100644 --- a/ui/components/findings/findings-filters.tsx +++ b/ui/components/findings/findings-filters.tsx @@ -18,11 +18,12 @@ import { Button } from "@/components/shadcn"; import { ExpandableSection } from "@/components/ui/expandable-section"; import { DataTableFilterCustom } from "@/components/ui/table"; import { useFilterBatch } from "@/hooks/use-filter-batch"; -import { formatLabel, getCategoryLabel, getGroupLabel } from "@/lib/categories"; -import { FilterType, FINDING_STATUS_DISPLAY_NAMES, ScanEntity } from "@/types"; +import { getCategoryLabel, getGroupLabel } from "@/lib/categories"; +import { FilterType, ScanEntity } from "@/types"; import { DATA_TABLE_FILTER_MODE, FilterParam } from "@/types/filters"; -import { getProviderDisplayName, ProviderProps } from "@/types/providers"; -import { SEVERITY_DISPLAY_NAMES } from "@/types/severities"; +import { ProviderProps } from "@/types/providers"; + +import { getFindingsFilterDisplayValue } from "./findings-filters.utils"; interface FindingsFiltersProps { /** Provider data for ProviderTypeSelector and AccountsSelector */ @@ -53,54 +54,13 @@ const FILTER_KEY_LABELS: Record = { "filter[resource_type__in]": "Resource Type", "filter[category__in]": "Category", "filter[resource_groups__in]": "Resource Group", - "filter[scan__in]": "Scan ID", + "filter[scan__in]": "Scan", + "filter[scan_id]": "Scan", + "filter[scan_id__in]": "Scan", "filter[inserted_at]": "Date", "filter[muted]": "Muted", }; -/** - * Formats a raw filter value into a human-readable display string. - * - Provider types: uses shared getProviderDisplayName utility - * - Severities: uses shared SEVERITY_DISPLAY_NAMES (e.g. "critical" → "Critical") - * - Status: uses shared FINDING_STATUS_DISPLAY_NAMES (e.g. "FAIL" → "Fail") - * - Categories: uses getCategoryLabel (handles IAM, EC2, IMDSv1, etc.) - * - Resource groups: uses getGroupLabel (underscore-delimited) - * - Date (filter[inserted_at]): returns the ISO date string as-is (YYYY-MM-DD) - * - Other values: uses formatLabel as a generic fallback (avoids naive capitalisation) - */ -const formatFilterValue = (filterKey: string, value: string): string => { - if (!value) return value; - if (filterKey === "filter[provider_type__in]") { - return getProviderDisplayName(value); - } - if (filterKey === "filter[severity__in]") { - return ( - SEVERITY_DISPLAY_NAMES[ - value.toLowerCase() as keyof typeof SEVERITY_DISPLAY_NAMES - ] ?? formatLabel(value) - ); - } - if (filterKey === "filter[status__in]") { - return ( - FINDING_STATUS_DISPLAY_NAMES[ - value as keyof typeof FINDING_STATUS_DISPLAY_NAMES - ] ?? formatLabel(value) - ); - } - if (filterKey === "filter[category__in]") { - return getCategoryLabel(value); - } - if (filterKey === "filter[resource_groups__in]") { - return getGroupLabel(value); - } - // Date filter: preserve ISO date string (YYYY-MM-DD) — do not run through formatLabel - if (filterKey === "filter[inserted_at]") { - return value; - } - // Generic fallback: handles hyphen/underscore-delimited IDs with smart capitalisation - return formatLabel(value); -}; - export const FindingsFilters = ({ providers, completedScanIds, @@ -128,7 +88,14 @@ export const FindingsFilters = ({ // Custom filters for the expandable section (removed Provider - now using AccountsSelector) const customFilters = [ - ...filterFindings, + ...filterFindings.map((filter) => ({ + ...filter, + labelFormatter: (value: string) => + getFindingsFilterDisplayValue(`filter[${filter.key}]`, value, { + providers, + scans: scanDetails, + }), + })), { key: FilterType.REGION, labelCheckboxGroup: "Regions", @@ -166,6 +133,11 @@ export const FindingsFilters = ({ labelCheckboxGroup: "Scan ID", values: completedScanIds, valueLabelMapping: scanDetails, + labelFormatter: (value: string) => + getFindingsFilterDisplayValue(`filter[${FilterType.SCAN}]`, value, { + providers, + scans: scanDetails, + }), index: 7, }, ]; @@ -185,7 +157,10 @@ export const FindingsFilters = ({ key, label, value, - displayValue: formatFilterValue(key, value), + displayValue: getFindingsFilterDisplayValue(key, value, { + providers, + scans: scanDetails, + }), }); }); }); diff --git a/ui/components/findings/findings-filters.utils.test.ts b/ui/components/findings/findings-filters.utils.test.ts new file mode 100644 index 0000000000..b4dc63c85b --- /dev/null +++ b/ui/components/findings/findings-filters.utils.test.ts @@ -0,0 +1,187 @@ +import { describe, expect, it } from "vitest"; + +import { ProviderProps } from "@/types/providers"; +import { ScanEntity } from "@/types/scans"; + +import { getFindingsFilterDisplayValue } from "./findings-filters.utils"; + +function makeProvider( + overrides: Partial & { id: string }, +): ProviderProps { + return { + type: "providers", + attributes: { + provider: "aws", + uid: "123456789012", + alias: "Production Account", + status: "completed", + resources: 10, + connection: { connected: true, last_checked_at: "2026-04-07T10:00:00Z" }, + scanner_args: { + only_logs: false, + excluded_checks: [], + aws_retries_max_attempts: 3, + }, + inserted_at: "2026-04-07T10:00:00Z", + updated_at: "2026-04-07T10:00:00Z", + created_by: { object: "user", id: "user-1" }, + }, + relationships: { + secret: { data: null }, + provider_groups: { meta: { count: 0 }, data: [] }, + }, + ...overrides, + } as ProviderProps; +} + +function makeScanMap( + scanId: string, + overrides?: Partial, +): { [scanId: string]: ScanEntity } { + return { + [scanId]: { + id: scanId, + providerInfo: { + provider: "aws", + alias: "Scan Account", + uid: "123456789012", + }, + attributes: { + name: "Nightly scan", + completed_at: "2026-04-07T10:00:00Z", + }, + ...overrides, + }, + }; +} + +const providers = [makeProvider({ id: "provider-1" })]; +const scans = [makeScanMap("scan-1")]; + +describe("getFindingsFilterDisplayValue", () => { + it("shows the account alias for provider_id filters instead of the raw provider id", () => { + expect( + getFindingsFilterDisplayValue("filter[provider_id__in]", "provider-1", { + providers, + }), + ).toBe("Production Account"); + }); + + it("falls back to the provider uid when the alias is empty", () => { + expect( + getFindingsFilterDisplayValue("filter[provider_id__in]", "provider-2", { + providers: [ + ...providers, + makeProvider({ + id: "provider-2", + attributes: { + ...providers[0].attributes, + alias: "", + uid: "210987654321", + }, + }), + ], + }), + ).toBe("210987654321"); + }); + + it("keeps the raw value when the provider cannot be resolved", () => { + expect( + getFindingsFilterDisplayValue( + "filter[provider_id__in]", + "missing-provider", + { providers }, + ), + ).toBe("missing-provider"); + }); + + it("shows the resolved scan badge label for scan filters instead of formatting the raw scan id", () => { + expect( + getFindingsFilterDisplayValue("filter[scan__in]", "scan-1", { scans }), + ).toBe("Nightly scan"); + }); + + it("normalizes finding statuses for display", () => { + expect(getFindingsFilterDisplayValue("filter[status__in]", "FAIL")).toBe( + "Fail", + ); + }); + + it("normalizes severities for display", () => { + expect( + getFindingsFilterDisplayValue("filter[severity__in]", "critical"), + ).toBe("Critical"); + }); + + it("formats delta values for display", () => { + expect(getFindingsFilterDisplayValue("filter[delta__in]", "new")).toBe( + "New", + ); + }); + + it("falls back to the scan provider uid when the alias is missing", () => { + expect( + getFindingsFilterDisplayValue("filter[scan__in]", "scan-2", { + scans: [ + ...scans, + makeScanMap("scan-2", { + providerInfo: { provider: "aws", uid: "210987654321" }, + attributes: { + name: "Weekly scan", + completed_at: "2026-04-08T10:00:00Z", + }, + }), + ], + }), + ).toBe("Weekly scan"); + }); + + it("falls back to the provider alias when the scan name is missing", () => { + expect( + getFindingsFilterDisplayValue("filter[scan__in]", "scan-3", { + scans: [ + ...scans, + makeScanMap("scan-3", { + providerInfo: { + provider: "aws", + alias: "Fallback Account", + uid: "333333333333", + }, + attributes: { + name: "", + completed_at: "2026-04-08T10:00:00Z", + }, + }), + ], + }), + ).toBe("Fallback Account"); + }); + + it("keeps the raw scan value when the scan cannot be resolved", () => { + expect( + getFindingsFilterDisplayValue("filter[scan__in]", "missing-scan", { + scans, + }), + ).toBe("missing-scan"); + }); + + it("passes through date values for inserted_at__gte filters", () => { + expect( + getFindingsFilterDisplayValue( + "filter[inserted_at__gte]", + "2026-04-03", + {}, + ), + ).toBe("2026-04-03"); + }); + + it("passes through date values for inserted_at__lte filters", () => { + expect( + getFindingsFilterDisplayValue( + "filter[inserted_at__lte]", + "2026-04-07", + {}, + ), + ).toBe("2026-04-07"); + }); +}); diff --git a/ui/components/findings/findings-filters.utils.ts b/ui/components/findings/findings-filters.utils.ts new file mode 100644 index 0000000000..2599650c1d --- /dev/null +++ b/ui/components/findings/findings-filters.utils.ts @@ -0,0 +1,95 @@ +import { formatLabel, getCategoryLabel, getGroupLabel } from "@/lib/categories"; +import { FINDING_STATUS_DISPLAY_NAMES } from "@/types"; +import { getProviderDisplayName, ProviderProps } from "@/types/providers"; +import { ScanEntity } from "@/types/scans"; +import { SEVERITY_DISPLAY_NAMES } from "@/types/severities"; + +interface GetFindingsFilterDisplayValueOptions { + providers?: ProviderProps[]; + scans?: Array<{ [scanId: string]: ScanEntity }>; +} + +const FINDING_DELTA_DISPLAY_NAMES: Record = { + new: "New", + changed: "Changed", +}; + +function getProviderAccountDisplayValue( + providerId: string, + providers: ProviderProps[], +): string { + const provider = providers.find((item) => item.id === providerId); + if (!provider) { + return providerId; + } + + return provider.attributes.alias || provider.attributes.uid || providerId; +} + +function getScanDisplayValue( + scanId: string, + scans: Array<{ [scanId: string]: ScanEntity }>, +): string { + const scan = scans.find((item) => item[scanId])?.[scanId]; + if (!scan) { + return scanId; + } + + return ( + scan.attributes.name || + scan.providerInfo.alias || + scan.providerInfo.uid || + scanId + ); +} + +export function getFindingsFilterDisplayValue( + filterKey: string, + value: string, + options: GetFindingsFilterDisplayValueOptions = {}, +): string { + if (!value) return value; + if (filterKey === "filter[provider_type__in]") { + return getProviderDisplayName(value); + } + if (filterKey === "filter[provider_id__in]") { + return getProviderAccountDisplayValue(value, options.providers || []); + } + if (filterKey === "filter[scan__in]") { + return getScanDisplayValue(value, options.scans || []); + } + if (filterKey === "filter[severity__in]") { + return ( + SEVERITY_DISPLAY_NAMES[ + value.toLowerCase() as keyof typeof SEVERITY_DISPLAY_NAMES + ] ?? formatLabel(value) + ); + } + if (filterKey === "filter[status__in]") { + return ( + FINDING_STATUS_DISPLAY_NAMES[ + value as keyof typeof FINDING_STATUS_DISPLAY_NAMES + ] ?? formatLabel(value) + ); + } + if (filterKey === "filter[delta__in]") { + return ( + FINDING_DELTA_DISPLAY_NAMES[value.toLowerCase()] ?? formatLabel(value) + ); + } + if (filterKey === "filter[category__in]") { + return getCategoryLabel(value); + } + if (filterKey === "filter[resource_groups__in]") { + return getGroupLabel(value); + } + if ( + filterKey === "filter[inserted_at]" || + filterKey === "filter[inserted_at__gte]" || + filterKey === "filter[inserted_at__lte]" + ) { + return value; + } + + return formatLabel(value); +} diff --git a/ui/components/findings/floating-mute-button.test.tsx b/ui/components/findings/floating-mute-button.test.tsx index 0123e2e44e..b953b47b43 100644 --- a/ui/components/findings/floating-mute-button.test.tsx +++ b/ui/components/findings/floating-mute-button.test.tsx @@ -24,6 +24,17 @@ vi.mock("next/navigation", () => ({ import { FloatingMuteButton } from "./floating-mute-button"; +function deferredPromise() { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { promise, resolve, reject }; +} + // --------------------------------------------------------------------------- // Fix 3: onBeforeOpen rejection resets isResolving // --------------------------------------------------------------------------- @@ -31,7 +42,6 @@ import { FloatingMuteButton } from "./floating-mute-button"; describe("FloatingMuteButton — onBeforeOpen error handling", () => { beforeEach(() => { vi.clearAllMocks(); - vi.spyOn(console, "error").mockImplementation(() => {}); }); it("should reset isResolving (re-enable button) when onBeforeOpen rejects", async () => { @@ -58,11 +68,9 @@ describe("FloatingMuteButton — onBeforeOpen error handling", () => { }); }); - it("should log the error when onBeforeOpen rejects", async () => { + it("should show the preparation error in the modal when onBeforeOpen rejects", async () => { // Given - const error = new Error("Fetch failed"); - const onBeforeOpen = vi.fn().mockRejectedValue(error); - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const onBeforeOpen = vi.fn().mockRejectedValue(new Error("Fetch failed")); const user = userEvent.setup(); render( @@ -76,9 +84,26 @@ describe("FloatingMuteButton — onBeforeOpen error handling", () => { // When await user.click(screen.getByRole("button")); - // Then — error was logged + // Then await waitFor(() => { - expect(consoleSpy).toHaveBeenCalled(); + const lastCall = ( + MuteFindingsModalMock.mock.calls as unknown as Array< + [ + { + isOpen: boolean; + isPreparing?: boolean; + preparationError?: string | null; + }, + ] + > + ).at(-1); + + expect(lastCall?.[0]).toMatchObject({ + isOpen: true, + isPreparing: false, + preparationError: + "We couldn't prepare this mute action. Please try again.", + }); }); }); @@ -102,10 +127,76 @@ describe("FloatingMuteButton — onBeforeOpen error handling", () => { await waitFor(() => { const lastCall = ( MuteFindingsModalMock.mock.calls as unknown as Array< - [{ isOpen: boolean; findingIds: string[] }] + [ + { + isOpen: boolean; + findingIds: string[]; + isPreparing?: boolean; + }, + ] > ).at(-1); expect(lastCall?.[0]?.isOpen).toBe(true); }); }); + + it("should open the modal immediately in preparing state while IDs are still resolving", async () => { + // Given + const deferred = deferredPromise(); + const onBeforeOpen = vi.fn().mockReturnValue(deferred.promise); + const user = userEvent.setup(); + + render( + , + ); + + // When + await user.click(screen.getByRole("button")); + + // Then + const preparingCall = ( + MuteFindingsModalMock.mock.calls as unknown as Array< + [ + { + isOpen: boolean; + findingIds: string[]; + isPreparing?: boolean; + }, + ] + > + ).at(-1); + + expect(preparingCall?.[0]).toMatchObject({ + isOpen: true, + isPreparing: true, + findingIds: [], + }); + + // And when the IDs resolve + deferred.resolve(["id-1", "id-2"]); + + await waitFor(() => { + const resolvedCall = ( + MuteFindingsModalMock.mock.calls as unknown as Array< + [ + { + isOpen: boolean; + findingIds: string[]; + isPreparing?: boolean; + }, + ] + > + ).at(-1); + + expect(resolvedCall?.[0]).toMatchObject({ + isOpen: true, + isPreparing: false, + findingIds: ["id-1", "id-2"], + }); + }); + }); }); diff --git a/ui/components/findings/floating-mute-button.tsx b/ui/components/findings/floating-mute-button.tsx index f3b7933ce8..b6c779370e 100644 --- a/ui/components/findings/floating-mute-button.tsx +++ b/ui/components/findings/floating-mute-button.tsx @@ -31,22 +31,46 @@ export function FloatingMuteButton({ const [isModalOpen, setIsModalOpen] = useState(false); const [resolvedIds, setResolvedIds] = useState([]); const [isResolving, setIsResolving] = useState(false); + const [isPreparingMuteModal, setIsPreparingMuteModal] = useState(false); + const [mutePreparationError, setMutePreparationError] = useState< + string | null + >(null); + + const handleModalOpenChange = ( + nextOpen: boolean | ((previousOpen: boolean) => boolean), + ) => { + const resolvedOpen = + typeof nextOpen === "function" ? nextOpen(isModalOpen) : nextOpen; + setIsModalOpen(resolvedOpen); + + if (!resolvedOpen) { + setResolvedIds([]); + setIsPreparingMuteModal(false); + setMutePreparationError(null); + } + }; const handleClick = async () => { if (onBeforeOpen) { + setResolvedIds([]); + setMutePreparationError(null); + setIsPreparingMuteModal(true); + setIsModalOpen(true); setIsResolving(true); try { const ids = await onBeforeOpen(); setResolvedIds(ids); - if (ids.length > 0) { - setIsModalOpen(true); - } - } catch (error) { - console.error( - "FloatingMuteButton: failed to resolve finding IDs", - error, + setMutePreparationError( + ids.length === 0 + ? "No findings could be resolved for this selection. Try refreshing the page and trying again." + : null, + ); + } catch { + setMutePreparationError( + "We couldn't prepare this mute action. Please try again.", ); } finally { + setIsPreparingMuteModal(false); setIsResolving(false); } } else { @@ -65,10 +89,12 @@ export function FloatingMuteButton({ <>
diff --git a/ui/components/findings/mute-findings-modal.test.tsx b/ui/components/findings/mute-findings-modal.test.tsx new file mode 100644 index 0000000000..dbcec60517 --- /dev/null +++ b/ui/components/findings/mute-findings-modal.test.tsx @@ -0,0 +1,165 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import type { ReactNode } from "react"; +import { describe, expect, it, vi } from "vitest"; + +const { createMuteRuleMock, toastMock } = vi.hoisted(() => ({ + createMuteRuleMock: vi.fn(), + toastMock: vi.fn(), +})); + +vi.mock("@/actions/mute-rules", () => ({ + createMuteRule: createMuteRuleMock, +})); + +vi.mock("@/components/shadcn", () => ({ + Button: ({ + children, + ...props + }: React.ButtonHTMLAttributes) => ( + + ), + Input: (props: React.InputHTMLAttributes) => ( + + ), + Textarea: (props: React.TextareaHTMLAttributes) => ( +