diff --git a/.github/workflows/api-security.yml b/.github/workflows/api-security.yml index e8a8efc879..cf5403df99 100644 --- a/.github/workflows/api-security.yml +++ b/.github/workflows/api-security.yml @@ -1,14 +1,14 @@ -name: 'API: Security' +name: "API: Security" on: push: branches: - - 'master' - - 'v5.*' + - "master" + - "v5.*" pull_request: branches: - - 'master' - - 'v5.*' + - "master" + - "v5.*" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -26,7 +26,7 @@ jobs: strategy: matrix: python-version: - - '3.12' + - "3.12" defaults: run: working-directory: ./api @@ -61,8 +61,9 @@ jobs: - name: Safety if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run safety check --ignore 79023,79027 + run: poetry run safety check --ignore 79023,79027,84420 # TODO: 79023 & 79027 knack ReDoS until `azure-cli-core` (via `cartography`) allows `knack` >=0.13.0 + # TODO: 84420 from `azure-core`, that we need fix alltogether with `azure-cli-core` and `knack` - name: Vulture if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 85ccd34cc2..1f44080229 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -51,18 +51,16 @@ jobs: "amitsharm" "andoniaf" "cesararroba" - "Chan9390" "danibarranqueroo" "HugoPBrito" "jfagoagas" - "josemazo" + "josema-xyz" "lydiavilchez" "mmuller88" - "MrCloudSec" + # "MrCloudSec" "pedrooot" "prowler-bot" "puchy22" - "rakan-pro" "RosaRivasProwler" "StylusFrost" "toniblyx" diff --git a/.github/workflows/sdk-refresh-oci-regions.yml b/.github/workflows/sdk-refresh-oci-regions.yml new file mode 100644 index 0000000000..35cbf37828 --- /dev/null +++ b/.github/workflows/sdk-refresh-oci-regions.yml @@ -0,0 +1,93 @@ +name: 'SDK: Refresh OCI Regions' + +on: + schedule: + - cron: '0 9 * * 1' # Every Monday at 09:00 UTC + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +env: + PYTHON_VERSION: '3.12' + +jobs: + refresh-oci-regions: + if: github.repository == 'prowler-cloud/prowler' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + pull-requests: write + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: 'master' + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + + - name: Install dependencies + run: pip install oci + + - name: Update OCI regions + env: + OCI_CLI_USER: ${{ secrets.E2E_OCI_USER_ID }} + OCI_CLI_FINGERPRINT: ${{ secrets.E2E_OCI_FINGERPRINT }} + OCI_CLI_TENANCY: ${{ secrets.E2E_OCI_TENANCY_ID }} + OCI_CLI_KEY_CONTENT: ${{ secrets.E2E_OCI_KEY_CONTENT }} + OCI_CLI_REGION: ${{ secrets.E2E_OCI_REGION }} + run: python util/update_oci_regions.py + + - name: Create pull request + id: create-pr + uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + with: + token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} + author: 'prowler-bot <179230569+prowler-bot@users.noreply.github.com>' + committer: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>' + commit-message: 'feat(oraclecloud): update commercial regions' + branch: 'oci-regions-update-${{ github.run_number }}' + title: 'feat(oraclecloud): Update commercial regions' + labels: | + status/waiting-for-revision + no-changelog + body: | + ### Description + + Automated update of OCI commercial regions from the official Oracle Cloud Infrastructure Identity service. + + **Trigger:** ${{ github.event_name == 'schedule' && 'Scheduled (weekly)' || github.event_name == 'workflow_dispatch' && 'Manual' || 'Workflow update' }} + **Run:** [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + ### Changes + + This PR updates the `OCI_COMMERCIAL_REGIONS` dictionary in `prowler/providers/oraclecloud/config.py` with the latest regions fetched from the OCI Identity API (`list_regions()`). + + - Government regions (`OCI_GOVERNMENT_REGIONS`) are preserved unchanged + - Region display names are mapped from Oracle's official documentation + + ### Checklist + + - [x] This is an automated update from OCI official sources + - [x] Government regions (us-langley-1, us-luke-1) preserved + - [x] No manual review of region data required + + ### License + + By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. + + - name: PR creation result + run: | + if [[ "${{ steps.create-pr.outputs.pull-request-number }}" ]]; then + echo "✓ Pull request #${{ steps.create-pr.outputs.pull-request-number }} created successfully" + echo "URL: ${{ steps.create-pr.outputs.pull-request-url }}" + else + echo "✓ No changes detected - OCI regions are up to date" + fi diff --git a/.github/workflows/ui-e2e-tests-v2.yml b/.github/workflows/ui-e2e-tests-v2.yml index caa712f3dd..9936737320 100644 --- a/.github/workflows/ui-e2e-tests-v2.yml +++ b/.github/workflows/ui-e2e-tests-v2.yml @@ -65,6 +65,10 @@ jobs: E2E_OCI_KEY_CONTENT: ${{ secrets.E2E_OCI_KEY_CONTENT }} E2E_OCI_REGION: ${{ secrets.E2E_OCI_REGION }} E2E_NEW_USER_PASSWORD: ${{ secrets.E2E_NEW_USER_PASSWORD }} + E2E_ALIBABACLOUD_ACCOUNT_ID: ${{ secrets.E2E_ALIBABACLOUD_ACCOUNT_ID }} + E2E_ALIBABACLOUD_ACCESS_KEY_ID: ${{ secrets.E2E_ALIBABACLOUD_ACCESS_KEY_ID }} + E2E_ALIBABACLOUD_ACCESS_KEY_SECRET: ${{ secrets.E2E_ALIBABACLOUD_ACCESS_KEY_SECRET }} + E2E_ALIBABACLOUD_ROLE_ARN: ${{ secrets.E2E_ALIBABACLOUD_ROLE_ARN }} # Pass E2E paths from impact analysis E2E_TEST_PATHS: ${{ needs.impact-analysis.outputs.ui-e2e }} RUN_ALL_TESTS: ${{ needs.impact-analysis.outputs.run-all }} diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index e7d1834f91..cc1a318277 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -44,6 +44,35 @@ jobs: ui/README.md ui/AGENTS.md + - name: Get changed source files for targeted tests + id: changed-source + if: steps.check-changes.outputs.any_changed == 'true' + uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + with: + files: | + ui/**/*.ts + ui/**/*.tsx + files_ignore: | + ui/**/*.test.ts + ui/**/*.test.tsx + ui/**/*.spec.ts + ui/**/*.spec.tsx + ui/vitest.config.ts + ui/vitest.setup.ts + + - 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@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + with: + files: | + ui/lib/** + ui/types/** + ui/config/** + ui/middleware.ts + ui/vitest.config.ts + ui/vitest.setup.ts + - name: Setup Node.js ${{ env.NODE_VERSION }} if: steps.check-changes.outputs.any_changed == 'true' uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 @@ -83,6 +112,27 @@ jobs: if: steps.check-changes.outputs.any_changed == 'true' run: pnpm run healthcheck + - name: Run unit tests (all - critical paths changed) + if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed == 'true' + run: | + echo "Critical paths changed - running ALL unit tests" + pnpm run test:run + + - name: Run unit tests (related to changes only) + if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed != 'true' && steps.changed-source.outputs.all_changed_files != '' + run: | + echo "Running tests related to changed files:" + echo "${{ steps.changed-source.outputs.all_changed_files }}" + # Convert space-separated to vitest related format (remove ui/ prefix for relative paths) + CHANGED_FILES=$(echo "${{ steps.changed-source.outputs.all_changed_files }}" | tr ' ' '\n' | sed 's|^ui/||' | tr '\n' ' ') + pnpm exec vitest related $CHANGED_FILES --run + + - name: Run unit tests (test files only changed) + if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed != 'true' && steps.changed-source.outputs.all_changed_files == '' + run: | + echo "Only test files changed - running ALL unit tests" + pnpm run test:run + - name: Build application if: steps.check-changes.outputs.any_changed == 'true' run: pnpm run build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a4164bc43..0aaf524047 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -85,7 +85,6 @@ repos: args: ["--directory=./"] pass_filenames: false - - repo: https://github.com/hadolint/hadolint rev: v2.13.0-beta hooks: @@ -121,7 +120,8 @@ repos: description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities" # 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 - entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745,79023,79027' + # TODO: 84420 from `azure-core`, that we need fix alltogether with `azure-cli-core` and `knack` + entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745,79023,79027,84420' language: system - id: vulture diff --git a/AGENTS.md b/AGENTS.md index 5d31782ef1..c0b5209a73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,8 @@ Use these skills for detailed patterns on-demand: | `zod-4` | New API (z.email(), z.uuid()) | [SKILL.md](skills/zod-4/SKILL.md) | | `zustand-5` | Persist, selectors, slices | [SKILL.md](skills/zustand-5/SKILL.md) | | `ai-sdk-5` | UIMessage, streaming, LangChain | [SKILL.md](skills/ai-sdk-5/SKILL.md) | +| `vitest` | Unit testing, React Testing Library | [SKILL.md](skills/vitest/SKILL.md) | +| `tdd` | Test-Driven Development workflow | [SKILL.md](skills/tdd/SKILL.md) | ### Prowler-Specific Skills | Skill | Description | URL | @@ -56,8 +58,8 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Add changelog entry for a PR or feature | `prowler-changelog` | | Adding DRF pagination or permissions | `django-drf` | | Adding new providers | `prowler-provider` | -| Adding services to existing providers | `prowler-provider` | | Adding privilege escalation detection queries | `prowler-attack-paths-query` | +| Adding services to existing providers | `prowler-provider` | | After creating/modifying a skill | `skill-sync` | | App Router / Server Actions | `nextjs-15` | | Building AI chat features | `ai-sdk-5` | @@ -76,30 +78,38 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Creating/updating compliance frameworks | `prowler-compliance` | | Debug why a GitHub Actions job is failing | `prowler-ci` | | Fill .github/pull_request_template.md (Context/Description/Steps to review/Checklist) | `prowler-pr` | +| Fixing bug | `tdd` | | General Prowler development questions | `prowler` | | Implementing JSON:API endpoints | `django-drf` | +| Implementing feature | `tdd` | | Inspect PR CI checks and gates (.github/workflows/*) | `prowler-ci` | | Inspect PR CI workflows (.github/workflows/*): conventional-commit, pr-check-changelog, pr-conflict-checker, labeler | `prowler-pr` | | Mapping checks to compliance controls | `prowler-compliance` | | Mocking AWS with moto in tests | `prowler-test-sdk` | | Modifying API responses | `jsonapi` | +| Modifying component | `tdd` | +| Refactoring code | `tdd` | | Regenerate AGENTS.md Auto-invoke tables (sync.sh) | `skill-sync` | | Review PR requirements: template, title conventions, changelog gate | `prowler-pr` | | Review changelog format and conventions | `prowler-changelog` | | Reviewing JSON:API compliance | `jsonapi` | | Reviewing compliance framework PRs | `prowler-compliance-review` | | Testing RLS tenant isolation | `prowler-test-api` | +| Testing hooks or utilities | `vitest` | | Troubleshoot why a skill is missing from AGENTS.md auto-invoke | `skill-sync` | | Understand CODEOWNERS/labeler-based automation | `prowler-ci` | | Understand PR title conventional-commit validation | `prowler-ci` | | Understand changelog gate and no-changelog label behavior | `prowler-ci` | | Understand review ownership with CODEOWNERS | `prowler-pr` | | Update CHANGELOG.md in any component | `prowler-changelog` | +| Updating README.md provider statistics table | `prowler-readme-table` | +| Updating checks, services, compliance, or categories count in README.md | `prowler-readme-table` | | Updating existing Attack Paths queries | `prowler-attack-paths-query` | | Updating existing checks and metadata | `prowler-sdk-check` | | Using Zustand stores | `zustand-5` | | Working on MCP server tools | `prowler-mcp` | | Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` | +| Working on task | `tdd` | | Working with Prowler UI test helpers/pages | `prowler-test-ui` | | Working with Tailwind classes | `tailwind-4` | | Writing Playwright E2E tests | `playwright` | @@ -107,9 +117,12 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Writing Prowler SDK tests | `prowler-test-sdk` | | Writing Prowler UI E2E tests | `prowler-test-ui` | | Writing Python tests with pytest | `pytest` | +| Writing React component tests | `vitest` | | Writing React components | `react-19` | | Writing TypeScript types/interfaces | `typescript` | +| Writing Vitest tests | `vitest` | | Writing documentation | `prowler-docs` | +| Writing unit tests for UI | `vitest` | --- diff --git a/README.md b/README.md index d583a33a15..9e5be11c53 100644 --- a/README.md +++ b/README.md @@ -104,15 +104,15 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically | Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) | Support | Interface | |---|---|---|---|---|---|---| -| AWS | 585 | 84 | 40 | 17 | Official | UI, API, CLI | -| Azure | 169 | 22 | 17 | 13 | Official | UI, API, CLI | -| GCP | 100 | 17 | 14 | 7 | Official | UI, API, CLI | -| Kubernetes | 84 | 7 | 7 | 9 | Official | UI, API, CLI | -| GitHub | 20 | 2 | 1 | 2 | Official | UI, API, CLI | -| M365 | 72 | 7 | 4 | 4 | Official | UI, API, CLI | -| OCI | 52 | 14 | 1 | 12 | Official | UI, API, CLI | -| Alibaba Cloud | 64 | 9 | 2 | 9 | Official | UI, API, CLI | -| Cloudflare | 29 | 3 | 0 | 5 | Official | CLI | +| AWS | 572 | 83 | 41 | 17 | Official | UI, API, CLI | +| Azure | 165 | 20 | 18 | 13 | Official | UI, API, CLI | +| GCP | 100 | 13 | 15 | 11 | Official | UI, API, CLI | +| Kubernetes | 83 | 7 | 7 | 9 | Official | UI, API, CLI | +| GitHub | 21 | 2 | 1 | 2 | Official | UI, API, CLI | +| M365 | 75 | 7 | 4 | 4 | Official | UI, API, CLI | +| OCI | 51 | 13 | 3 | 12 | Official | UI, API, CLI | +| Alibaba Cloud | 61 | 9 | 3 | 9 | Official | UI, API, CLI | +| Cloudflare | 29 | 2 | 0 | 5 | Official | CLI, API | | IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | UI, API, CLI | | MongoDB Atlas | 10 | 3 | 0 | 3 | Official | UI, API, CLI | | LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | CLI | diff --git a/api/AGENTS.md b/api/AGENTS.md index e1e989d751..e1738b4a26 100644 --- a/api/AGENTS.md +++ b/api/AGENTS.md @@ -24,13 +24,18 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Creating ViewSets, serializers, or filters in api/ | `django-drf` | | Creating a git commit | `prowler-commit` | | Creating/modifying models, views, serializers | `prowler-api` | +| Fixing bug | `tdd` | | Implementing JSON:API endpoints | `django-drf` | +| Implementing feature | `tdd` | | Modifying API responses | `jsonapi` | +| Modifying component | `tdd` | +| Refactoring code | `tdd` | | Review changelog format and conventions | `prowler-changelog` | | Reviewing JSON:API compliance | `jsonapi` | | Testing RLS tenant isolation | `prowler-test-api` | | Update CHANGELOG.md in any component | `prowler-changelog` | | Updating existing Attack Paths queries | `prowler-attack-paths-query` | +| Working on task | `tdd` | | Writing Prowler API tests | `prowler-test-api` | | Writing Python tests with pytest | `pytest` | diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 4b404586a1..2197a9a40b 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -2,6 +2,54 @@ All notable changes to the **Prowler API** are documented in this file. +## [1.20.0] (Prowler UNRELEASED) + +### 🚀 Added + +- OpenStack provider support [(#10003)](https://github.com/prowler-cloud/prowler/pull/10003) +- PDF report for the CSA CCM compliance framework [(#10088)](https://github.com/prowler-cloud/prowler/pull/10088) + +### 🔄 Changed + +- Attack Paths: Queries definition now has short description and attribution [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983) +- Attack Paths: Internet node is created while scan [(#9992)](https://github.com/prowler-cloud/prowler/pull/9992) +- Attack Paths: Add full paths set from [pathfinding.cloud](https://pathfinding.cloud/) [(#10008)](https://github.com/prowler-cloud/prowler/pull/10008) +- Support CSA CCM 4.0 for the AWS provider [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018) +- Support CSA CCM 4.0 for the GCP provider [(#10042)](https://github.com/prowler-cloud/prowler/pull/10042) +- Support CSA CCM 4.0 for the Azure provider [(#10039)](https://github.com/prowler-cloud/prowler/pull/10039) +- Support CSA CCM 4.0 for the Oracle Cloud provider [(#10057)](https://github.com/prowler-cloud/prowler/pull/10057) +- Support CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061) +- Attack Paths: Mark attack Paths scan as failed when Celery task fails outside job error handling [(#10065)](https://github.com/prowler-cloud/prowler/pull/10065) +- Attack Paths: Remove legacy per-scan `graph_database` and `is_graph_database_deleted` fields from AttackPathsScan model [(#10077)](https://github.com/prowler-cloud/prowler/pull/10077) +- Attack Paths: Add `graph_data_ready` field to decouple query availability from scan state [(#10089)](https://github.com/prowler-cloud/prowler/pull/10089) +- AI agent guidelines with TDD and testing skills references [(#9925)](https://github.com/prowler-cloud/prowler/pull/9925) + +### 🐞 Fixed + +- Attack Paths: Orphaned temporary Neo4j databases are now cleaned up on scan failure and provider deletion [(#10101)](https://github.com/prowler-cloud/prowler/pull/10101) + +### 🔐 Security + +- Bump `Pillow` to 12.1.1 (CVE-2021-25289) [(#10027)](https://github.com/prowler-cloud/prowler/pull/10027) + +--- + +## [1.19.3] (Prowler UNRELEASED) + +### 🐞 Fixed + +- GCP provider UID validation regex to allow domain prefixes [(#10078)](https://github.com/prowler-cloud/prowler/pull/10078) + +--- + +## [1.19.2] (Prowler v5.18.2) + +### 🐞 Fixed + +- SAML role mapping now prevents removing the last MANAGE_ACCOUNT user [(#10007)](https://github.com/prowler-cloud/prowler/pull/10007) + +--- + ## [1.19.0] (Prowler v5.18.0) ### 🚀 Added diff --git a/api/poetry.lock b/api/poetry.lock index 8ab49cf6bf..203147d55f 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -2508,43 +2508,49 @@ dev = ["bandit", "coverage", "flake8", "pydocstyle", "pylint", "pytest", "pytest [[package]] name = "cryptography" -version = "44.0.1" +version = "44.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main", "dev"] files = [ - {file = "cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0"}, - {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf"}, - {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864"}, - {file = "cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a"}, - {file = "cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00"}, - {file = "cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41"}, - {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b"}, - {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7"}, - {file = "cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9"}, - {file = "cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f"}, - {file = "cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183"}, - {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12"}, - {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83"}, - {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420"}, - {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4"}, - {file = "cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7"}, - {file = "cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14"}, + {file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"}, + {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"}, + {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"}, + {file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"}, + {file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"}, + {file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"}, + {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"}, + {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"}, + {file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"}, + {file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"}, + {file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"}, ] [package.dependencies] @@ -2557,7 +2563,7 @@ nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==44.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -5830,46 +5836,6 @@ files = [ {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, ] -[[package]] -name = "netifaces" -version = "0.11.0" -description = "Portable network interface information." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "netifaces-0.11.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eb4813b77d5df99903af4757ce980a98c4d702bbcb81f32a0b305a1537bdf0b1"}, - {file = "netifaces-0.11.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5f9ca13babe4d845e400921973f6165a4c2f9f3379c7abfc7478160e25d196a4"}, - {file = "netifaces-0.11.0-cp27-cp27m-win32.whl", hash = "sha256:7dbb71ea26d304e78ccccf6faccef71bb27ea35e259fb883cfd7fd7b4f17ecb1"}, - {file = "netifaces-0.11.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0f6133ac02521270d9f7c490f0c8c60638ff4aec8338efeff10a1b51506abe85"}, - {file = "netifaces-0.11.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:08e3f102a59f9eaef70948340aeb6c89bd09734e0dca0f3b82720305729f63ea"}, - {file = "netifaces-0.11.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c03fb2d4ef4e393f2e6ffc6376410a22a3544f164b336b3a355226653e5efd89"}, - {file = "netifaces-0.11.0-cp34-cp34m-win32.whl", hash = "sha256:73ff21559675150d31deea8f1f8d7e9a9a7e4688732a94d71327082f517fc6b4"}, - {file = "netifaces-0.11.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:815eafdf8b8f2e61370afc6add6194bd5a7252ae44c667e96c4c1ecf418811e4"}, - {file = "netifaces-0.11.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:50721858c935a76b83dd0dd1ab472cad0a3ef540a1408057624604002fcfb45b"}, - {file = "netifaces-0.11.0-cp35-cp35m-win32.whl", hash = "sha256:c9a3a47cd3aaeb71e93e681d9816c56406ed755b9442e981b07e3618fb71d2ac"}, - {file = "netifaces-0.11.0-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:aab1dbfdc55086c789f0eb37affccf47b895b98d490738b81f3b2360100426be"}, - {file = "netifaces-0.11.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c37a1ca83825bc6f54dddf5277e9c65dec2f1b4d0ba44b8fd42bc30c91aa6ea1"}, - {file = "netifaces-0.11.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:28f4bf3a1361ab3ed93c5ef360c8b7d4a4ae060176a3529e72e5e4ffc4afd8b0"}, - {file = "netifaces-0.11.0-cp36-cp36m-win32.whl", hash = "sha256:2650beee182fed66617e18474b943e72e52f10a24dc8cac1db36c41ee9c041b7"}, - {file = "netifaces-0.11.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cb925e1ca024d6f9b4f9b01d83215fd00fe69d095d0255ff3f64bffda74025c8"}, - {file = "netifaces-0.11.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:84e4d2e6973eccc52778735befc01638498781ce0e39aa2044ccfd2385c03246"}, - {file = "netifaces-0.11.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18917fbbdcb2d4f897153c5ddbb56b31fa6dd7c3fa9608b7e3c3a663df8206b5"}, - {file = "netifaces-0.11.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:48324183af7f1bc44f5f197f3dad54a809ad1ef0c78baee2c88f16a5de02c4c9"}, - {file = "netifaces-0.11.0-cp37-cp37m-win32.whl", hash = "sha256:8f7da24eab0d4184715d96208b38d373fd15c37b0dafb74756c638bd619ba150"}, - {file = "netifaces-0.11.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2479bb4bb50968089a7c045f24d120f37026d7e802ec134c4490eae994c729b5"}, - {file = "netifaces-0.11.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3ecb3f37c31d5d51d2a4d935cfa81c9bc956687c6f5237021b36d6fdc2815b2c"}, - {file = "netifaces-0.11.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:96c0fe9696398253f93482c84814f0e7290eee0bfec11563bd07d80d701280c3"}, - {file = "netifaces-0.11.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c92ff9ac7c2282009fe0dcb67ee3cd17978cffbe0c8f4b471c00fe4325c9b4d4"}, - {file = "netifaces-0.11.0-cp38-cp38-win32.whl", hash = "sha256:d07b01c51b0b6ceb0f09fc48ec58debd99d2c8430b09e56651addeaf5de48048"}, - {file = "netifaces-0.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:469fc61034f3daf095e02f9f1bbac07927b826c76b745207287bc594884cfd05"}, - {file = "netifaces-0.11.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5be83986100ed1fdfa78f11ccff9e4757297735ac17391b95e17e74335c2047d"}, - {file = "netifaces-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54ff6624eb95b8a07e79aa8817288659af174e954cca24cdb0daeeddfc03c4ff"}, - {file = "netifaces-0.11.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:841aa21110a20dc1621e3dd9f922c64ca64dd1eb213c47267a2c324d823f6c8f"}, - {file = "netifaces-0.11.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76c7f351e0444721e85f975ae92718e21c1f361bda946d60a214061de1f00a1"}, - {file = "netifaces-0.11.0.tar.gz", hash = "sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32"}, -] - [[package]] name = "nltk" version = "3.9.2" @@ -6037,14 +6003,14 @@ voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] [[package]] name = "openstacksdk" -version = "4.0.1" +version = "4.2.0" description = "An SDK for building applications to work with OpenStack" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "openstacksdk-4.0.1-py3-none-any.whl", hash = "sha256:d63187a006fff7c1de1486c9e2e1073a787af402620c3c0ed0cf5291225998ac"}, - {file = "openstacksdk-4.0.1.tar.gz", hash = "sha256:19faa1d5e6a78a2c1dc06a171e65e776ba82e9df23e1d08586225dc5ade9fc63"}, + {file = "openstacksdk-4.2.0-py3-none-any.whl", hash = "sha256:238be0fa5d9899872b00787ab38e84f92fd6dc87525fde0965dadcdc12196dc6"}, + {file = "openstacksdk-4.2.0.tar.gz", hash = "sha256:5cb9450dcce8054a2caf89d8be9e55057ddfa219a954e781032241eb29280445"}, ] [package.dependencies] @@ -6055,10 +6021,10 @@ iso8601 = ">=0.1.11" jmespath = ">=0.9.0" jsonpatch = ">=1.16,<1.20 || >1.20" keystoneauth1 = ">=3.18.0" -netifaces = ">=0.10.4" os-service-types = ">=1.7.0" pbr = ">=2.0.0,<2.1.0 || >2.1.0" platformdirs = ">=3" +psutil = ">=3.2.2" PyYAML = ">=3.13" requestsexceptions = ">=1.2.0" @@ -6258,103 +6224,103 @@ urllib3 = "*" [[package]] name = "pillow" -version = "12.1.0" +version = "12.1.1" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd"}, - {file = "pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0"}, - {file = "pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8"}, - {file = "pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1"}, - {file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda"}, - {file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7"}, - {file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a"}, - {file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef"}, - {file = "pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09"}, - {file = "pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91"}, - {file = "pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea"}, - {file = "pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3"}, - {file = "pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0"}, - {file = "pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451"}, - {file = "pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e"}, - {file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84"}, - {file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0"}, - {file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b"}, - {file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18"}, - {file = "pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64"}, - {file = "pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75"}, - {file = "pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304"}, - {file = "pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b"}, - {file = "pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551"}, - {file = "pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208"}, - {file = "pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5"}, - {file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661"}, - {file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17"}, - {file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670"}, - {file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616"}, - {file = "pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7"}, - {file = "pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d"}, - {file = "pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c"}, - {file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1"}, - {file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179"}, - {file = "pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0"}, - {file = "pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587"}, - {file = "pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac"}, - {file = "pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b"}, - {file = "pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea"}, - {file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c"}, - {file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc"}, - {file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644"}, - {file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c"}, - {file = "pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171"}, - {file = "pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a"}, - {file = "pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45"}, - {file = "pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d"}, - {file = "pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0"}, - {file = "pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554"}, - {file = "pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e"}, - {file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82"}, - {file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4"}, - {file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0"}, - {file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b"}, - {file = "pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65"}, - {file = "pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0"}, - {file = "pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8"}, - {file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91"}, - {file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796"}, - {file = "pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd"}, - {file = "pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13"}, - {file = "pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e"}, - {file = "pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643"}, - {file = "pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5"}, - {file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de"}, - {file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9"}, - {file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a"}, - {file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a"}, - {file = "pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030"}, - {file = "pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94"}, - {file = "pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4"}, - {file = "pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2"}, - {file = "pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61"}, - {file = "pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51"}, - {file = "pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc"}, - {file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14"}, - {file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8"}, - {file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924"}, - {file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef"}, - {file = "pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988"}, - {file = "pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6"}, - {file = "pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19"}, - {file = "pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"}, + {file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"}, + {file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"}, + {file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"}, + {file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"}, + {file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"}, + {file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"}, + {file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"}, + {file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"}, + {file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"}, + {file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"}, + {file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"}, + {file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"}, + {file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"}, + {file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"}, + {file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"}, + {file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"}, + {file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"}, + {file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"}, + {file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"}, + {file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"}, + {file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"}, + {file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"}, ] [package.extras] @@ -6660,7 +6626,7 @@ files = [ [[package]] name = "prowler" -version = "5.18.0" +version = "5.19.0" description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks." optional = false python-versions = ">3.9.1,<3.13" @@ -6715,7 +6681,7 @@ boto3 = "1.40.61" botocore = "1.40.61" cloudflare = "4.3.1" colorama = "0.4.6" -cryptography = "44.0.1" +cryptography = "44.0.3" dash = "3.1.1" dash-bootstrap-components = "2.0.3" detect-secrets = "1.5.0" @@ -6730,10 +6696,10 @@ microsoft-kiota-abstractions = "1.9.2" msgraph-sdk = "1.23.0" numpy = "2.0.2" oci = "2.160.3" -openstacksdk = "4.0.1" +openstacksdk = "4.2.0" pandas = "2.2.3" py-iam-expand = "0.1.0" -py-ocsf-models = "0.5.0" +py-ocsf-models = "0.8.1" pydantic = ">=2.0,<3.0" pygithub = "2.5.0" python-dateutil = ">=2.9.0.post0,<3.0.0" @@ -6748,7 +6714,7 @@ tzlocal = "5.3.1" type = "git" url = "https://github.com/prowler-cloud/prowler.git" reference = "master" -resolved_reference = "b1f99716171856bf787a7695a588ffad6bf8d596" +resolved_reference = "ceb4691c3657e7db3d178896bfc241d14f194295" [[package]] name = "psutil" @@ -6896,20 +6862,20 @@ iamdata = ">=0.1.202504091" [[package]] name = "py-ocsf-models" -version = "0.5.0" +version = "0.8.1" description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/." optional = false -python-versions = "<3.14,>3.9.1" +python-versions = "<3.15,>3.9.1" groups = ["main"] files = [ - {file = "py_ocsf_models-0.5.0-py3-none-any.whl", hash = "sha256:7933253f56782c04c412d976796db429577810b951fe4195351794500b5962d8"}, - {file = "py_ocsf_models-0.5.0.tar.gz", hash = "sha256:bf05e955809d1ec3ab1007e4a4b2a8a0afa74b6e744ea8ffbf386e46b3af0a76"}, + {file = "py_ocsf_models-0.8.1-py3-none-any.whl", hash = "sha256:061eb446c4171534c09a8b37f5a9d2a2fe9f87c5db32edbd1182446bc5fd097e"}, + {file = "py_ocsf_models-0.8.1.tar.gz", hash = "sha256:c9045237857f951e073c9f9d1f57954c90d86875b469260725292d47f7a7d73c"}, ] [package.dependencies] -cryptography = "44.0.1" +cryptography = ">=44.0.3,<47" email-validator = "2.2.0" -pydantic = ">=2.9.2,<3.0.0" +pydantic = ">=2.12.0,<3.0.0" [[package]] name = "pyasn1" @@ -9400,4 +9366,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "bada7223d576ddd48ff74aa101d18e7465492cf014006e17354dbe2190a02b29" +content-hash = "c575bc849038db5b5d0882bec441529bf474a42b28c96718372ad4ceb388432c" diff --git a/api/pyproject.toml b/api/pyproject.toml index e0f577e076..5f5c5444d1 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "drf-spectacular-jsonapi==0.5.1", "gunicorn==23.0.0", "lxml==5.3.2", - "prowler @ git+https://github.com/prowler-cloud/prowler.git@master", + "prowler @ git+https://github.com/prowler-cloud/prowler.git@feat/PROWLER-940-stage-2-a-image-provider-api-v3", "psycopg2-binary==2.9.9", "pytest-celery[redis] (>=1.0.1,<2.0.0)", "sentry-sdk[django] (>=2.20.0,<3.0.0)", diff --git a/api/src/backend/api/attack_paths/queries/aws.py b/api/src/backend/api/attack_paths/queries/aws.py index 60208b7e51..39e5e5716f 100644 --- a/api/src/backend/api/attack_paths/queries/aws.py +++ b/api/src/backend/api/attack_paths/queries/aws.py @@ -1,17 +1,18 @@ from api.attack_paths.queries.types import ( + AttackPathsQueryAttribution, AttackPathsQueryDefinition, AttackPathsQueryParameterDefinition, ) from tasks.jobs.attack_paths.config import PROWLER_FINDING_LABEL -# Privilege Escalation Queries (based on pathfinding.cloud research) -# https://github.com/DataDog/pathfinding.cloud -# ------------------------------------------------------------------- +# Custom Attack Path Queries +# -------------------------- AWS_INTERNET_EXPOSED_EC2_SENSITIVE_S3_ACCESS = AttackPathsQueryDefinition( id="aws-internet-exposed-ec2-sensitive-s3-access", - name="Identify internet-exposed EC2 instances with sensitive S3 access", + name="Internet-Exposed EC2 with Sensitive S3 Access", + short_description="Find SSH-exposed EC2 instances that can assume roles to read tagged sensitive S3 buckets.", description="Detect EC2 instances with SSH exposed to the internet that can assume higher-privileged roles to read tagged sensitive S3 buckets despite bucket-level public access blocks.", provider="aws", cypher=f""" @@ -35,8 +36,7 @@ AWS_INTERNET_EXPOSED_EC2_SENSITIVE_S3_ACCESS = AttackPathsQueryDefinition( YIELD rel AS can_access UNWIND nodes(path_s3) + nodes(path_ec2) + nodes(path_role) + nodes(path_assume_role) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path_s3, path_ec2, path_role, path_assume_role, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, internet, can_access """, @@ -62,15 +62,15 @@ AWS_INTERNET_EXPOSED_EC2_SENSITIVE_S3_ACCESS = AttackPathsQueryDefinition( AWS_RDS_INSTANCES = AttackPathsQueryDefinition( id="aws-rds-instances", - name="Identify provisioned RDS instances", + name="RDS Instances Inventory", + short_description="List all provisioned RDS database instances in the account.", description="List the selected AWS account alongside the RDS instances it owns.", provider="aws", cypher=f""" MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(rds:RDSInstance) UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, @@ -79,7 +79,8 @@ AWS_RDS_INSTANCES = AttackPathsQueryDefinition( AWS_RDS_UNENCRYPTED_STORAGE = AttackPathsQueryDefinition( id="aws-rds-unencrypted-storage", - name="Identify RDS instances without storage encryption", + name="Unencrypted RDS Instances", + short_description="Find RDS instances with storage encryption disabled.", description="Find RDS instances with storage encryption disabled within the selected account.", provider="aws", cypher=f""" @@ -87,8 +88,7 @@ AWS_RDS_UNENCRYPTED_STORAGE = AttackPathsQueryDefinition( WHERE rds.storage_encrypted = false UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, @@ -97,7 +97,8 @@ AWS_RDS_UNENCRYPTED_STORAGE = AttackPathsQueryDefinition( AWS_S3_ANONYMOUS_ACCESS_BUCKETS = AttackPathsQueryDefinition( id="aws-s3-anonymous-access-buckets", - name="Identify S3 buckets with anonymous access", + name="S3 Buckets with Anonymous Access", + short_description="Find S3 buckets that allow anonymous access.", description="Find S3 buckets that allow anonymous access within the selected account.", provider="aws", cypher=f""" @@ -105,8 +106,7 @@ AWS_S3_ANONYMOUS_ACCESS_BUCKETS = AttackPathsQueryDefinition( WHERE s3.anonymous_access = true UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, @@ -115,7 +115,8 @@ AWS_S3_ANONYMOUS_ACCESS_BUCKETS = AttackPathsQueryDefinition( AWS_IAM_STATEMENTS_ALLOW_ALL_ACTIONS = AttackPathsQueryDefinition( id="aws-iam-statements-allow-all-actions", - name="Identify IAM statements that allow all actions", + name="IAM Statements Allowing All Actions", + short_description="Find IAM policy statements that allow all actions via wildcard (*).", description="Find IAM policy statements that allow all actions via '*' within the selected account.", provider="aws", cypher=f""" @@ -124,8 +125,7 @@ AWS_IAM_STATEMENTS_ALLOW_ALL_ACTIONS = AttackPathsQueryDefinition( AND any(x IN stmt.action WHERE x = '*') UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, @@ -134,7 +134,8 @@ AWS_IAM_STATEMENTS_ALLOW_ALL_ACTIONS = AttackPathsQueryDefinition( AWS_IAM_STATEMENTS_ALLOW_DELETE_POLICY = AttackPathsQueryDefinition( id="aws-iam-statements-allow-delete-policy", - name="Identify IAM statements that allow iam:DeletePolicy", + name="IAM Statements Allowing Policy Deletion", + short_description="Find IAM policy statements that allow iam:DeletePolicy.", description="Find IAM policy statements that allow the iam:DeletePolicy action within the selected account.", provider="aws", cypher=f""" @@ -143,8 +144,7 @@ AWS_IAM_STATEMENTS_ALLOW_DELETE_POLICY = AttackPathsQueryDefinition( AND any(x IN stmt.action WHERE x = "iam:DeletePolicy") UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, @@ -153,7 +153,8 @@ AWS_IAM_STATEMENTS_ALLOW_DELETE_POLICY = AttackPathsQueryDefinition( AWS_IAM_STATEMENTS_ALLOW_CREATE_ACTIONS = AttackPathsQueryDefinition( id="aws-iam-statements-allow-create-actions", - name="Identify IAM statements that allow create actions", + name="IAM Statements Allowing Create Actions", + short_description="Find IAM policy statements that allow any create action.", description="Find IAM policy statements that allow actions containing 'create' within the selected account.", provider="aws", cypher=f""" @@ -162,8 +163,7 @@ AWS_IAM_STATEMENTS_ALLOW_CREATE_ACTIONS = AttackPathsQueryDefinition( AND any(x IN stmt.action WHERE toLower(x) CONTAINS "create") UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, @@ -176,7 +176,8 @@ AWS_IAM_STATEMENTS_ALLOW_CREATE_ACTIONS = AttackPathsQueryDefinition( AWS_EC2_INSTANCES_INTERNET_EXPOSED = AttackPathsQueryDefinition( id="aws-ec2-instances-internet-exposed", - name="Identify internet-exposed EC2 instances", + name="Internet-Exposed EC2 Instances", + short_description="Find EC2 instances flagged as exposed to the internet.", description="Find EC2 instances flagged as exposed to the internet within the selected account.", provider="aws", cypher=f""" @@ -190,8 +191,7 @@ AWS_EC2_INSTANCES_INTERNET_EXPOSED = AttackPathsQueryDefinition( YIELD rel AS can_access UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, internet, can_access """, @@ -200,7 +200,8 @@ AWS_EC2_INSTANCES_INTERNET_EXPOSED = AttackPathsQueryDefinition( AWS_SECURITY_GROUPS_OPEN_INTERNET_FACING = AttackPathsQueryDefinition( id="aws-security-groups-open-internet-facing", - name="Identify internet-facing resources with open security groups", + name="Open Security Groups on Internet-Facing Resources", + short_description="Find internet-facing resources with security groups allowing inbound from 0.0.0.0/0.", description="Find internet-facing resources associated with security groups that allow inbound access from '0.0.0.0/0'.", provider="aws", cypher=f""" @@ -216,8 +217,7 @@ AWS_SECURITY_GROUPS_OPEN_INTERNET_FACING = AttackPathsQueryDefinition( YIELD rel AS can_access UNWIND nodes(path_ec2) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path_ec2, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, internet, can_access """, @@ -226,7 +226,8 @@ AWS_SECURITY_GROUPS_OPEN_INTERNET_FACING = AttackPathsQueryDefinition( AWS_CLASSIC_ELB_INTERNET_EXPOSED = AttackPathsQueryDefinition( id="aws-classic-elb-internet-exposed", - name="Identify internet-exposed Classic Load Balancers", + name="Internet-Exposed Classic Load Balancers", + short_description="Find Classic Load Balancers exposed to the internet with their listeners.", description="Find Classic Load Balancers exposed to the internet along with their listeners.", provider="aws", cypher=f""" @@ -240,8 +241,7 @@ AWS_CLASSIC_ELB_INTERNET_EXPOSED = AttackPathsQueryDefinition( YIELD rel AS can_access UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, internet, can_access """, @@ -250,7 +250,8 @@ AWS_CLASSIC_ELB_INTERNET_EXPOSED = AttackPathsQueryDefinition( AWS_ELBV2_INTERNET_EXPOSED = AttackPathsQueryDefinition( id="aws-elbv2-internet-exposed", - name="Identify internet-exposed ELBv2 load balancers", + name="Internet-Exposed ALB/NLB Load Balancers", + short_description="Find ELBv2 (ALB/NLB) load balancers exposed to the internet with their listeners.", description="Find ELBv2 load balancers exposed to the internet along with their listeners.", provider="aws", cypher=f""" @@ -264,8 +265,7 @@ AWS_ELBV2_INTERNET_EXPOSED = AttackPathsQueryDefinition( YIELD rel AS can_access UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, internet, can_access """, @@ -274,7 +274,8 @@ AWS_ELBV2_INTERNET_EXPOSED = AttackPathsQueryDefinition( AWS_PUBLIC_IP_RESOURCE_LOOKUP = AttackPathsQueryDefinition( id="aws-public-ip-resource-lookup", - name="Identify resources by public IP address", + name="Resource Lookup by Public IP", + short_description="Find the AWS resource associated with a given public IP address.", description="Given a public IP address, find the related AWS resource and its adjacent node within the selected account.", provider="aws", cypher=f""" @@ -305,8 +306,7 @@ AWS_PUBLIC_IP_RESOURCE_LOOKUP = AttackPathsQueryDefinition( YIELD rel AS can_access UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, internet, can_access """, @@ -320,38 +320,24 @@ AWS_PUBLIC_IP_RESOURCE_LOOKUP = AttackPathsQueryDefinition( ], ) +# Privilege Escalation Queries (based on pathfinding.cloud research) +# https://github.com/DataDog/pathfinding.cloud +# ------------------------------------------------------------------- -AWS_IAM_PRIVESC_PASSROLE_EC2 = AttackPathsQueryDefinition( - id="aws-iam-privesc-passrole-ec2", - name="Privilege Escalation: iam:PassRole + ec2:RunInstances", - description="Detect principals who can launch EC2 instances with privileged IAM roles attached. This allows gaining the permissions of the passed role by accessing the EC2 instance metadata service. This is a new-passrole escalation path (pathfinding.cloud: ec2-001).", +# APPRUNNER-001 +AWS_APPRUNNER_PRIVESC_PASSROLE_CREATE_SERVICE = AttackPathsQueryDefinition( + id="aws-apprunner-privesc-passrole-create-service", + name="App Runner Service Creation with Privileged Role (APPRUNNER-001)", + short_description="Create an App Runner service with a privileged IAM role to gain its permissions.", + description="Detect principals who can pass IAM roles and create App Runner services. This allows creating a service with a privileged role attached, gaining that role's permissions via StartCommand execution, a container web shell, or a malicious apprunner.yaml configuration.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - APPRUNNER-001 - iam:PassRole + apprunner:CreateService", + link="https://pathfinding.cloud/paths/apprunner-001", + ), provider="aws", cypher=f""" - // Create a single shared virtual EC2 instance node - CALL apoc.create.vNode(['EC2Instance'], {{ - id: 'potential-ec2-passrole', - name: 'New EC2 Instance', - description: 'Attacker-controlled EC2 with privileged role' - }}) - YIELD node AS ec2_node - - // Create a single shared virtual escalation outcome node (styled like a finding) - CALL apoc.create.vNode(['PrivilegeEscalation'], {{ - id: 'effective-administrator-passrole-ec2', - check_title: 'Privilege Escalation', - name: 'Effective Administrator', - status: 'FAIL', - severity: 'critical' - }}) - YIELD node AS escalation_outcome - - WITH ec2_node, escalation_outcome - - // Find principals in the account - MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal) - - // Find statements granting iam:PassRole - MATCH path_passrole = (principal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) WHERE stmt_passrole.effect = 'Allow' AND any(action IN stmt_passrole.action WHERE toLower(action) = 'iam:passrole' @@ -359,8 +345,664 @@ AWS_IAM_PRIVESC_PASSROLE_EC2 = AttackPathsQueryDefinition( OR action = '*' ) - // Find statements granting ec2:RunInstances - MATCH path_ec2 = (principal)--(ec2_policy:AWSPolicy)--(stmt_ec2:AWSPolicyStatement) + // Find apprunner:CreateService permission + MATCH (principal)--(apprunner_policy:AWSPolicy)--(stmt_apprunner:AWSPolicyStatement) + WHERE stmt_apprunner.effect = 'Allow' + AND any(action IN stmt_apprunner.action WHERE + toLower(action) = 'apprunner:createservice' + OR toLower(action) = 'apprunner:*' + OR action = '*' + ) + + // Find roles that trust App Runner tasks service (can be passed to App Runner) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'tasks.apprunner.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# APPRUNNER-002 +AWS_APPRUNNER_PRIVESC_UPDATE_SERVICE = AttackPathsQueryDefinition( + id="aws-apprunner-privesc-update-service", + name="App Runner Service Update for Role Access (APPRUNNER-002)", + short_description="Update an existing App Runner service to leverage its already-attached privileged role.", + description="Detect principals who can update existing App Runner services. This allows modifying a service's configuration to execute arbitrary code with the service's already-attached IAM role, without requiring iam:PassRole. Exploitation methods include injecting a malicious StartCommand, updating to a container image with a web shell, or pointing to a repository with a malicious apprunner.yaml file.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - APPRUNNER-002 - apprunner:UpdateService", + link="https://pathfinding.cloud/paths/apprunner-002", + ), + provider="aws", + cypher=f""" + // Find principals with apprunner:UpdateService permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(update_policy:AWSPolicy)--(stmt_update:AWSPolicyStatement) + WHERE stmt_update.effect = 'Allow' + AND any(action IN stmt_update.action WHERE + toLower(action) = 'apprunner:updateservice' + OR toLower(action) = 'apprunner:*' + OR action = '*' + ) + + // Find existing App Runner services with roles attached (potential targets) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'tasks.apprunner.amazonaws.com'}}) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# BEDROCK-001 +AWS_BEDROCK_PRIVESC_PASSROLE_CODE_INTERPRETER = AttackPathsQueryDefinition( + id="aws-bedrock-privesc-passrole-code-interpreter", + name="Bedrock Code Interpreter with Privileged Role (BEDROCK-001)", + short_description="Create a Bedrock AgentCore Code Interpreter with a privileged role attached.", + description="Detect principals who can pass IAM roles and create Bedrock AgentCore Code Interpreters. This allows creating a code interpreter with a privileged role attached, gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - BEDROCK-001 - iam:PassRole + bedrock-agentcore:CreateCodeInterpreter + bedrock-agentcore:StartCodeInterpreterSession + bedrock-agentcore:InvokeCodeInterpreter", + link="https://pathfinding.cloud/paths/bedrock-001", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find bedrock-agentcore:CreateCodeInterpreter permission + MATCH (principal)--(bedrock_policy:AWSPolicy)--(stmt_bedrock:AWSPolicyStatement) + WHERE stmt_bedrock.effect = 'Allow' + AND any(action IN stmt_bedrock.action WHERE + toLower(action) = 'bedrock-agentcore:createcodeinterpreter' + OR toLower(action) = 'bedrock-agentcore:*' + OR action = '*' + ) + + // Find bedrock-agentcore:StartCodeInterpreterSession permission + MATCH (principal)--(session_policy:AWSPolicy)--(stmt_session:AWSPolicyStatement) + WHERE stmt_session.effect = 'Allow' + AND any(action IN stmt_session.action WHERE + toLower(action) = 'bedrock-agentcore:startcodeinterpretersession' + OR toLower(action) = 'bedrock-agentcore:*' + OR action = '*' + ) + + // Find bedrock-agentcore:InvokeCodeInterpreter permission + MATCH (principal)--(invoke_policy:AWSPolicy)--(stmt_invoke:AWSPolicyStatement) + WHERE stmt_invoke.effect = 'Allow' + AND any(action IN stmt_invoke.action WHERE + toLower(action) = 'bedrock-agentcore:invokecodeinterpreter' + OR toLower(action) = 'bedrock-agentcore:*' + OR action = '*' + ) + + // Find roles that trust Bedrock service (can be passed to Bedrock) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'bedrock.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# BEDROCK-002 +AWS_BEDROCK_PRIVESC_INVOKE_CODE_INTERPRETER = AttackPathsQueryDefinition( + id="aws-bedrock-privesc-invoke-code-interpreter", + name="Bedrock Code Interpreter Session Hijacking (BEDROCK-002)", + short_description="Start a session on an existing Bedrock code interpreter to exfiltrate its privileged role credentials.", + description="Detect principals who can start sessions and invoke code on existing Bedrock AgentCore code interpreters. This allows executing arbitrary Python code within an interpreter that has a privileged role attached, gaining that role's credentials via the MicroVM Metadata Service without requiring iam:PassRole.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - BEDROCK-002 - bedrock-agentcore:StartCodeInterpreterSession + bedrock-agentcore:InvokeCodeInterpreter", + link="https://pathfinding.cloud/paths/bedrock-002", + ), + provider="aws", + cypher=f""" + // Find principals with bedrock-agentcore:StartCodeInterpreterSession permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(session_policy:AWSPolicy)--(stmt_session:AWSPolicyStatement) + WHERE stmt_session.effect = 'Allow' + AND any(action IN stmt_session.action WHERE + toLower(action) = 'bedrock-agentcore:startcodeinterpretersession' + OR toLower(action) = 'bedrock-agentcore:*' + OR action = '*' + ) + + // Find bedrock-agentcore:InvokeCodeInterpreter permission + MATCH (principal)--(invoke_policy:AWSPolicy)--(stmt_invoke:AWSPolicyStatement) + WHERE stmt_invoke.effect = 'Allow' + AND any(action IN stmt_invoke.action WHERE + toLower(action) = 'bedrock-agentcore:invokecodeinterpreter' + OR toLower(action) = 'bedrock-agentcore:*' + OR action = '*' + ) + + // Find roles that trust Bedrock service (already attached to existing code interpreters) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'bedrock.amazonaws.com'}}) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# CLOUDFORMATION-001 +AWS_CLOUDFORMATION_PRIVESC_PASSROLE_CREATE_STACK = AttackPathsQueryDefinition( + id="aws-cloudformation-privesc-passrole-create-stack", + name="CloudFormation Stack Creation with Privileged Role (CLOUDFORMATION-001)", + short_description="Create a CloudFormation stack with a privileged role to provision arbitrary AWS resources.", + description="Detect principals who can pass IAM roles and create CloudFormation stacks. This allows launching a stack with a malicious template that executes with the passed role's permissions, enabling creation of resources like IAM users, Lambda functions, or EC2 instances controlled by the attacker.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - CLOUDFORMATION-001 - iam:PassRole + cloudformation:CreateStack", + link="https://pathfinding.cloud/paths/cloudformation-001", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find cloudformation:CreateStack permission + MATCH (principal)--(cfn_policy:AWSPolicy)--(stmt_cfn:AWSPolicyStatement) + WHERE stmt_cfn.effect = 'Allow' + AND any(action IN stmt_cfn.action WHERE + toLower(action) = 'cloudformation:createstack' + OR toLower(action) = 'cloudformation:*' + OR action = '*' + ) + + // Find roles that trust CloudFormation service (can be passed to CloudFormation) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'cloudformation.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# CLOUDFORMATION-002 +AWS_CLOUDFORMATION_PRIVESC_UPDATE_STACK = AttackPathsQueryDefinition( + id="aws-cloudformation-privesc-update-stack", + name="CloudFormation Stack Update for Role Access (CLOUDFORMATION-002)", + short_description="Update an existing CloudFormation stack to leverage its already-attached privileged service role.", + description="Detect principals who can update existing CloudFormation stacks. This allows modifying a stack's template to add new resources (such as IAM roles with admin access) that are created with the stack's already-attached service role permissions, without requiring iam:PassRole.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - CLOUDFORMATION-002 - cloudformation:UpdateStack", + link="https://pathfinding.cloud/paths/cloudformation-002", + ), + provider="aws", + cypher=f""" + // Find principals with cloudformation:UpdateStack permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(update_policy:AWSPolicy)--(stmt_update:AWSPolicyStatement) + WHERE stmt_update.effect = 'Allow' + AND any(action IN stmt_update.action WHERE + toLower(action) = 'cloudformation:updatestack' + OR toLower(action) = 'cloudformation:*' + OR action = '*' + ) + + // Find roles that trust CloudFormation service (already attached to existing stacks) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'cloudformation.amazonaws.com'}}) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# CLOUDFORMATION-003 +AWS_CLOUDFORMATION_PRIVESC_PASSROLE_CREATE_STACKSET = AttackPathsQueryDefinition( + id="aws-cloudformation-privesc-passrole-create-stackset", + name="CloudFormation StackSet Creation with Privileged Role (CLOUDFORMATION-003)", + short_description="Create a CloudFormation StackSet with a privileged execution role to provision arbitrary resources across accounts.", + description="Detect principals who can pass IAM roles, create CloudFormation StackSets, and deploy stack instances. This allows creating a StackSet with a malicious template and a privileged execution role, then deploying instances that create resources (such as IAM roles with admin access) using that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - CLOUDFORMATION-003 - iam:PassRole + cloudformation:CreateStackSet + cloudformation:CreateStackInstances", + link="https://pathfinding.cloud/paths/cloudformation-003", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find cloudformation:CreateStackSet permission + MATCH (principal)--(cfn_policy:AWSPolicy)--(stmt_cfn:AWSPolicyStatement) + WHERE stmt_cfn.effect = 'Allow' + AND any(action IN stmt_cfn.action WHERE + toLower(action) = 'cloudformation:createstackset' + OR toLower(action) = 'cloudformation:*' + OR action = '*' + ) + + // Find cloudformation:CreateStackInstances permission + MATCH (principal)--(cfn_instances_policy:AWSPolicy)--(stmt_cfn_instances:AWSPolicyStatement) + WHERE stmt_cfn_instances.effect = 'Allow' + AND any(action IN stmt_cfn_instances.action WHERE + toLower(action) = 'cloudformation:createstackinstances' + OR toLower(action) = 'cloudformation:*' + OR action = '*' + ) + + // Find roles that trust CloudFormation service (can be passed as execution role) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'cloudformation.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# CLOUDFORMATION-004 +AWS_CLOUDFORMATION_PRIVESC_PASSROLE_UPDATE_STACKSET = AttackPathsQueryDefinition( + id="aws-cloudformation-privesc-passrole-update-stackset", + name="CloudFormation StackSet Update with Privileged Role (CLOUDFORMATION-004)", + short_description="Update an existing CloudFormation StackSet to inject malicious resources using a privileged execution role.", + description="Detect principals who can pass IAM roles and update CloudFormation StackSets. This allows modifying an existing StackSet's template to add resources (such as IAM roles with admin access) that are provisioned by the StackSet's privileged execution role across target accounts.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - CLOUDFORMATION-004 - iam:PassRole + cloudformation:UpdateStackSet", + link="https://pathfinding.cloud/paths/cloudformation-004", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find cloudformation:UpdateStackSet permission + MATCH (principal)--(cfn_policy:AWSPolicy)--(stmt_cfn:AWSPolicyStatement) + WHERE stmt_cfn.effect = 'Allow' + AND any(action IN stmt_cfn.action WHERE + toLower(action) = 'cloudformation:updatestackset' + OR toLower(action) = 'cloudformation:*' + OR action = '*' + ) + + // Find roles that trust CloudFormation service (can be passed as execution role) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'cloudformation.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# CLOUDFORMATION-005 +AWS_CLOUDFORMATION_PRIVESC_CHANGESET = AttackPathsQueryDefinition( + id="aws-cloudformation-privesc-changeset", + name="CloudFormation Change Set Privilege Escalation (CLOUDFORMATION-005)", + short_description="Create and execute a change set on an existing stack to leverage its privileged service role.", + description="Detect principals who can create and execute CloudFormation change sets. This allows modifying an existing stack's template through a staged change set, inheriting the stack's already-attached service role permissions to provision arbitrary resources without requiring iam:PassRole.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - CLOUDFORMATION-005 - cloudformation:CreateChangeSet + cloudformation:ExecuteChangeSet", + link="https://pathfinding.cloud/paths/cloudformation-005", + ), + provider="aws", + cypher=f""" + // Find principals with cloudformation:CreateChangeSet permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(create_policy:AWSPolicy)--(stmt_create:AWSPolicyStatement) + WHERE stmt_create.effect = 'Allow' + AND any(action IN stmt_create.action WHERE + toLower(action) = 'cloudformation:createchangeset' + OR toLower(action) = 'cloudformation:*' + OR action = '*' + ) + + // Find cloudformation:ExecuteChangeSet permission + MATCH (principal)--(exec_policy:AWSPolicy)--(stmt_exec:AWSPolicyStatement) + WHERE stmt_exec.effect = 'Allow' + AND any(action IN stmt_exec.action WHERE + toLower(action) = 'cloudformation:executechangeset' + OR toLower(action) = 'cloudformation:*' + OR action = '*' + ) + + // Find roles that trust CloudFormation service (already attached to existing stacks) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'cloudformation.amazonaws.com'}}) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# CODEBUILD-001 +AWS_CODEBUILD_PRIVESC_PASSROLE_CREATE_PROJECT = AttackPathsQueryDefinition( + id="aws-codebuild-privesc-passrole-create-project", + name="CodeBuild Project Creation with Privileged Role (CODEBUILD-001)", + short_description="Create a CodeBuild project with a privileged role to execute arbitrary code via a malicious buildspec.", + description="Detect principals who can pass IAM roles, create CodeBuild projects, and start builds. This allows creating a project with a privileged role attached and executing arbitrary code through a malicious buildspec, gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - CODEBUILD-001 - iam:PassRole + codebuild:CreateProject + codebuild:StartBuild", + link="https://pathfinding.cloud/paths/codebuild-001", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find codebuild:CreateProject permission + MATCH (principal)--(create_policy:AWSPolicy)--(stmt_create:AWSPolicyStatement) + WHERE stmt_create.effect = 'Allow' + AND any(action IN stmt_create.action WHERE + toLower(action) = 'codebuild:createproject' + OR toLower(action) = 'codebuild:*' + OR action = '*' + ) + + // Find codebuild:StartBuild permission + MATCH (principal)--(build_policy:AWSPolicy)--(stmt_build:AWSPolicyStatement) + WHERE stmt_build.effect = 'Allow' + AND any(action IN stmt_build.action WHERE + toLower(action) = 'codebuild:startbuild' + OR toLower(action) = 'codebuild:*' + OR action = '*' + ) + + // Find roles that trust CodeBuild service (can be passed to CodeBuild) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'codebuild.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# CODEBUILD-002 +AWS_CODEBUILD_PRIVESC_START_BUILD = AttackPathsQueryDefinition( + id="aws-codebuild-privesc-start-build", + name="CodeBuild Buildspec Override for Role Access (CODEBUILD-002)", + short_description="Start a build on an existing CodeBuild project with a buildspec override to execute code with its privileged role.", + description="Detect principals who can start builds on existing CodeBuild projects. This allows overriding the buildspec with malicious commands that execute with the project's already-attached service role permissions, without requiring iam:PassRole or codebuild:CreateProject.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - CODEBUILD-002 - codebuild:StartBuild", + link="https://pathfinding.cloud/paths/codebuild-002", + ), + provider="aws", + cypher=f""" + // Find principals with codebuild:StartBuild permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(build_policy:AWSPolicy)--(stmt_build:AWSPolicyStatement) + WHERE stmt_build.effect = 'Allow' + AND any(action IN stmt_build.action WHERE + toLower(action) = 'codebuild:startbuild' + OR toLower(action) = 'codebuild:*' + OR action = '*' + ) + + // Find roles that trust CodeBuild service (already attached to existing projects) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'codebuild.amazonaws.com'}}) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# CODEBUILD-003 +AWS_CODEBUILD_PRIVESC_START_BUILD_BATCH = AttackPathsQueryDefinition( + id="aws-codebuild-privesc-start-build-batch", + name="CodeBuild Batch Buildspec Override for Role Access (CODEBUILD-003)", + short_description="Start a batch build on an existing CodeBuild project with a buildspec override to execute code with its privileged role.", + description="Detect principals who can start batch builds on existing CodeBuild projects. This allows overriding the buildspec with malicious commands that execute with the project's already-attached service role permissions, without requiring iam:PassRole or codebuild:CreateProject.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - CODEBUILD-003 - codebuild:StartBuildBatch", + link="https://pathfinding.cloud/paths/codebuild-003", + ), + provider="aws", + cypher=f""" + // Find principals with codebuild:StartBuildBatch permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(build_policy:AWSPolicy)--(stmt_build:AWSPolicyStatement) + WHERE stmt_build.effect = 'Allow' + AND any(action IN stmt_build.action WHERE + toLower(action) = 'codebuild:startbuildbatch' + OR toLower(action) = 'codebuild:*' + OR action = '*' + ) + + // Find roles that trust CodeBuild service (already attached to existing projects) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'codebuild.amazonaws.com'}}) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# CODEBUILD-004 +AWS_CODEBUILD_PRIVESC_PASSROLE_CREATE_PROJECT_BATCH = AttackPathsQueryDefinition( + id="aws-codebuild-privesc-passrole-create-project-batch", + name="CodeBuild Batch Project Creation with Privileged Role (CODEBUILD-004)", + short_description="Create a CodeBuild project configured for batch builds with a privileged role to execute arbitrary code via a malicious buildspec.", + description="Detect principals who can pass IAM roles, create CodeBuild projects, and start batch builds. This allows creating a project with a privileged role attached and executing arbitrary code through a malicious batch buildspec, gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - CODEBUILD-004 - iam:PassRole + codebuild:CreateProject + codebuild:StartBuildBatch", + link="https://pathfinding.cloud/paths/codebuild-004", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find codebuild:CreateProject permission + MATCH (principal)--(create_policy:AWSPolicy)--(stmt_create:AWSPolicyStatement) + WHERE stmt_create.effect = 'Allow' + AND any(action IN stmt_create.action WHERE + toLower(action) = 'codebuild:createproject' + OR toLower(action) = 'codebuild:*' + OR action = '*' + ) + + // Find codebuild:StartBuildBatch permission + MATCH (principal)--(batch_policy:AWSPolicy)--(stmt_batch:AWSPolicyStatement) + WHERE stmt_batch.effect = 'Allow' + AND any(action IN stmt_batch.action WHERE + toLower(action) = 'codebuild:startbuildbatch' + OR toLower(action) = 'codebuild:*' + OR action = '*' + ) + + // Find roles that trust CodeBuild service (can be passed to CodeBuild) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'codebuild.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# DATAPIPELINE-001 +AWS_DATAPIPELINE_PRIVESC_PASSROLE_CREATE_PIPELINE = AttackPathsQueryDefinition( + id="aws-datapipeline-privesc-passrole-create-pipeline", + name="Data Pipeline Creation with Privileged Role (DATAPIPELINE-001)", + short_description="Create a Data Pipeline with a privileged role to execute arbitrary commands on provisioned infrastructure.", + description="Detect principals who can pass IAM roles, create Data Pipelines, define pipeline objects, and activate them. This allows creating a pipeline with a privileged role attached and executing arbitrary commands on the provisioned EC2 instances or EMR clusters, gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - DATAPIPELINE-001 - iam:PassRole + datapipeline:CreatePipeline + datapipeline:PutPipelineDefinition + datapipeline:ActivatePipeline", + link="https://pathfinding.cloud/paths/datapipeline-001", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find datapipeline:CreatePipeline permission + MATCH (principal)--(create_policy:AWSPolicy)--(stmt_create:AWSPolicyStatement) + WHERE stmt_create.effect = 'Allow' + AND any(action IN stmt_create.action WHERE + toLower(action) = 'datapipeline:createpipeline' + OR toLower(action) = 'datapipeline:*' + OR action = '*' + ) + + // Find datapipeline:PutPipelineDefinition permission + MATCH (principal)--(put_policy:AWSPolicy)--(stmt_put:AWSPolicyStatement) + WHERE stmt_put.effect = 'Allow' + AND any(action IN stmt_put.action WHERE + toLower(action) = 'datapipeline:putpipelinedefinition' + OR toLower(action) = 'datapipeline:*' + OR action = '*' + ) + + // Find datapipeline:ActivatePipeline permission + MATCH (principal)--(activate_policy:AWSPolicy)--(stmt_activate:AWSPolicyStatement) + WHERE stmt_activate.effect = 'Allow' + AND any(action IN stmt_activate.action WHERE + toLower(action) = 'datapipeline:activatepipeline' + OR toLower(action) = 'datapipeline:*' + OR action = '*' + ) + + // Find roles that trust Data Pipeline or EMR service (can be passed to DataPipeline) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(trusted_principal:AWSPrincipal) + WHERE trusted_principal.arn IN ['datapipeline.amazonaws.com', 'elasticmapreduce.amazonaws.com'] + AND any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# EC2-001 +AWS_EC2_PRIVESC_PASSROLE_IAM = AttackPathsQueryDefinition( + id="aws-ec2-privesc-passrole-iam", + name="EC2 Instance Launch with Privileged Role (EC2-001)", + short_description="Launch EC2 instances with privileged IAM roles to gain their permissions via IMDS.", + description="Detect principals who can launch EC2 instances with privileged IAM roles attached. This allows gaining the permissions of the passed role by accessing the EC2 instance metadata service.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - EC2-001 - iam:PassRole + ec2:RunInstances", + link="https://pathfinding.cloud/paths/ec2-001", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find ec2:RunInstances permission + MATCH (principal)--(ec2_policy:AWSPolicy)--(stmt_ec2:AWSPolicyStatement) WHERE stmt_ec2.effect = 'Allow' AND any(action IN stmt_ec2.action WHERE toLower(action) = 'ec2:runinstances' @@ -369,309 +1011,2369 @@ AWS_IAM_PRIVESC_PASSROLE_EC2 = AttackPathsQueryDefinition( ) // Find roles that trust EC2 service (can be passed to EC2) - MATCH path_target = (aws)--(target_role:AWSRole) - WHERE target_role.arn CONTAINS $provider_uid - // Check if principal can pass this role - AND any(resource IN stmt_passrole.resource WHERE - resource = '*' - OR target_role.arn CONTAINS resource - OR resource CONTAINS target_role.name - ) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ec2.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) - // Check if target role has elevated permissions (optional, for severity assessment) - OPTIONAL MATCH (target_role)--(role_policy:AWSPolicy)--(role_stmt:AWSPolicyStatement) - WHERE role_stmt.effect = 'Allow' - AND ( - any(action IN role_stmt.action WHERE action = '*') - OR any(action IN role_stmt.action WHERE toLower(action) = 'iam:*') - ) - - CALL apoc.create.vRelationship(principal, 'CAN_LAUNCH', {{ - via: 'ec2:RunInstances + iam:PassRole' - }}, ec2_node) - YIELD rel AS launch_rel - - CALL apoc.create.vRelationship(ec2_node, 'ASSUMES_ROLE', {{}}, target_role) - YIELD rel AS assumes_rel - - CALL apoc.create.vRelationship(target_role, 'GRANTS_ACCESS', {{ - reference: 'https://pathfinding.cloud/paths/ec2-001' - }}, escalation_outcome) - YIELD rel AS grants_rel - - UNWIND nodes(path_principal) + nodes(path_passrole) + nodes(path_ec2) + nodes(path_target) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' - - RETURN path_principal, path_passrole, path_ec2, path_target, - ec2_node, escalation_outcome, launch_rel, assumes_rel, grants_rel, - collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr - """, - parameters=[], -) - -# TODO: Add ProwlerFinding nodes -AWS_GLUE_PRIVESC_PASSROLE_DEV_ENDPOINT = AttackPathsQueryDefinition( - id="aws-glue-privesc-passrole-dev-endpoint", - name="Privilege Escalation: Glue Dev Endpoint with PassRole", - description="Detect principals that can escalate privileges by passing a role to a Glue development endpoint. The attacker creates a dev endpoint with an arbitrary role attached, then accesses those credentials through the endpoint.", - provider="aws", - cypher=""" - CALL apoc.create.vNode(['PrivilegeEscalation'], { - id: 'effective-administrator-glue', - check_title: 'Privilege Escalation', - name: 'Effective Administrator (Glue)', - status: 'FAIL', - severity: 'critical' - }) - YIELD node AS escalation_outcome - - WITH escalation_outcome - - // Find principals in the account - MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal) - - // Principal can assume roles (up to 2 hops) - OPTIONAL MATCH path_assume = (principal)-[:STS_ASSUMEROLE_ALLOW*0..2]->(acting_as:AWSRole) - WITH escalation_outcome, principal, path_principal, path_assume, - CASE WHEN path_assume IS NULL THEN principal ELSE acting_as END AS effective_principal - - // Find iam:PassRole permission - MATCH path_passrole = (effective_principal)--(passrole_policy:AWSPolicy)--(passrole_stmt:AWSPolicyStatement) - WHERE passrole_stmt.effect = 'Allow' - AND any(action IN passrole_stmt.action WHERE toLower(action) = 'iam:passrole' OR action = '*') - - // Find Glue CreateDevEndpoint permission - MATCH (effective_principal)--(glue_policy:AWSPolicy)--(glue_stmt:AWSPolicyStatement) - WHERE glue_stmt.effect = 'Allow' - AND any(action IN glue_stmt.action WHERE toLower(action) = 'glue:createdevendpoint' OR action = '*' OR toLower(action) = 'glue:*') - - // Find target role with elevated permissions - MATCH (aws)--(target_role:AWSRole)--(target_policy:AWSPolicy)--(target_stmt:AWSPolicyStatement) - WHERE target_stmt.effect = 'Allow' - AND ( - any(action IN target_stmt.action WHERE action = '*') - OR any(action IN target_stmt.action WHERE toLower(action) = 'iam:*') - ) - - // Deduplicate before creating virtual nodes - WITH DISTINCT escalation_outcome, aws, principal, effective_principal, target_role - - // Create virtual Glue endpoint node (one per unique principal->target pair) - CALL apoc.create.vNode(['GlueDevEndpoint'], { - name: 'New Dev Endpoint', - description: 'Glue endpoint with target role attached', - id: effective_principal.arn + '->' + target_role.arn - }) - YIELD node AS glue_endpoint - - CALL apoc.create.vRelationship(effective_principal, 'CREATES_ENDPOINT', { - permissions: ['iam:PassRole', 'glue:CreateDevEndpoint'], - technique: 'new-passrole' - }, glue_endpoint) - YIELD rel AS create_rel - - CALL apoc.create.vRelationship(glue_endpoint, 'RUNS_AS', {}, target_role) - YIELD rel AS runs_rel - - CALL apoc.create.vRelationship(target_role, 'GRANTS_ACCESS', { - reference: 'https://pathfinding.cloud/paths/glue-001' - }, escalation_outcome) - YIELD rel AS grants_rel - - // Re-match paths for visualization - MATCH path_principal = (aws)--(principal) - MATCH path_target = (aws)--(target_role) + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path_principal, path_target, - glue_endpoint, escalation_outcome, create_rel, runs_rel, grants_rel + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, parameters=[], ) -AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY_ASSUME_ROLE = AttackPathsQueryDefinition( - id="aws-iam-privesc-attach-role-policy-assume-role", - name="Privilege Escalation: iam:AttachRolePolicy + sts:AssumeRole", - description="Detect principals who can both attach policies to roles AND assume those roles. This two-step attack allows modifying a role's permissions then assuming it to gain elevated access. This is a principal-access escalation path (pathfinding.cloud: iam-014).", +# EC2-002 +AWS_EC2_PRIVESC_MODIFY_INSTANCE_ATTRIBUTE = AttackPathsQueryDefinition( + id="aws-ec2-privesc-modify-instance-attribute", + name="EC2 Role Hijacking via UserData Injection (EC2-002)", + short_description="Inject malicious scripts into EC2 instance userData to gain the attached role's permissions.", + description="Detect principals who can modify EC2 instance userData, stop, and start instances. This allows injecting malicious scripts that execute on instance restart, gaining the permissions of the instance's attached IAM role.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - EC2-002 - ec2:ModifyInstanceAttribute + ec2:StopInstances + ec2:StartInstances", + link="https://pathfinding.cloud/paths/ec2-002", + ), provider="aws", cypher=f""" - // Create a virtual escalation outcome node (styled like a finding) - CALL apoc.create.vNode(['PrivilegeEscalation'], {{ - id: 'effective-administrator', - check_title: 'Privilege Escalation', - name: 'Effective Administrator', - status: 'FAIL', - severity: 'critical' - }}) - YIELD node AS admin_outcome + // Find principals with ec2:ModifyInstanceAttribute permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(modify_policy:AWSPolicy)--(stmt_modify:AWSPolicyStatement) + WHERE stmt_modify.effect = 'Allow' + AND any(action IN stmt_modify.action WHERE + toLower(action) = 'ec2:modifyinstanceattribute' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) - WITH admin_outcome + // Find ec2:StopInstances permission (can be same or different policy) + MATCH (principal)--(stop_policy:AWSPolicy)--(stmt_stop:AWSPolicyStatement) + WHERE stmt_stop.effect = 'Allow' + AND any(action IN stmt_stop.action WHERE + toLower(action) = 'ec2:stopinstances' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) - // Find principals in the account - MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal) + // Find ec2:StartInstances permission (can be same or different policy) + MATCH (principal)--(start_policy:AWSPolicy)--(stmt_start:AWSPolicyStatement) + WHERE stmt_start.effect = 'Allow' + AND any(action IN stmt_start.action WHERE + toLower(action) = 'ec2:startinstances' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) - // Find statements granting iam:AttachRolePolicy - MATCH path_attach = (principal)--(attach_policy:AWSPolicy)--(stmt_attach:AWSPolicyStatement) - WHERE stmt_attach.effect = 'Allow' - AND any(action IN stmt_attach.action WHERE + // Find EC2 instances with instance profiles (potential targets) + MATCH path_target = (aws)--(ec2:EC2Instance)-[:STS_ASSUMEROLE_ALLOW]->(target_role:AWSRole) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# EC2-003 +AWS_EC2_PRIVESC_PASSROLE_SPOT_INSTANCES = AttackPathsQueryDefinition( + id="aws-ec2-privesc-passrole-spot-instances", + name="Spot Instance Launch with Privileged Role (EC2-003)", + short_description="Launch EC2 Spot Instances with privileged IAM roles to gain their permissions via IMDS.", + description="Detect principals who can pass IAM roles and request EC2 Spot Instances. This allows launching a spot instance with a privileged role attached, gaining that role's permissions via the instance metadata service.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - EC2-003 - iam:PassRole + ec2:RequestSpotInstances", + link="https://pathfinding.cloud/paths/ec2-003", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find ec2:RequestSpotInstances permission + MATCH (principal)--(spot_policy:AWSPolicy)--(stmt_spot:AWSPolicyStatement) + WHERE stmt_spot.effect = 'Allow' + AND any(action IN stmt_spot.action WHERE + toLower(action) = 'ec2:requestspotinstances' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) + + // Find roles that trust EC2 service (can be passed to EC2 spot instances) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ec2.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# EC2-004 +AWS_EC2_PRIVESC_LAUNCH_TEMPLATE = AttackPathsQueryDefinition( + id="aws-ec2-privesc-launch-template", + name="Launch Template Poisoning for Role Access (EC2-004)", + short_description="Inject malicious userData into launch templates that reference privileged roles, no PassRole needed.", + description="Detect principals who can create new launch template versions and modify launch templates. This allows injecting malicious user data into existing templates that already reference privileged IAM roles, without requiring iam:PassRole permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - EC2-004 - ec2:CreateLaunchTemplateVersion + ec2:ModifyLaunchTemplate", + link="https://pathfinding.cloud/paths/ec2-004", + ), + provider="aws", + cypher=f""" + // Find principals with ec2:CreateLaunchTemplateVersion permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(create_policy:AWSPolicy)--(stmt_create:AWSPolicyStatement) + WHERE stmt_create.effect = 'Allow' + AND any(action IN stmt_create.action WHERE + toLower(action) = 'ec2:createlaunchtemplateversion' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) + + // Find ec2:ModifyLaunchTemplate permission + MATCH (principal)--(modify_policy:AWSPolicy)--(stmt_modify:AWSPolicyStatement) + WHERE stmt_modify.effect = 'Allow' + AND any(action IN stmt_modify.action WHERE + toLower(action) = 'ec2:modifylaunchtemplate' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) + + // Find launch templates in the account (potential targets) + MATCH path_target = (aws)--(template:LaunchTemplate) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# EC2INSTANCECONNECT-003 +AWS_EC2INSTANCECONNECT_PRIVESC_SEND_SSH_PUBLIC_KEY = AttackPathsQueryDefinition( + id="aws-ec2instanceconnect-privesc-send-ssh-public-key", + name="EC2 Instance Connect SSH Access for Role Credentials (EC2INSTANCECONNECT-003)", + short_description="Push a temporary SSH key to an EC2 instance via Instance Connect to access its attached role credentials through IMDS.", + description="Detect principals who can send SSH public keys via EC2 Instance Connect. This allows establishing an SSH session on a running EC2 instance and retrieving the attached IAM role's temporary credentials from the Instance Metadata Service (IMDS), gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - EC2INSTANCECONNECT-003 - ec2-instance-connect:SendSSHPublicKey", + link="https://pathfinding.cloud/paths/ec2instanceconnect-003", + ), + provider="aws", + cypher=f""" + // Find principals with ec2-instance-connect:SendSSHPublicKey permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(connect_policy:AWSPolicy)--(stmt_connect:AWSPolicyStatement) + WHERE stmt_connect.effect = 'Allow' + AND any(action IN stmt_connect.action WHERE + toLower(action) = 'ec2-instance-connect:sendsshpublickey' + OR toLower(action) = 'ec2-instance-connect:*' + OR action = '*' + ) + + // Find EC2 instances with attached roles (targets for credential theft via IMDS) + MATCH path_target = (aws)--(ec2:EC2Instance)-[:STS_ASSUMEROLE_ALLOW]->(target_role:AWSRole) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# ECS-001 +AWS_ECS_PRIVESC_PASSROLE_CREATE_SERVICE = AttackPathsQueryDefinition( + id="aws-ecs-privesc-passrole-create-service", + name="ECS Service Creation with Privileged Role (ECS-001 - New Cluster)", + short_description="Create an ECS cluster and service with a privileged Fargate task role to execute arbitrary code.", + description="Detect principals who can pass IAM roles, create ECS clusters, register task definitions, and create services. This allows creating a Fargate task with a privileged role attached, gaining that role's permissions to execute arbitrary code via the container.", + provider="aws", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - ECS-001 - iam:PassRole + ecs:CreateCluster + ecs:RegisterTaskDefinition + ecs:CreateService", + link="https://pathfinding.cloud/paths/ecs-001", + ), + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find ecs:CreateCluster permission + MATCH (principal)--(cluster_policy:AWSPolicy)--(stmt_cluster:AWSPolicyStatement) + WHERE stmt_cluster.effect = 'Allow' + AND any(action IN stmt_cluster.action WHERE + toLower(action) = 'ecs:createcluster' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:RegisterTaskDefinition permission + MATCH (principal)--(taskdef_policy:AWSPolicy)--(stmt_taskdef:AWSPolicyStatement) + WHERE stmt_taskdef.effect = 'Allow' + AND any(action IN stmt_taskdef.action WHERE + toLower(action) = 'ecs:registertaskdefinition' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:CreateService permission + MATCH (principal)--(service_policy:AWSPolicy)--(stmt_service:AWSPolicyStatement) + WHERE stmt_service.effect = 'Allow' + AND any(action IN stmt_service.action WHERE + toLower(action) = 'ecs:createservice' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find roles that trust ECS tasks service (can be passed to ECS tasks) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ecs-tasks.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# ECS-002 +AWS_ECS_PRIVESC_PASSROLE_RUN_TASK = AttackPathsQueryDefinition( + id="aws-ecs-privesc-passrole-run-task", + name="ECS Task Execution with Privileged Role (ECS-002 - New Cluster)", + short_description="Create an ECS cluster and run a one-off Fargate task with a privileged role to execute arbitrary code.", + description="Detect principals who can pass IAM roles, create ECS clusters, register task definitions, and run tasks. This allows creating a Fargate task with a privileged role attached, gaining that role's permissions to execute arbitrary code via the container. Unlike ecs:CreateService, ecs:RunTask executes the task once without creating a persistent service.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - ECS-002 - iam:PassRole + ecs:CreateCluster + ecs:RegisterTaskDefinition + ecs:RunTask", + link="https://pathfinding.cloud/paths/ecs-002", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find ecs:CreateCluster permission + MATCH (principal)--(cluster_policy:AWSPolicy)--(stmt_cluster:AWSPolicyStatement) + WHERE stmt_cluster.effect = 'Allow' + AND any(action IN stmt_cluster.action WHERE + toLower(action) = 'ecs:createcluster' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:RegisterTaskDefinition permission + MATCH (principal)--(taskdef_policy:AWSPolicy)--(stmt_taskdef:AWSPolicyStatement) + WHERE stmt_taskdef.effect = 'Allow' + AND any(action IN stmt_taskdef.action WHERE + toLower(action) = 'ecs:registertaskdefinition' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:RunTask permission + MATCH (principal)--(runtask_policy:AWSPolicy)--(stmt_runtask:AWSPolicyStatement) + WHERE stmt_runtask.effect = 'Allow' + AND any(action IN stmt_runtask.action WHERE + toLower(action) = 'ecs:runtask' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find roles that trust ECS tasks service (can be passed to ECS tasks) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ecs-tasks.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# ECS-003 +AWS_ECS_PRIVESC_PASSROLE_CREATE_SERVICE_EXISTING_CLUSTER = AttackPathsQueryDefinition( + id="aws-ecs-privesc-passrole-create-service-existing-cluster", + name="ECS Service Creation with Privileged Role (ECS-003 - Existing Cluster)", + short_description="Deploy a Fargate service with a privileged role on an existing ECS cluster.", + description="Detect principals who can pass IAM roles, register ECS task definitions, and create services on existing clusters. Unlike ECS-001, this does not require ecs:CreateCluster since it targets clusters that already exist. The attacker registers a task definition with a privileged role and launches it as a Fargate service, gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - ECS-003 - iam:PassRole + ecs:RegisterTaskDefinition + ecs:CreateService", + link="https://pathfinding.cloud/paths/ecs-003", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find ecs:RegisterTaskDefinition permission + MATCH (principal)--(taskdef_policy:AWSPolicy)--(stmt_taskdef:AWSPolicyStatement) + WHERE stmt_taskdef.effect = 'Allow' + AND any(action IN stmt_taskdef.action WHERE + toLower(action) = 'ecs:registertaskdefinition' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:CreateService permission + MATCH (principal)--(service_policy:AWSPolicy)--(stmt_service:AWSPolicyStatement) + WHERE stmt_service.effect = 'Allow' + AND any(action IN stmt_service.action WHERE + toLower(action) = 'ecs:createservice' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find roles that trust ECS tasks service (can be passed to ECS tasks) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ecs-tasks.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# ECS-004 +AWS_ECS_PRIVESC_PASSROLE_RUN_TASK_EXISTING_CLUSTER = AttackPathsQueryDefinition( + id="aws-ecs-privesc-passrole-run-task-existing-cluster", + name="ECS Task Execution with Privileged Role (ECS-004 - Existing Cluster)", + short_description="Run a one-off Fargate task with a privileged role on an existing ECS cluster.", + description="Detect principals who can pass IAM roles, register ECS task definitions, and run tasks on existing clusters. Unlike ECS-002, this does not require ecs:CreateCluster since it targets clusters that already exist. The attacker registers a task definition with a privileged role and runs it as a one-off Fargate task, gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - ECS-004 - iam:PassRole + ecs:RegisterTaskDefinition + ecs:RunTask", + link="https://pathfinding.cloud/paths/ecs-004", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find ecs:RegisterTaskDefinition permission + MATCH (principal)--(taskdef_policy:AWSPolicy)--(stmt_taskdef:AWSPolicyStatement) + WHERE stmt_taskdef.effect = 'Allow' + AND any(action IN stmt_taskdef.action WHERE + toLower(action) = 'ecs:registertaskdefinition' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:RunTask permission + MATCH (principal)--(runtask_policy:AWSPolicy)--(stmt_runtask:AWSPolicyStatement) + WHERE stmt_runtask.effect = 'Allow' + AND any(action IN stmt_runtask.action WHERE + toLower(action) = 'ecs:runtask' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find roles that trust ECS tasks service (can be passed to ECS tasks) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ecs-tasks.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# ECS-005 +AWS_ECS_PRIVESC_PASSROLE_START_TASK_EXISTING_CLUSTER = AttackPathsQueryDefinition( + id="aws-ecs-privesc-passrole-start-task-existing-cluster", + name="ECS Task Start with Privileged Role on EC2 (ECS-005 - Existing Cluster)", + short_description="Register a task definition with a privileged role and start it on an EC2 container instance to execute arbitrary code.", + description="Detect principals who can pass IAM roles, register ECS task definitions, and start tasks on existing EC2 container instances. Unlike ecs:RunTask which works with both EC2 and Fargate, ecs:StartTask is specific to EC2 launch types and requires specifying an existing container instance ARN. The attacker registers a task definition with a privileged role and starts it on a container instance, gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - ECS-005 - iam:PassRole + ecs:RegisterTaskDefinition + ecs:StartTask", + link="https://pathfinding.cloud/paths/ecs-005", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find ecs:RegisterTaskDefinition permission + MATCH (principal)--(taskdef_policy:AWSPolicy)--(stmt_taskdef:AWSPolicyStatement) + WHERE stmt_taskdef.effect = 'Allow' + AND any(action IN stmt_taskdef.action WHERE + toLower(action) = 'ecs:registertaskdefinition' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:StartTask permission + MATCH (principal)--(starttask_policy:AWSPolicy)--(stmt_starttask:AWSPolicyStatement) + WHERE stmt_starttask.effect = 'Allow' + AND any(action IN stmt_starttask.action WHERE + toLower(action) = 'ecs:starttask' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find roles that trust ECS tasks service (can be passed to ECS tasks) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ecs-tasks.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# ECS-006 +AWS_ECS_PRIVESC_EXECUTE_COMMAND = AttackPathsQueryDefinition( + id="aws-ecs-privesc-execute-command", + name="ECS Exec Container Hijacking for Role Credentials (ECS-006)", + short_description="Shell into a running ECS container via ECS Exec to steal the attached task role's credentials.", + description="Detect principals who can execute commands in running ECS containers and describe tasks. This allows establishing an interactive shell session in a container where ECS Exec is enabled, then retrieving the task role's temporary credentials from the container metadata service, without requiring iam:PassRole.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - ECS-006 - ecs:ExecuteCommand + ecs:DescribeTasks", + link="https://pathfinding.cloud/paths/ecs-006", + ), + provider="aws", + cypher=f""" + // Find principals with ecs:ExecuteCommand permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(exec_policy:AWSPolicy)--(stmt_exec:AWSPolicyStatement) + WHERE stmt_exec.effect = 'Allow' + AND any(action IN stmt_exec.action WHERE + toLower(action) = 'ecs:executecommand' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:DescribeTasks permission (required by AWS CLI to get container runtime ID) + MATCH (principal)--(describe_policy:AWSPolicy)--(stmt_describe:AWSPolicyStatement) + WHERE stmt_describe.effect = 'Allow' + AND any(action IN stmt_describe.action WHERE + toLower(action) = 'ecs:describetasks' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find roles that trust ECS tasks service (already attached to running tasks) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ecs-tasks.amazonaws.com'}}) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# GLUE-001 +AWS_GLUE_PRIVESC_PASSROLE_DEV_ENDPOINT = AttackPathsQueryDefinition( + id="aws-glue-privesc-passrole-dev-endpoint", + name="Glue Dev Endpoint with Privileged Role (GLUE-001)", + short_description="Create a Glue development endpoint with a privileged role attached to gain its permissions.", + description="Detect principals who can pass IAM roles and create Glue development endpoints. This allows creating a dev endpoint with a privileged role attached, gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - GLUE-001 - iam:PassRole + glue:CreateDevEndpoint", + link="https://pathfinding.cloud/paths/glue-001", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find glue:CreateDevEndpoint permission + MATCH (principal)--(glue_policy:AWSPolicy)--(stmt_glue:AWSPolicyStatement) + WHERE stmt_glue.effect = 'Allow' + AND any(action IN stmt_glue.action WHERE + toLower(action) = 'glue:createdevendpoint' + OR toLower(action) = 'glue:*' + OR action = '*' + ) + + // Find roles that trust Glue service (can be passed to Glue) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'glue.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# GLUE-002 +AWS_GLUE_PRIVESC_UPDATE_DEV_ENDPOINT = AttackPathsQueryDefinition( + id="aws-glue-privesc-update-dev-endpoint", + name="Glue Dev Endpoint SSH Hijacking via Update (GLUE-002)", + short_description="Update an existing Glue development endpoint to inject an SSH public key and access its attached role credentials.", + description="Detect principals who can update Glue development endpoints. This allows adding an attacker-controlled SSH public key to an existing dev endpoint that already has a privileged role attached, then SSHing into it to steal the role's temporary credentials without requiring iam:PassRole.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - GLUE-002 - glue:UpdateDevEndpoint", + link="https://pathfinding.cloud/paths/glue-002", + ), + provider="aws", + cypher=f""" + // Find principals with glue:UpdateDevEndpoint permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'glue:updatedevendpoint' + OR toLower(action) = 'glue:*' + OR action = '*' + ) + + // Find roles that trust Glue service (already attached to existing dev endpoints) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'glue.amazonaws.com'}}) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# GLUE-003 +AWS_GLUE_PRIVESC_PASSROLE_CREATE_JOB = AttackPathsQueryDefinition( + id="aws-glue-privesc-passrole-create-job", + name="Glue Job Creation with Privileged Role (GLUE-003)", + short_description="Create a Glue job with a privileged role and start it to execute arbitrary code with that role's permissions.", + description="Detect principals who can pass IAM roles, create Glue jobs, and start job runs. This allows creating a Python shell job with a privileged role attached and executing arbitrary code that modifies IAM permissions, a cost-effective alternative to Glue development endpoints.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - GLUE-003 - iam:PassRole + glue:CreateJob + glue:StartJobRun", + link="https://pathfinding.cloud/paths/glue-003", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find glue:CreateJob permission + MATCH (principal)--(createjob_policy:AWSPolicy)--(stmt_createjob:AWSPolicyStatement) + WHERE stmt_createjob.effect = 'Allow' + AND any(action IN stmt_createjob.action WHERE + toLower(action) = 'glue:createjob' + OR toLower(action) = 'glue:*' + OR action = '*' + ) + + // Find glue:StartJobRun permission + MATCH (principal)--(startjob_policy:AWSPolicy)--(stmt_startjob:AWSPolicyStatement) + WHERE stmt_startjob.effect = 'Allow' + AND any(action IN stmt_startjob.action WHERE + toLower(action) = 'glue:startjobrun' + OR toLower(action) = 'glue:*' + OR action = '*' + ) + + // Find roles that trust Glue service (can be passed to Glue jobs) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'glue.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# GLUE-004 +AWS_GLUE_PRIVESC_PASSROLE_CREATE_JOB_TRIGGER = AttackPathsQueryDefinition( + id="aws-glue-privesc-passrole-create-job-trigger", + name="Glue Job Creation with Scheduled Trigger and Privileged Role (GLUE-004)", + short_description="Create a Glue job with a privileged role and a scheduled trigger to persistently execute arbitrary code.", + description="Detect principals who can pass IAM roles, create Glue jobs, and create triggers with automatic activation. Unlike manual execution via StartJobRun, this creates a persistent attack by scheduling the job to run repeatedly, making it harder to detect and remediate.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - GLUE-004 - iam:PassRole + glue:CreateJob + glue:CreateTrigger", + link="https://pathfinding.cloud/paths/glue-004", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find glue:CreateJob permission + MATCH (principal)--(createjob_policy:AWSPolicy)--(stmt_createjob:AWSPolicyStatement) + WHERE stmt_createjob.effect = 'Allow' + AND any(action IN stmt_createjob.action WHERE + toLower(action) = 'glue:createjob' + OR toLower(action) = 'glue:*' + OR action = '*' + ) + + // Find glue:CreateTrigger permission + MATCH (principal)--(trigger_policy:AWSPolicy)--(stmt_trigger:AWSPolicyStatement) + WHERE stmt_trigger.effect = 'Allow' + AND any(action IN stmt_trigger.action WHERE + toLower(action) = 'glue:createtrigger' + OR toLower(action) = 'glue:*' + OR action = '*' + ) + + // Find roles that trust Glue service (can be passed to Glue jobs) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'glue.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# GLUE-005 +AWS_GLUE_PRIVESC_PASSROLE_UPDATE_JOB = AttackPathsQueryDefinition( + id="aws-glue-privesc-passrole-update-job", + name="Glue Job Hijacking via Update with Privileged Role (GLUE-005)", + short_description="Update an existing Glue job to attach a privileged role and inject malicious code, then start it to gain that role's permissions.", + description="Detect principals who can pass IAM roles, update existing Glue jobs, and start job runs. This allows modifying an existing job's role and script to execute arbitrary code with elevated privileges, a stealthier variant of job creation since it reuses existing infrastructure rather than creating new resources.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - GLUE-005 - iam:PassRole + glue:UpdateJob + glue:StartJobRun", + link="https://pathfinding.cloud/paths/glue-005", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find glue:UpdateJob permission + MATCH (principal)--(updatejob_policy:AWSPolicy)--(stmt_updatejob:AWSPolicyStatement) + WHERE stmt_updatejob.effect = 'Allow' + AND any(action IN stmt_updatejob.action WHERE + toLower(action) = 'glue:updatejob' + OR toLower(action) = 'glue:*' + OR action = '*' + ) + + // Find glue:StartJobRun permission + MATCH (principal)--(startjob_policy:AWSPolicy)--(stmt_startjob:AWSPolicyStatement) + WHERE stmt_startjob.effect = 'Allow' + AND any(action IN stmt_startjob.action WHERE + toLower(action) = 'glue:startjobrun' + OR toLower(action) = 'glue:*' + OR action = '*' + ) + + // Find roles that trust Glue service (can be passed to Glue jobs) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'glue.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# GLUE-006 +AWS_GLUE_PRIVESC_PASSROLE_UPDATE_JOB_TRIGGER = AttackPathsQueryDefinition( + id="aws-glue-privesc-passrole-update-job-trigger", + name="Glue Job Hijacking with Scheduled Trigger and Privileged Role (GLUE-006)", + short_description="Update an existing Glue job to attach a privileged role and inject malicious code, then create a scheduled trigger for persistent automated execution.", + description="Detect principals who can pass IAM roles, update existing Glue jobs, and create triggers with automatic activation. This combines the stealth of modifying existing infrastructure with the persistence of scheduled automation, creating a recurring backdoor that re-executes even after remediation attempts.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - GLUE-006 - iam:PassRole + glue:UpdateJob + glue:CreateTrigger", + link="https://pathfinding.cloud/paths/glue-006", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find glue:UpdateJob permission + MATCH (principal)--(updatejob_policy:AWSPolicy)--(stmt_updatejob:AWSPolicyStatement) + WHERE stmt_updatejob.effect = 'Allow' + AND any(action IN stmt_updatejob.action WHERE + toLower(action) = 'glue:updatejob' + OR toLower(action) = 'glue:*' + OR action = '*' + ) + + // Find glue:CreateTrigger permission + MATCH (principal)--(trigger_policy:AWSPolicy)--(stmt_trigger:AWSPolicyStatement) + WHERE stmt_trigger.effect = 'Allow' + AND any(action IN stmt_trigger.action WHERE + toLower(action) = 'glue:createtrigger' + OR toLower(action) = 'glue:*' + OR action = '*' + ) + + // Find roles that trust Glue service (can be passed to Glue jobs) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'glue.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-001 +AWS_IAM_PRIVESC_CREATE_POLICY_VERSION = AttackPathsQueryDefinition( + id="aws-iam-privesc-create-policy-version", + name="Policy Version Override for Self-Escalation (IAM-001)", + short_description="Create a new version of an attached policy with administrative permissions, instantly escalating the principal's own privileges.", + description="Detect principals who can create new policy versions. If a customer-managed policy is already attached to a principal and that principal has iam:CreatePolicyVersion on that policy, they can replace its contents with a fully permissive policy and set it as the default, gaining immediate administrative access.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-001 - iam:CreatePolicyVersion", + link="https://pathfinding.cloud/paths/iam-001", + ), + provider="aws", + cypher=f""" + // Find principals with iam:CreatePolicyVersion permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:createpolicyversion' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find customer-managed policies attached to the same principal that can be overwritten + MATCH path_target = (aws)--(target_policy:AWSPolicy)--(principal) + WHERE target_policy.arn CONTAINS $provider_uid + AND any(resource IN stmt.resource WHERE + resource = '*' + OR target_policy.arn CONTAINS resource + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-002 +AWS_IAM_PRIVESC_CREATE_ACCESS_KEY = AttackPathsQueryDefinition( + id="aws-iam-privesc-create-access-key", + name="Access Key Creation for Lateral Movement (IAM-002)", + short_description="Create access keys for other IAM users to gain their permissions and move laterally across the account.", + description="Detect principals who can create access keys for other IAM users. This allows generating new credentials for any target user within the resource scope, immediately gaining that user's permissions without needing their password or existing keys.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-002 - iam:CreateAccessKey", + link="https://pathfinding.cloud/paths/iam-002", + ), + provider="aws", + cypher=f""" + // Find principals with iam:CreateAccessKey permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:createaccesskey' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target users that the principal can create access keys for + MATCH path_target = (aws)--(target_user:AWSUser) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_user.arn CONTAINS resource + OR resource CONTAINS target_user.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-003 +AWS_IAM_PRIVESC_DELETE_CREATE_ACCESS_KEY = AttackPathsQueryDefinition( + id="aws-iam-privesc-delete-create-access-key", + name="Access Key Rotation Attack for Lateral Movement (IAM-003)", + short_description="Delete and recreate access keys for other IAM users to bypass the two-key limit and gain their permissions.", + description="Detect principals who can both delete and create access keys for other IAM users. This variation of IAM-002 handles the scenario where a target user already has the maximum of two access keys by first deleting one, then creating a replacement under the attacker's control.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-003 - iam:CreateAccessKey + iam:DeleteAccessKey", + link="https://pathfinding.cloud/paths/iam-003", + ), + provider="aws", + cypher=f""" + // Find principals with iam:CreateAccessKey permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:createaccesskey' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find iam:DeleteAccessKey permission + MATCH (principal)--(delete_policy:AWSPolicy)--(stmt_delete:AWSPolicyStatement) + WHERE stmt_delete.effect = 'Allow' + AND any(action IN stmt_delete.action WHERE + toLower(action) = 'iam:deleteaccesskey' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target users that the principal can rotate access keys for + MATCH path_target = (aws)--(target_user:AWSUser) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_user.arn CONTAINS resource + OR resource CONTAINS target_user.name + ) + AND any(resource IN stmt_delete.resource WHERE + resource = '*' + OR target_user.arn CONTAINS resource + OR resource CONTAINS target_user.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-004 +AWS_IAM_PRIVESC_CREATE_LOGIN_PROFILE = AttackPathsQueryDefinition( + id="aws-iam-privesc-create-login-profile", + name="Console Login Profile Creation for Lateral Movement (IAM-004)", + short_description="Create console login profiles for other IAM users to access the AWS Console with their permissions.", + description="Detect principals who can create console login profiles for other IAM users. By setting a known password on a target user that lacks a login profile, the attacker gains AWS Console access with that user's permissions without needing their existing credentials.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-004 - iam:CreateLoginProfile", + link="https://pathfinding.cloud/paths/iam-004", + ), + provider="aws", + cypher=f""" + // Find principals with iam:CreateLoginProfile permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:createloginprofile' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target users that the principal can create login profiles for + MATCH path_target = (aws)--(target_user:AWSUser) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_user.arn CONTAINS resource + OR resource CONTAINS target_user.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-005 +AWS_IAM_PRIVESC_PUT_ROLE_POLICY = AttackPathsQueryDefinition( + id="aws-iam-privesc-put-role-policy", + name="Inline Policy Injection for Self-Escalation (IAM-005)", + short_description="Attach an inline policy with administrative permissions to your own role, instantly escalating privileges.", + description="Detect roles that can use iam:PutRolePolicy on themselves. A role with this permission can attach an inline policy granting any permissions, including full administrative access, without needing to modify or assume any other resource.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-005 - iam:PutRolePolicy", + link="https://pathfinding.cloud/paths/iam-005", + ), + provider="aws", + cypher=f""" + // Find roles with iam:PutRolePolicy permission scoped to themselves + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(role:AWSRole)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:putrolepolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + AND any(resource IN stmt.resource WHERE + resource = '*' + OR role.arn CONTAINS resource + OR resource CONTAINS role.name + ) + + UNWIND nodes(path_principal) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-006 +AWS_IAM_PRIVESC_UPDATE_LOGIN_PROFILE = AttackPathsQueryDefinition( + id="aws-iam-privesc-update-login-profile", + name="Console Password Override for Lateral Movement (IAM-006)", + short_description="Change the console password of other IAM users to log in as them and gain their permissions.", + description="Detect principals who can update console login profiles for other IAM users. By resetting a target user's password, the attacker gains AWS Console access with that user's permissions. Unlike IAM-004, this targets users who already have a login profile configured.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-006 - iam:UpdateLoginProfile", + link="https://pathfinding.cloud/paths/iam-006", + ), + provider="aws", + cypher=f""" + // Find principals with iam:UpdateLoginProfile permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:updateloginprofile' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target users that the principal can update login profiles for + MATCH path_target = (aws)--(target_user:AWSUser) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_user.arn CONTAINS resource + OR resource CONTAINS target_user.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-007 +AWS_IAM_PRIVESC_PUT_USER_POLICY = AttackPathsQueryDefinition( + id="aws-iam-privesc-put-user-policy", + name="Inline Policy Injection on User for Self-Escalation (IAM-007)", + short_description="Attach an inline policy with administrative permissions to your own IAM user, instantly escalating privileges.", + description="Detect IAM users that can use iam:PutUserPolicy on themselves. A user with this permission can attach an inline policy granting any permissions, including full administrative access, without needing to modify or assume any other resource. This is the user equivalent of IAM-005 (PutRolePolicy).", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-007 - iam:PutUserPolicy", + link="https://pathfinding.cloud/paths/iam-007", + ), + provider="aws", + cypher=f""" + // Find users with iam:PutUserPolicy permission scoped to themselves + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(user:AWSUser)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:putuserpolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + AND any(resource IN stmt.resource WHERE + resource = '*' + OR user.arn CONTAINS resource + OR resource CONTAINS user.name + ) + + UNWIND nodes(path_principal) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-008 +AWS_IAM_PRIVESC_ATTACH_USER_POLICY = AttackPathsQueryDefinition( + id="aws-iam-privesc-attach-user-policy", + name="Managed Policy Attachment on User for Self-Escalation (IAM-008)", + short_description="Attach existing managed policies with administrative permissions to your own IAM user, instantly escalating privileges.", + description="Detect IAM users that can use iam:AttachUserPolicy on themselves. A user with this permission can attach any existing managed policy, including AdministratorAccess, to themselves without needing to modify or assume any other resource. Unlike IAM-007 (PutUserPolicy), this requires an existing managed policy with elevated permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-008 - iam:AttachUserPolicy", + link="https://pathfinding.cloud/paths/iam-008", + ), + provider="aws", + cypher=f""" + // Find users with iam:AttachUserPolicy permission scoped to themselves + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(user:AWSUser)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:attachuserpolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + AND any(resource IN stmt.resource WHERE + resource = '*' + OR user.arn CONTAINS resource + OR resource CONTAINS user.name + ) + + UNWIND nodes(path_principal) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-009 +AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY = AttackPathsQueryDefinition( + id="aws-iam-privesc-attach-role-policy", + name="Managed Policy Attachment on Role for Self-Escalation (IAM-009)", + short_description="Attach existing managed policies with administrative permissions to your own IAM role, instantly escalating privileges.", + description="Detect IAM roles that can use iam:AttachRolePolicy on themselves. A role with this permission can attach any existing managed policy, including AdministratorAccess, to itself without needing to modify or assume any other resource. Unlike IAM-005 (PutRolePolicy), this requires an existing managed policy with elevated permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-009 - iam:AttachRolePolicy", + link="https://pathfinding.cloud/paths/iam-009", + ), + provider="aws", + cypher=f""" + // Find roles with iam:AttachRolePolicy permission scoped to themselves + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(role:AWSRole)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:attachrolepolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + AND any(resource IN stmt.resource WHERE + resource = '*' + OR role.arn CONTAINS resource + OR resource CONTAINS role.name + ) + + UNWIND nodes(path_principal) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-010 +AWS_IAM_PRIVESC_ATTACH_GROUP_POLICY = AttackPathsQueryDefinition( + id="aws-iam-privesc-attach-group-policy", + name="Managed Policy Attachment on Group for Self-Escalation (IAM-010)", + short_description="Attach existing managed policies with administrative permissions to a group you belong to, escalating privileges for all group members.", + description="Detect IAM users that can use iam:AttachGroupPolicy on a group they are a member of. A user with this permission can attach any existing managed policy, including AdministratorAccess, to a group they belong to, immediately escalating privileges for all group members.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-010 - iam:AttachGroupPolicy", + link="https://pathfinding.cloud/paths/iam-010", + ), + provider="aws", + cypher=f""" + // Find users with iam:AttachGroupPolicy permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(user:AWSUser)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:attachgrouppolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find groups the user is a member of and can attach policies to + MATCH path_target = (aws)-[:RESOURCE]->(target_group:AWSGroup)<-[:MEMBER_AWS_GROUP]-(user) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_group.arn CONTAINS resource + OR resource CONTAINS target_group.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-011 +AWS_IAM_PRIVESC_PUT_GROUP_POLICY = AttackPathsQueryDefinition( + id="aws-iam-privesc-put-group-policy", + name="Inline Policy Injection on Group for Self-Escalation (IAM-011)", + short_description="Attach an inline policy with administrative permissions to a group you belong to, escalating privileges for all group members.", + description="Detect IAM users that can use iam:PutGroupPolicy on a group they are a member of. A user with this permission can attach an inline policy granting any permissions to a group they belong to, immediately escalating privileges for all group members. Unlike IAM-010, this does not require an existing managed policy.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-011 - iam:PutGroupPolicy", + link="https://pathfinding.cloud/paths/iam-011", + ), + provider="aws", + cypher=f""" + // Find users with iam:PutGroupPolicy permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(user:AWSUser)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:putgrouppolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find groups the user is a member of and can put policies on + MATCH path_target = (aws)-[:RESOURCE]->(target_group:AWSGroup)<-[:MEMBER_AWS_GROUP]-(user) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_group.arn CONTAINS resource + OR resource CONTAINS target_group.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-012 +AWS_IAM_PRIVESC_UPDATE_ASSUME_ROLE_POLICY = AttackPathsQueryDefinition( + id="aws-iam-privesc-update-assume-role-policy", + name="Trust Policy Hijacking for Role Assumption (IAM-012)", + short_description="Modify a role's trust policy to allow yourself to assume it, gaining the role's permissions.", + description="Detect principals who can update the assume role policy (trust policy) of other IAM roles. By modifying a target role's trust policy to trust the attacker's principal, the attacker can then assume the role and gain all its permissions, including potential administrative access.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-012 - iam:UpdateAssumeRolePolicy", + link="https://pathfinding.cloud/paths/iam-012", + ), + provider="aws", + cypher=f""" + // Find principals with iam:UpdateAssumeRolePolicy permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:updateassumerolepolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target roles whose trust policy can be modified + MATCH path_target = (aws)--(target_role:AWSRole) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-013 +AWS_IAM_PRIVESC_ADD_USER_TO_GROUP = AttackPathsQueryDefinition( + id="aws-iam-privesc-add-user-to-group", + name="Group Membership Hijacking for Privilege Escalation (IAM-013)", + short_description="Add yourself to a privileged IAM group to inherit its permissions, gaining access to all policies attached to the group.", + description="Detect principals who can add users to IAM groups. By adding themselves to a group with elevated permissions such as AdministratorAccess, the attacker immediately inherits all policies attached to that group. The level of access gained depends on the permissions of the target group.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-013 - iam:AddUserToGroup", + link="https://pathfinding.cloud/paths/iam-013", + ), + provider="aws", + cypher=f""" + // Find principals with iam:AddUserToGroup permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:addusertogroup' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target groups the principal can add users to + MATCH path_target = (aws)-[:RESOURCE]->(target_group:AWSGroup) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_group.arn CONTAINS resource + OR resource CONTAINS target_group.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-014 +AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY_ASSUME_ROLE = AttackPathsQueryDefinition( + id="aws-iam-privesc-attach-role-policy-assume-role", + name="Managed Policy Attachment with Role Assumption for Lateral Movement (IAM-014)", + short_description="Attach administrative managed policies to another role you can assume, then assume it to gain elevated privileges.", + description="Detect principals who can attach managed policies to a different IAM role and also assume that role. By attaching AdministratorAccess to a target role and then assuming it, the attacker gains full administrative access. This is a variation of IAM-009 for lateral movement where the principal targets another assumable role instead of their own.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-014 - iam:AttachRolePolicy + sts:AssumeRole", + link="https://pathfinding.cloud/paths/iam-014", + ), + provider="aws", + cypher=f""" + // Find principals with iam:AttachRolePolicy permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE toLower(action) = 'iam:attachrolepolicy' OR toLower(action) = 'iam:*' OR action = '*' ) - // Find statements granting sts:AssumeRole - MATCH path_assume = (principal)--(assume_policy:AWSPolicy)--(stmt_assume:AWSPolicyStatement) - WHERE stmt_assume.effect = 'Allow' - AND any(action IN stmt_assume.action WHERE + // Find target roles the principal can assume and attach policies to + MATCH path_target = (aws)--(target_role:AWSRole)<-[:STS_ASSUMEROLE_ALLOW]-(principal) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-015 +AWS_IAM_PRIVESC_ATTACH_USER_POLICY_CREATE_ACCESS_KEY = AttackPathsQueryDefinition( + id="aws-iam-privesc-attach-user-policy-create-access-key", + name="Managed Policy Attachment with Access Key Creation for Lateral Movement (IAM-015)", + short_description="Attach administrative managed policies to another IAM user and create access keys for them to gain programmatic access with elevated privileges.", + description="Detect principals who can attach managed policies to another IAM user and also create access keys for that user. By attaching AdministratorAccess to a target user and creating access keys, the attacker gains programmatic access with the target user's elevated permissions. This combines IAM-008 (AttachUserPolicy) with IAM-002 (CreateAccessKey) for lateral movement.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-015 - iam:AttachUserPolicy + iam:CreateAccessKey", + link="https://pathfinding.cloud/paths/iam-015", + ), + provider="aws", + cypher=f""" + // Find principals with iam:AttachUserPolicy permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:attachuserpolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find iam:CreateAccessKey permission + MATCH (principal)--(policy2:AWSPolicy)--(stmt2:AWSPolicyStatement) + WHERE stmt2.effect = 'Allow' + AND any(action IN stmt2.action WHERE + toLower(action) = 'iam:createaccesskey' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target users the principal can attach policies to and create keys for + MATCH path_target = (aws)--(target_user:AWSUser) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_user.arn CONTAINS resource + OR resource CONTAINS target_user.name + ) + AND any(resource IN stmt2.resource WHERE + resource = '*' + OR target_user.arn CONTAINS resource + OR resource CONTAINS target_user.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-016 +AWS_IAM_PRIVESC_CREATE_POLICY_VERSION_ASSUME_ROLE = AttackPathsQueryDefinition( + id="aws-iam-privesc-create-policy-version-assume-role", + name="Policy Version Override with Role Assumption for Lateral Movement (IAM-016)", + short_description="Create a new version of a customer-managed policy attached to another role with administrative permissions, then assume that role to gain elevated access.", + description="Detect principals who can create new versions of customer-managed policies attached to other roles and also assume those roles. By creating a new policy version with administrative permissions on a policy attached to a target role, then assuming that role, the attacker gains full administrative access. This is a variation of IAM-001 for lateral movement where the modified policy is attached to an assumable role rather than the attacker's own principal.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-016 - iam:CreatePolicyVersion + sts:AssumeRole", + link="https://pathfinding.cloud/paths/iam-016", + ), + provider="aws", + cypher=f""" + // Find principals with iam:CreatePolicyVersion permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:createpolicyversion' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target roles the principal can assume that have customer-managed policies the principal can modify + MATCH path_target = (aws)--(target_role:AWSRole)<-[:STS_ASSUMEROLE_ALLOW]-(principal) + MATCH (target_role)--(target_policy:AWSPolicy) + WHERE target_policy.arn CONTAINS $provider_uid + AND any(resource IN stmt.resource WHERE + resource = '*' + OR target_policy.arn CONTAINS resource + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-017 +AWS_IAM_PRIVESC_PUT_ROLE_POLICY_ASSUME_ROLE = AttackPathsQueryDefinition( + id="aws-iam-privesc-put-role-policy-assume-role", + name="Inline Policy Injection with Role Assumption for Lateral Movement (IAM-017)", + short_description="Attach an inline policy with administrative permissions to another role you can assume, then assume it to gain elevated privileges.", + description="Detect principals who can add inline policies to a different IAM role and also assume that role. By adding an inline policy granting administrative permissions to a target role and then assuming it, the attacker gains full administrative access. This is a variation of IAM-005 for lateral movement where the principal targets another assumable role instead of their own.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-017 - iam:PutRolePolicy + sts:AssumeRole", + link="https://pathfinding.cloud/paths/iam-017", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PutRolePolicy permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:putrolepolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target roles the principal can assume and put inline policies on + MATCH path_target = (aws)--(target_role:AWSRole)<-[:STS_ASSUMEROLE_ALLOW]-(principal) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-018 +AWS_IAM_PRIVESC_PUT_USER_POLICY_CREATE_ACCESS_KEY = AttackPathsQueryDefinition( + id="aws-iam-privesc-put-user-policy-create-access-key", + name="Inline Policy Injection with Access Key Creation for Lateral Movement (IAM-018)", + short_description="Attach an inline policy with administrative permissions to another IAM user and create access keys for them to gain programmatic access with elevated privileges.", + description="Detect principals who can add inline policies to another IAM user and also create access keys for that user. By adding an administrative inline policy to a target user and creating access keys, the attacker gains programmatic access with the target user's elevated permissions. This combines IAM-007 (PutUserPolicy) with IAM-002 (CreateAccessKey) for lateral movement.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-018 - iam:PutUserPolicy + iam:CreateAccessKey", + link="https://pathfinding.cloud/paths/iam-018", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PutUserPolicy permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:putuserpolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find iam:CreateAccessKey permission + MATCH (principal)--(policy2:AWSPolicy)--(stmt2:AWSPolicyStatement) + WHERE stmt2.effect = 'Allow' + AND any(action IN stmt2.action WHERE + toLower(action) = 'iam:createaccesskey' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target users the principal can put policies on and create keys for + MATCH path_target = (aws)--(target_user:AWSUser) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_user.arn CONTAINS resource + OR resource CONTAINS target_user.name + ) + AND any(resource IN stmt2.resource WHERE + resource = '*' + OR target_user.arn CONTAINS resource + OR resource CONTAINS target_user.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-019 +AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY_UPDATE_ASSUME_ROLE = AttackPathsQueryDefinition( + id="aws-iam-privesc-attach-role-policy-update-assume-role", + name="Managed Policy Attachment with Trust Policy Hijacking for Privilege Escalation (IAM-019)", + short_description="Attach administrative managed policies to a role and modify its trust policy to allow yourself to assume it, gaining elevated privileges without prior assume-role access.", + description="Detect principals who can attach managed policies to an IAM role and also update that role's trust policy. By attaching AdministratorAccess and modifying the trust policy to allow the attacker, the principal can then assume the role without needing pre-existing sts:AssumeRole permission. This combines IAM-009 (AttachRolePolicy) with IAM-012 (UpdateAssumeRolePolicy).", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-019 - iam:AttachRolePolicy + iam:UpdateAssumeRolePolicy", + link="https://pathfinding.cloud/paths/iam-019", + ), + provider="aws", + cypher=f""" + // Find principals with iam:AttachRolePolicy permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:attachrolepolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find iam:UpdateAssumeRolePolicy permission + MATCH (principal)--(policy2:AWSPolicy)--(stmt2:AWSPolicyStatement) + WHERE stmt2.effect = 'Allow' + AND any(action IN stmt2.action WHERE + toLower(action) = 'iam:updateassumerolepolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target roles the principal can attach policies to and update trust policy for + MATCH path_target = (aws)--(target_role:AWSRole) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + AND any(resource IN stmt2.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-020 +AWS_IAM_PRIVESC_CREATE_POLICY_VERSION_UPDATE_ASSUME_ROLE = AttackPathsQueryDefinition( + id="aws-iam-privesc-create-policy-version-update-assume-role", + name="Policy Version Override with Trust Policy Hijacking for Privilege Escalation (IAM-020)", + short_description="Create a new version of a customer-managed policy attached to a role with administrative permissions and modify its trust policy to assume it, without prior assume-role access.", + description="Detect principals who can create new versions of customer-managed policies attached to roles and also update those roles' trust policies. By creating an administrative policy version and modifying the trust policy to allow the attacker, the principal can assume the role without needing pre-existing sts:AssumeRole permission. This combines IAM-001 (CreatePolicyVersion) with IAM-012 (UpdateAssumeRolePolicy).", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-020 - iam:CreatePolicyVersion + iam:UpdateAssumeRolePolicy", + link="https://pathfinding.cloud/paths/iam-020", + ), + provider="aws", + cypher=f""" + // Find principals with iam:CreatePolicyVersion permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:createpolicyversion' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find iam:UpdateAssumeRolePolicy permission + MATCH (principal)--(policy2:AWSPolicy)--(stmt2:AWSPolicyStatement) + WHERE stmt2.effect = 'Allow' + AND any(action IN stmt2.action WHERE + toLower(action) = 'iam:updateassumerolepolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target roles with customer-managed policies the principal can modify and update trust policy for + MATCH path_target = (aws)--(target_role:AWSRole) + WHERE any(resource IN stmt2.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + MATCH (target_role)--(target_policy:AWSPolicy) + WHERE target_policy.arn CONTAINS $provider_uid + AND any(resource IN stmt.resource WHERE + resource = '*' + OR target_policy.arn CONTAINS resource + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-021 +AWS_IAM_PRIVESC_PUT_ROLE_POLICY_UPDATE_ASSUME_ROLE = AttackPathsQueryDefinition( + id="aws-iam-privesc-put-role-policy-update-assume-role", + name="Inline Policy Injection with Trust Policy Hijacking for Privilege Escalation (IAM-021)", + short_description="Add an inline policy with administrative permissions to a role and modify its trust policy to allow yourself to assume it, gaining elevated privileges without prior assume-role access.", + description="Detect principals who can add inline policies to an IAM role and also update that role's trust policy. By adding an administrative inline policy and modifying the trust policy to allow the attacker, the principal can then assume the role without needing pre-existing sts:AssumeRole permission. This combines IAM-005 (PutRolePolicy) with IAM-012 (UpdateAssumeRolePolicy).", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-021 - iam:PutRolePolicy + iam:UpdateAssumeRolePolicy", + link="https://pathfinding.cloud/paths/iam-021", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PutRolePolicy permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:putrolepolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find iam:UpdateAssumeRolePolicy permission + MATCH (principal)--(policy2:AWSPolicy)--(stmt2:AWSPolicyStatement) + WHERE stmt2.effect = 'Allow' + AND any(action IN stmt2.action WHERE + toLower(action) = 'iam:updateassumerolepolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find target roles the principal can put inline policies on and update trust policy for + MATCH path_target = (aws)--(target_role:AWSRole) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + AND any(resource IN stmt2.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# LAMBDA-001 +AWS_LAMBDA_PRIVESC_PASSROLE_CREATE_FUNCTION = AttackPathsQueryDefinition( + id="aws-lambda-privesc-passrole-create-function", + name="Lambda Function Creation with Privileged Role (LAMBDA-001)", + short_description="Create a Lambda function with a privileged IAM role and invoke it to execute code with that role's permissions.", + description="Detect principals who can create Lambda functions with privileged IAM roles and invoke them. By passing a privileged role to a new Lambda function and invoking it, the attacker executes code with the role's permissions, gaining access to any resources the role can access.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - LAMBDA-001 - iam:PassRole + lambda:CreateFunction + lambda:InvokeFunction", + link="https://pathfinding.cloud/paths/lambda-001", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find lambda:CreateFunction permission + MATCH (principal)--(create_policy:AWSPolicy)--(stmt_create:AWSPolicyStatement) + WHERE stmt_create.effect = 'Allow' + AND any(action IN stmt_create.action WHERE + toLower(action) = 'lambda:createfunction' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find lambda:InvokeFunction permission + MATCH (principal)--(invoke_policy:AWSPolicy)--(stmt_invoke:AWSPolicyStatement) + WHERE stmt_invoke.effect = 'Allow' + AND any(action IN stmt_invoke.action WHERE + toLower(action) = 'lambda:invokefunction' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find roles that trust Lambda service (can be passed to Lambda) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'lambda.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# LAMBDA-002 +AWS_LAMBDA_PRIVESC_PASSROLE_CREATE_FUNCTION_EVENT_SOURCE = AttackPathsQueryDefinition( + id="aws-lambda-privesc-passrole-create-function-event-source", + name="Lambda Function Creation with Event Source Trigger (LAMBDA-002)", + short_description="Create a Lambda function with a privileged IAM role and an event source mapping to trigger it automatically, executing code with the role's permissions.", + description="Detect principals who can create Lambda functions with privileged IAM roles and configure event source mappings to trigger them. By passing a privileged role to a new Lambda function and creating an event source mapping (DynamoDB stream, Kinesis, SQS), the attacker executes code with elevated privileges without needing to invoke the function directly.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - LAMBDA-002 - iam:PassRole + lambda:CreateFunction + lambda:CreateEventSourceMapping", + link="https://pathfinding.cloud/paths/lambda-002", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find lambda:CreateFunction permission + MATCH (principal)--(create_policy:AWSPolicy)--(stmt_create:AWSPolicyStatement) + WHERE stmt_create.effect = 'Allow' + AND any(action IN stmt_create.action WHERE + toLower(action) = 'lambda:createfunction' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find lambda:CreateEventSourceMapping permission + MATCH (principal)--(event_policy:AWSPolicy)--(stmt_event:AWSPolicyStatement) + WHERE stmt_event.effect = 'Allow' + AND any(action IN stmt_event.action WHERE + toLower(action) = 'lambda:createeventsourcemapping' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find roles that trust Lambda service (can be passed to Lambda) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'lambda.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# LAMBDA-003 +AWS_LAMBDA_PRIVESC_UPDATE_FUNCTION_CODE = AttackPathsQueryDefinition( + id="aws-lambda-privesc-update-function-code", + name="Lambda Function Code Injection (LAMBDA-003)", + short_description="Modify the code of an existing Lambda function to execute arbitrary commands with the function's execution role permissions.", + description="Detect principals who can update the code of existing Lambda functions. By replacing a Lambda function's code with malicious code, the attacker executes arbitrary commands with the privileges of the function's execution role when it is next invoked, either manually or via automatic triggers.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - LAMBDA-003 - lambda:UpdateFunctionCode", + link="https://pathfinding.cloud/paths/lambda-003", + ), + provider="aws", + cypher=f""" + // Find principals with lambda:UpdateFunctionCode permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'lambda:updatefunctioncode' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find existing Lambda functions with execution roles + MATCH path_target = (aws)-[:RESOURCE]->(lambda_fn:AWSLambda)-[:STS_ASSUMEROLE_ALLOW]->(target_role:AWSRole) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR lambda_fn.arn CONTAINS resource + OR resource CONTAINS lambda_fn.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# LAMBDA-004 +AWS_LAMBDA_PRIVESC_UPDATE_FUNCTION_CODE_INVOKE = AttackPathsQueryDefinition( + id="aws-lambda-privesc-update-function-code-invoke", + name="Lambda Function Code Injection with Direct Invocation (LAMBDA-004)", + short_description="Modify the code of an existing Lambda function and invoke it directly to execute arbitrary commands with the function's execution role permissions.", + description="Detect principals who can update the code of existing Lambda functions and invoke them. By replacing a Lambda function's code with malicious code and invoking it directly, the attacker executes arbitrary commands with the privileges of the function's execution role immediately, without waiting for automatic triggers.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - LAMBDA-004 - lambda:UpdateFunctionCode + lambda:InvokeFunction", + link="https://pathfinding.cloud/paths/lambda-004", + ), + provider="aws", + cypher=f""" + // Find principals with lambda:UpdateFunctionCode permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'lambda:updatefunctioncode' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find lambda:InvokeFunction permission + MATCH (principal)--(policy2:AWSPolicy)--(stmt2:AWSPolicyStatement) + WHERE stmt2.effect = 'Allow' + AND any(action IN stmt2.action WHERE + toLower(action) = 'lambda:invokefunction' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find existing Lambda functions with execution roles + MATCH path_target = (aws)-[:RESOURCE]->(lambda_fn:AWSLambda)-[:STS_ASSUMEROLE_ALLOW]->(target_role:AWSRole) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR lambda_fn.arn CONTAINS resource + OR resource CONTAINS lambda_fn.name + ) + AND any(resource IN stmt2.resource WHERE + resource = '*' + OR lambda_fn.arn CONTAINS resource + OR resource CONTAINS lambda_fn.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# LAMBDA-005 +AWS_LAMBDA_PRIVESC_UPDATE_FUNCTION_CODE_ADD_PERMISSION = AttackPathsQueryDefinition( + id="aws-lambda-privesc-update-function-code-add-permission", + name="Lambda Function Code Injection with Resource Policy Grant (LAMBDA-005)", + short_description="Modify the code of an existing Lambda function and grant yourself invocation permission via its resource-based policy to execute code with the function's execution role.", + description="Detect principals who can update the code of existing Lambda functions and add permissions to their resource-based policies. By replacing a Lambda function's code and granting themselves invoke access through the resource-based policy, the attacker executes malicious code with the function's execution role without needing lambda:InvokeFunction as an IAM permission.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - LAMBDA-005 - lambda:UpdateFunctionCode + lambda:AddPermission", + link="https://pathfinding.cloud/paths/lambda-005", + ), + provider="aws", + cypher=f""" + // Find principals with lambda:UpdateFunctionCode permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'lambda:updatefunctioncode' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find lambda:AddPermission permission + MATCH (principal)--(policy2:AWSPolicy)--(stmt2:AWSPolicyStatement) + WHERE stmt2.effect = 'Allow' + AND any(action IN stmt2.action WHERE + toLower(action) = 'lambda:addpermission' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find existing Lambda functions with execution roles + MATCH path_target = (aws)-[:RESOURCE]->(lambda_fn:AWSLambda)-[:STS_ASSUMEROLE_ALLOW]->(target_role:AWSRole) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR lambda_fn.arn CONTAINS resource + OR resource CONTAINS lambda_fn.name + ) + AND any(resource IN stmt2.resource WHERE + resource = '*' + OR lambda_fn.arn CONTAINS resource + OR resource CONTAINS lambda_fn.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# LAMBDA-006 +AWS_LAMBDA_PRIVESC_PASSROLE_CREATE_FUNCTION_ADD_PERMISSION = AttackPathsQueryDefinition( + id="aws-lambda-privesc-passrole-create-function-add-permission", + name="Lambda Function Creation with Resource Policy Invocation (LAMBDA-006)", + short_description="Create a Lambda function with a privileged IAM role and grant yourself invocation permission via its resource-based policy to execute code with the role's permissions.", + description="Detect principals who can create Lambda functions with privileged IAM roles and add permissions to their resource-based policies. By passing a privileged role to a new Lambda function and granting themselves invoke access through the resource-based policy, the attacker executes malicious code with elevated privileges without needing lambda:InvokeFunction as an IAM permission.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - LAMBDA-006 - iam:PassRole + lambda:CreateFunction + lambda:AddPermission", + link="https://pathfinding.cloud/paths/lambda-006", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find lambda:CreateFunction permission + MATCH (principal)--(create_policy:AWSPolicy)--(stmt_create:AWSPolicyStatement) + WHERE stmt_create.effect = 'Allow' + AND any(action IN stmt_create.action WHERE + toLower(action) = 'lambda:createfunction' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find lambda:AddPermission permission + MATCH (principal)--(perm_policy:AWSPolicy)--(stmt_perm:AWSPolicyStatement) + WHERE stmt_perm.effect = 'Allow' + AND any(action IN stmt_perm.action WHERE + toLower(action) = 'lambda:addpermission' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find roles that trust Lambda service (can be passed to Lambda) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'lambda.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# SAGEMAKER-001 +AWS_SAGEMAKER_PRIVESC_PASSROLE_CREATE_NOTEBOOK = AttackPathsQueryDefinition( + id="aws-sagemaker-privesc-passrole-create-notebook", + name="SageMaker Notebook Creation with Privileged Role (SAGEMAKER-001)", + short_description="Create a SageMaker notebook instance with a privileged IAM role to execute arbitrary code with the role's permissions via the Jupyter environment.", + description="Detect principals who can create SageMaker notebook instances with privileged IAM roles. By passing a privileged role to a new notebook instance, the attacker gains shell access through the Jupyter environment and can execute arbitrary commands with the role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - SAGEMAKER-001 - iam:PassRole + sagemaker:CreateNotebookInstance", + link="https://pathfinding.cloud/paths/sagemaker-001", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find sagemaker:CreateNotebookInstance permission + MATCH (principal)--(sm_policy:AWSPolicy)--(stmt_sm:AWSPolicyStatement) + WHERE stmt_sm.effect = 'Allow' + AND any(action IN stmt_sm.action WHERE + toLower(action) = 'sagemaker:createnotebookinstance' + OR toLower(action) = 'sagemaker:*' + OR action = '*' + ) + + // Find roles that trust SageMaker service (can be passed to SageMaker) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'sagemaker.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# SAGEMAKER-002 +AWS_SAGEMAKER_PRIVESC_PASSROLE_CREATE_TRAINING_JOB = AttackPathsQueryDefinition( + id="aws-sagemaker-privesc-passrole-create-training-job", + name="SageMaker Training Job Creation with Privileged Role (SAGEMAKER-002)", + short_description="Create a SageMaker training job with a privileged IAM role to execute arbitrary container code with the role's permissions.", + description="Detect principals who can create SageMaker training jobs with privileged IAM roles. By passing a privileged role to a new training job with a malicious training script or container, the attacker executes code with elevated privileges and can exfiltrate credentials or modify AWS resources.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - SAGEMAKER-002 - iam:PassRole + sagemaker:CreateTrainingJob", + link="https://pathfinding.cloud/paths/sagemaker-002", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find sagemaker:CreateTrainingJob permission + MATCH (principal)--(sm_policy:AWSPolicy)--(stmt_sm:AWSPolicyStatement) + WHERE stmt_sm.effect = 'Allow' + AND any(action IN stmt_sm.action WHERE + toLower(action) = 'sagemaker:createtrainingjob' + OR toLower(action) = 'sagemaker:*' + OR action = '*' + ) + + // Find roles that trust SageMaker service (can be passed to SageMaker) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'sagemaker.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# SAGEMAKER-003 +AWS_SAGEMAKER_PRIVESC_PASSROLE_CREATE_PROCESSING_JOB = AttackPathsQueryDefinition( + id="aws-sagemaker-privesc-passrole-create-processing-job", + name="SageMaker Processing Job Creation with Privileged Role (SAGEMAKER-003)", + short_description="Create a SageMaker processing job with a privileged IAM role to execute arbitrary container code with the role's permissions.", + description="Detect principals who can create SageMaker processing jobs with privileged IAM roles. By passing a privileged role to a new processing job with a malicious script or container, the attacker executes code with elevated privileges and can exfiltrate credentials or modify AWS resources.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - SAGEMAKER-003 - iam:PassRole + sagemaker:CreateProcessingJob", + link="https://pathfinding.cloud/paths/sagemaker-003", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find sagemaker:CreateProcessingJob permission + MATCH (principal)--(sm_policy:AWSPolicy)--(stmt_sm:AWSPolicyStatement) + WHERE stmt_sm.effect = 'Allow' + AND any(action IN stmt_sm.action WHERE + toLower(action) = 'sagemaker:createprocessingjob' + OR toLower(action) = 'sagemaker:*' + OR action = '*' + ) + + // Find roles that trust SageMaker service (can be passed to SageMaker) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'sagemaker.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# SAGEMAKER-004 +AWS_SAGEMAKER_PRIVESC_PRESIGNED_NOTEBOOK_URL = AttackPathsQueryDefinition( + id="aws-sagemaker-privesc-presigned-notebook-url", + name="SageMaker Presigned Notebook URL for Privilege Escalation (SAGEMAKER-004)", + short_description="Generate a presigned URL to access an existing SageMaker notebook instance and execute code with its execution role's permissions.", + description="Detect principals who can generate presigned URLs to access existing SageMaker notebook instances. By accessing the Jupyter environment via a presigned URL, the attacker can execute arbitrary code with the permissions of the notebook's execution role without creating any new resources.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - SAGEMAKER-004 - sagemaker:CreatePresignedNotebookInstanceUrl", + link="https://pathfinding.cloud/paths/sagemaker-004", + ), + provider="aws", + cypher=f""" + // Find principals with sagemaker:CreatePresignedNotebookInstanceUrl permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'sagemaker:createpresignednotebookinstanceurl' + OR toLower(action) = 'sagemaker:*' + OR action = '*' + ) + + // Find existing SageMaker notebook instances with execution roles + MATCH path_target = (aws)-[:RESOURCE]->(notebook:AWSSageMakerNotebookInstance)-[:HAS_EXECUTION_ROLE]->(target_role:AWSRole) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR notebook.arn CONTAINS resource + OR resource CONTAINS notebook.notebook_instance_name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# SAGEMAKER-005 +AWS_SAGEMAKER_PRIVESC_LIFECYCLE_CONFIG_NOTEBOOK = AttackPathsQueryDefinition( + id="aws-sagemaker-privesc-lifecycle-config-notebook", + name="SageMaker Notebook Lifecycle Config Injection (SAGEMAKER-005)", + short_description="Inject a malicious lifecycle configuration into an existing SageMaker notebook to execute code with the notebook's execution role during startup.", + description="Detect principals who can inject malicious lifecycle configurations into existing SageMaker notebook instances. By stopping a notebook, attaching a malicious lifecycle config, and restarting it, the attacker executes arbitrary code with the notebook's execution role permissions during startup.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - SAGEMAKER-005 - sagemaker:CreateNotebookInstanceLifecycleConfig + sagemaker:StopNotebookInstance + sagemaker:UpdateNotebookInstance + sagemaker:StartNotebookInstance", + link="https://pathfinding.cloud/paths/sagemaker-005", + ), + provider="aws", + cypher=f""" + // Find principals with sagemaker:CreateNotebookInstanceLifecycleConfig permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'sagemaker:createnotebookinstancelifecycleconfig' + OR toLower(action) = 'sagemaker:*' + OR action = '*' + ) + + // Find sagemaker:UpdateNotebookInstance permission + MATCH (principal)--(policy2:AWSPolicy)--(stmt2:AWSPolicyStatement) + WHERE stmt2.effect = 'Allow' + AND any(action IN stmt2.action WHERE + toLower(action) = 'sagemaker:updatenotebookinstance' + OR toLower(action) = 'sagemaker:*' + OR action = '*' + ) + + // Find sagemaker:StopNotebookInstance permission + MATCH (principal)--(policy3:AWSPolicy)--(stmt3:AWSPolicyStatement) + WHERE stmt3.effect = 'Allow' + AND any(action IN stmt3.action WHERE + toLower(action) = 'sagemaker:stopnotebookinstance' + OR toLower(action) = 'sagemaker:*' + OR action = '*' + ) + + // Find sagemaker:StartNotebookInstance permission + MATCH (principal)--(policy4:AWSPolicy)--(stmt4:AWSPolicyStatement) + WHERE stmt4.effect = 'Allow' + AND any(action IN stmt4.action WHERE + toLower(action) = 'sagemaker:startnotebookinstance' + OR toLower(action) = 'sagemaker:*' + OR action = '*' + ) + + // Find existing SageMaker notebook instances with execution roles + MATCH path_target = (aws)-[:RESOURCE]->(notebook:AWSSageMakerNotebookInstance)-[:HAS_EXECUTION_ROLE]->(target_role:AWSRole) + WHERE any(resource IN stmt2.resource WHERE + resource = '*' + OR notebook.arn CONTAINS resource + OR resource CONTAINS notebook.notebook_instance_name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# SSM-001 +AWS_SSM_PRIVESC_START_SESSION = AttackPathsQueryDefinition( + id="aws-ssm-privesc-start-session", + name="SSM Session Access for EC2 Role Credentials (SSM-001)", + short_description="Start an SSM session on an EC2 instance to access its attached role credentials through IMDS.", + description="Detect principals who can start SSM sessions on EC2 instances. This allows establishing a shell session on a running EC2 instance and retrieving the attached IAM role's temporary credentials from the Instance Metadata Service (IMDS), gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - SSM-001 - ssm:StartSession", + link="https://pathfinding.cloud/paths/ssm-001", + ), + provider="aws", + cypher=f""" + // Find principals with ssm:StartSession permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'ssm:startsession' + OR toLower(action) = 'ssm:*' + OR action = '*' + ) + + // Find EC2 instances with attached roles (targets for credential theft via IMDS) + MATCH path_target = (aws)--(ec2:EC2Instance)-[:STS_ASSUMEROLE_ALLOW]->(target_role:AWSRole) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# SSM-002 +AWS_SSM_PRIVESC_SEND_COMMAND = AttackPathsQueryDefinition( + id="aws-ssm-privesc-send-command", + name="SSM Send Command for EC2 Role Credentials (SSM-002)", + short_description="Execute commands on an EC2 instance via SSM Run Command to access its attached role credentials through IMDS.", + description="Detect principals who can send SSM commands to EC2 instances. This allows executing arbitrary commands on a running EC2 instance and retrieving the attached IAM role's temporary credentials from the Instance Metadata Service (IMDS), gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - SSM-002 - ssm:SendCommand", + link="https://pathfinding.cloud/paths/ssm-002", + ), + provider="aws", + cypher=f""" + // Find principals with ssm:SendCommand permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'ssm:sendcommand' + OR toLower(action) = 'ssm:*' + OR action = '*' + ) + + // Find EC2 instances with attached roles (targets for credential theft via IMDS) + MATCH path_target = (aws)--(ec2:EC2Instance)-[:STS_ASSUMEROLE_ALLOW]->(target_role:AWSRole) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# STS-001 +AWS_STS_PRIVESC_ASSUME_ROLE = AttackPathsQueryDefinition( + id="aws-sts-privesc-assume-role", + name="Role Assumption for Privilege Escalation (STS-001)", + short_description="Assume IAM roles with elevated permissions by exploiting bidirectional trust between the starting principal and the target role.", + description="Detect principals who can assume other IAM roles via sts:AssumeRole. When a principal has sts:AssumeRole permission and the target role's trust policy allows the principal to assume it (bidirectional trust), the attacker gains all permissions of the target role. This enables privilege escalation when the target role has higher privileges than the starting principal.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - STS-001 - sts:AssumeRole", + link="https://pathfinding.cloud/paths/sts-001", + ), + provider="aws", + cypher=f""" + // Find principals with sts:AssumeRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE toLower(action) = 'sts:assumerole' OR toLower(action) = 'sts:*' OR action = '*' ) - // Find target roles that the principal can both modify AND assume - MATCH path_target = (aws)--(target_role:AWSRole) - WHERE target_role.arn CONTAINS $provider_uid - // Can attach policy to this role - AND any(resource IN stmt_attach.resource WHERE - resource = '*' - OR target_role.arn CONTAINS resource - OR resource CONTAINS target_role.name - ) - // Can assume this role - AND any(resource IN stmt_assume.resource WHERE - resource = '*' - OR target_role.arn CONTAINS resource - OR resource CONTAINS target_role.name - ) - - // Deduplicate before creating virtual relationships - WITH DISTINCT admin_outcome, aws, principal, target_role - - // Create virtual relationships showing the attack path - CALL apoc.create.vRelationship(principal, 'CAN_MODIFY', {{ - via: 'iam:AttachRolePolicy' - }}, target_role) - YIELD rel AS modify_rel - - CALL apoc.create.vRelationship(target_role, 'LEADS_TO', {{ - technique: 'iam:AttachRolePolicy + sts:AssumeRole', - via: 'sts:AssumeRole', - reference: 'https://pathfinding.cloud/paths/iam-014' - }}, admin_outcome) - YIELD rel AS escalation_rel - - // Re-match paths for visualization - MATCH path_principal = (aws)--(principal) - MATCH path_target = (aws)--(target_role) + // Find target roles the principal can assume (bidirectional trust via Cartography) + MATCH path_target = (aws)--(target_role:AWSRole)<-[:STS_ASSUMEROLE_ALLOW]-(principal) + WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) UNWIND nodes(path_principal) + nodes(path_target) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path_principal, path_target, - admin_outcome, modify_rel, escalation_rel, - collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, parameters=[], ) -# TODO: Add ProwlerFinding nodes -AWS_BEDROCK_PRIVESC_PASSROLE_CODE_INTERPRETER = AttackPathsQueryDefinition( - id="aws-bedrock-privesc-passrole-code-interpreter", - name="Privilege Escalation: Bedrock Code Interpreter with PassRole", - description="Detect principals that can escalate privileges by passing a role to a Bedrock AgentCore Code Interpreter. The attacker creates a code interpreter with an arbitrary role, then invokes it to execute code with those credentials.", - provider="aws", - cypher=""" - CALL apoc.create.vNode(['PrivilegeEscalation'], { - id: 'effective-administrator-bedrock', - check_title: 'Privilege Escalation', - name: 'Effective Administrator (Bedrock)', - status: 'FAIL', - severity: 'critical' - }) - YIELD node AS escalation_outcome - - WITH escalation_outcome - - // Find principals in the account - MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal) - - // Principal can assume roles (up to 2 hops) - OPTIONAL MATCH path_assume = (principal)-[:STS_ASSUMEROLE_ALLOW*0..2]->(acting_as:AWSRole) - WITH escalation_outcome, aws, principal, path_principal, path_assume, - CASE WHEN path_assume IS NULL THEN principal ELSE acting_as END AS effective_principal - - // Find iam:PassRole permission - MATCH path_passrole = (effective_principal)--(passrole_policy:AWSPolicy)--(passrole_stmt:AWSPolicyStatement) - WHERE passrole_stmt.effect = 'Allow' - AND any(action IN passrole_stmt.action WHERE toLower(action) = 'iam:passrole' OR action = '*') - - // Find Bedrock AgentCore permissions - MATCH (effective_principal)--(bedrock_policy:AWSPolicy)--(bedrock_stmt:AWSPolicyStatement) - WHERE bedrock_stmt.effect = 'Allow' - AND ( - any(action IN bedrock_stmt.action WHERE toLower(action) = 'bedrock-agentcore:createcodeinterpreter' OR action = '*' OR toLower(action) = 'bedrock-agentcore:*') - ) - AND ( - any(action IN bedrock_stmt.action WHERE toLower(action) = 'bedrock-agentcore:startsession' OR action = '*' OR toLower(action) = 'bedrock-agentcore:*') - ) - AND ( - any(action IN bedrock_stmt.action WHERE toLower(action) = 'bedrock-agentcore:invoke' OR action = '*' OR toLower(action) = 'bedrock-agentcore:*') - ) - - // Find target roles with elevated permissions that could be passed - MATCH (aws)--(target_role:AWSRole)--(target_policy:AWSPolicy)--(target_stmt:AWSPolicyStatement) - WHERE target_stmt.effect = 'Allow' - AND ( - any(action IN target_stmt.action WHERE action = '*') - OR any(action IN target_stmt.action WHERE toLower(action) = 'iam:*') - ) - - // Deduplicate per (principal, target_role) pair - WITH DISTINCT escalation_outcome, aws, principal, target_role - - // Group by principal, collect target_roles - WITH escalation_outcome, aws, principal, - collect(DISTINCT target_role) AS target_roles, - count(DISTINCT target_role) AS target_count - - // Create single virtual Bedrock node per principal - CALL apoc.create.vNode(['BedrockCodeInterpreter'], { - name: 'New Code Interpreter', - description: toString(target_count) + ' admin role(s) can be passed', - id: principal.arn, - target_role_count: target_count - }) - YIELD node AS bedrock_agent - - // Connect from principal (not effective_principal) to keep graph connected - CALL apoc.create.vRelationship(principal, 'CREATES_INTERPRETER', { - permissions: ['iam:PassRole', 'bedrock-agentcore:CreateCodeInterpreter', 'bedrock-agentcore:StartSession', 'bedrock-agentcore:Invoke'], - technique: 'new-passrole' - }, bedrock_agent) - YIELD rel AS create_rel - - // UNWIND target_roles to show which roles can be passed - UNWIND target_roles AS target_role - - CALL apoc.create.vRelationship(bedrock_agent, 'PASSES_ROLE', {}, target_role) - YIELD rel AS pass_rel - - CALL apoc.create.vRelationship(target_role, 'GRANTS_ACCESS', { - reference: 'https://pathfinding.cloud/paths/bedrock-001' - }, escalation_outcome) - YIELD rel AS grants_rel - - // Re-match path for visualization - MATCH path_principal = (aws)--(principal) - - RETURN path_principal, - bedrock_agent, target_role, escalation_outcome, create_rel, pass_rel, grants_rel, target_count - """, - parameters=[], -) - - # AWS Queries List # ---------------- @@ -688,8 +3390,70 @@ AWS_QUERIES: list[AttackPathsQueryDefinition] = [ AWS_CLASSIC_ELB_INTERNET_EXPOSED, AWS_ELBV2_INTERNET_EXPOSED, AWS_PUBLIC_IP_RESOURCE_LOOKUP, - AWS_IAM_PRIVESC_PASSROLE_EC2, - AWS_GLUE_PRIVESC_PASSROLE_DEV_ENDPOINT, - AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY_ASSUME_ROLE, + AWS_APPRUNNER_PRIVESC_PASSROLE_CREATE_SERVICE, + AWS_APPRUNNER_PRIVESC_UPDATE_SERVICE, AWS_BEDROCK_PRIVESC_PASSROLE_CODE_INTERPRETER, + AWS_BEDROCK_PRIVESC_INVOKE_CODE_INTERPRETER, + AWS_CLOUDFORMATION_PRIVESC_PASSROLE_CREATE_STACK, + AWS_CLOUDFORMATION_PRIVESC_UPDATE_STACK, + AWS_CLOUDFORMATION_PRIVESC_PASSROLE_CREATE_STACKSET, + AWS_CLOUDFORMATION_PRIVESC_PASSROLE_UPDATE_STACKSET, + AWS_CLOUDFORMATION_PRIVESC_CHANGESET, + AWS_CODEBUILD_PRIVESC_PASSROLE_CREATE_PROJECT, + AWS_CODEBUILD_PRIVESC_START_BUILD, + AWS_CODEBUILD_PRIVESC_START_BUILD_BATCH, + AWS_CODEBUILD_PRIVESC_PASSROLE_CREATE_PROJECT_BATCH, + AWS_DATAPIPELINE_PRIVESC_PASSROLE_CREATE_PIPELINE, + AWS_EC2_PRIVESC_PASSROLE_IAM, + AWS_EC2_PRIVESC_MODIFY_INSTANCE_ATTRIBUTE, + AWS_EC2_PRIVESC_PASSROLE_SPOT_INSTANCES, + AWS_EC2_PRIVESC_LAUNCH_TEMPLATE, + AWS_EC2INSTANCECONNECT_PRIVESC_SEND_SSH_PUBLIC_KEY, + AWS_ECS_PRIVESC_PASSROLE_CREATE_SERVICE, + AWS_ECS_PRIVESC_PASSROLE_RUN_TASK, + AWS_ECS_PRIVESC_PASSROLE_CREATE_SERVICE_EXISTING_CLUSTER, + AWS_ECS_PRIVESC_PASSROLE_RUN_TASK_EXISTING_CLUSTER, + AWS_ECS_PRIVESC_PASSROLE_START_TASK_EXISTING_CLUSTER, + AWS_ECS_PRIVESC_EXECUTE_COMMAND, + AWS_GLUE_PRIVESC_PASSROLE_DEV_ENDPOINT, + AWS_GLUE_PRIVESC_UPDATE_DEV_ENDPOINT, + AWS_GLUE_PRIVESC_PASSROLE_CREATE_JOB, + AWS_GLUE_PRIVESC_PASSROLE_CREATE_JOB_TRIGGER, + AWS_GLUE_PRIVESC_PASSROLE_UPDATE_JOB, + AWS_GLUE_PRIVESC_PASSROLE_UPDATE_JOB_TRIGGER, + AWS_IAM_PRIVESC_CREATE_POLICY_VERSION, + AWS_IAM_PRIVESC_CREATE_ACCESS_KEY, + AWS_IAM_PRIVESC_DELETE_CREATE_ACCESS_KEY, + AWS_IAM_PRIVESC_CREATE_LOGIN_PROFILE, + AWS_IAM_PRIVESC_PUT_ROLE_POLICY, + AWS_IAM_PRIVESC_UPDATE_LOGIN_PROFILE, + AWS_IAM_PRIVESC_PUT_USER_POLICY, + AWS_IAM_PRIVESC_ATTACH_USER_POLICY, + AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY, + AWS_IAM_PRIVESC_ATTACH_GROUP_POLICY, + AWS_IAM_PRIVESC_PUT_GROUP_POLICY, + AWS_IAM_PRIVESC_UPDATE_ASSUME_ROLE_POLICY, + AWS_IAM_PRIVESC_ADD_USER_TO_GROUP, + AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY_ASSUME_ROLE, + AWS_IAM_PRIVESC_ATTACH_USER_POLICY_CREATE_ACCESS_KEY, + AWS_IAM_PRIVESC_CREATE_POLICY_VERSION_ASSUME_ROLE, + AWS_IAM_PRIVESC_PUT_ROLE_POLICY_ASSUME_ROLE, + AWS_IAM_PRIVESC_PUT_USER_POLICY_CREATE_ACCESS_KEY, + AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY_UPDATE_ASSUME_ROLE, + AWS_IAM_PRIVESC_CREATE_POLICY_VERSION_UPDATE_ASSUME_ROLE, + AWS_IAM_PRIVESC_PUT_ROLE_POLICY_UPDATE_ASSUME_ROLE, + AWS_LAMBDA_PRIVESC_PASSROLE_CREATE_FUNCTION, + AWS_LAMBDA_PRIVESC_PASSROLE_CREATE_FUNCTION_EVENT_SOURCE, + AWS_LAMBDA_PRIVESC_UPDATE_FUNCTION_CODE, + AWS_LAMBDA_PRIVESC_UPDATE_FUNCTION_CODE_INVOKE, + AWS_LAMBDA_PRIVESC_UPDATE_FUNCTION_CODE_ADD_PERMISSION, + AWS_LAMBDA_PRIVESC_PASSROLE_CREATE_FUNCTION_ADD_PERMISSION, + AWS_SAGEMAKER_PRIVESC_PASSROLE_CREATE_NOTEBOOK, + AWS_SAGEMAKER_PRIVESC_PASSROLE_CREATE_TRAINING_JOB, + AWS_SAGEMAKER_PRIVESC_PASSROLE_CREATE_PROCESSING_JOB, + AWS_SAGEMAKER_PRIVESC_PRESIGNED_NOTEBOOK_URL, + AWS_SAGEMAKER_PRIVESC_LIFECYCLE_CONFIG_NOTEBOOK, + AWS_SSM_PRIVESC_START_SESSION, + AWS_SSM_PRIVESC_SEND_COMMAND, + AWS_STS_PRIVESC_ASSUME_ROLE, ] diff --git a/api/src/backend/api/attack_paths/queries/types.py b/api/src/backend/api/attack_paths/queries/types.py index d798dbbcdb..3a70805cd7 100644 --- a/api/src/backend/api/attack_paths/queries/types.py +++ b/api/src/backend/api/attack_paths/queries/types.py @@ -1,6 +1,14 @@ from dataclasses import dataclass, field +@dataclass +class AttackPathsQueryAttribution: + """Source attribution for an Attack Path query.""" + + text: str + link: str + + @dataclass class AttackPathsQueryParameterDefinition: """ @@ -23,7 +31,9 @@ class AttackPathsQueryDefinition: id: str name: str + short_description: str description: str provider: str cypher: str + attribution: AttackPathsQueryAttribution | None = None parameters: list[AttackPathsQueryParameterDefinition] = field(default_factory=list) diff --git a/api/src/backend/api/attack_paths/views_helpers.py b/api/src/backend/api/attack_paths/views_helpers.py index cb14c7f44b..b3e860a4b8 100644 --- a/api/src/backend/api/attack_paths/views_helpers.py +++ b/api/src/backend/api/attack_paths/views_helpers.py @@ -5,7 +5,6 @@ from typing import Any, Iterable from rest_framework.exceptions import APIException, ValidationError from api.attack_paths import database as graph_database, AttackPathsQueryDefinition -from api.models import AttackPathsScan from config.custom_logging import BackendLogger from tasks.jobs.attack_paths.config import INTERNAL_LABELS @@ -80,12 +79,12 @@ def prepare_query_parameters( def execute_attack_paths_query( - attack_paths_scan: AttackPathsScan, + database_name: str, definition: AttackPathsQueryDefinition, parameters: dict[str, Any], ) -> dict[str, Any]: try: - with graph_database.get_session(attack_paths_scan.graph_database) as session: + with graph_database.get_session(database_name) as session: result = session.run(definition.cypher, parameters) return _serialize_graph(result.graph()) diff --git a/api/src/backend/api/fixtures/dev/8_dev_attack_paths_scans.json b/api/src/backend/api/fixtures/dev/8_dev_attack_paths_scans.json index fdf310458a..4f0c6e94ed 100644 --- a/api/src/backend/api/fixtures/dev/8_dev_attack_paths_scans.json +++ b/api/src/backend/api/fixtures/dev/8_dev_attack_paths_scans.json @@ -7,10 +7,9 @@ "provider": "b85601a8-4b45-4194-8135-03fb980ef428", "scan": "01920573-aa9c-73c9-bcda-f2e35c9b19d2", "state": "completed", + "graph_data_ready": true, "progress": 100, "update_tag": 1693586667, - "graph_database": "db-a7f0f6de-6f8e-4b3a-8cbe-3f6dd9012345", - "is_graph_database_deleted": false, "task": null, "inserted_at": "2024-09-01T17:24:37Z", "updated_at": "2024-09-01T17:44:37Z", @@ -30,8 +29,6 @@ "state": "executing", "progress": 48, "update_tag": 1697625000, - "graph_database": "db-4a2fb2af-8a60-4d7d-9cae-4ca65e098765", - "is_graph_database_deleted": false, "task": null, "inserted_at": "2024-10-18T10:55:57Z", "updated_at": "2024-10-18T10:56:15Z", diff --git a/api/src/backend/api/migrations/0076_openstack_provider.py b/api/src/backend/api/migrations/0076_openstack_provider.py new file mode 100644 index 0000000000..9cc80707ea --- /dev/null +++ b/api/src/backend/api/migrations/0076_openstack_provider.py @@ -0,0 +1,39 @@ +# Generated by Django migration for OpenStack provider support + +from django.db import migrations + +import api.db_utils + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0075_cloudflare_provider"), + ] + + operations = [ + migrations.AlterField( + model_name="provider", + name="provider", + field=api.db_utils.ProviderEnumField( + choices=[ + ("aws", "AWS"), + ("azure", "Azure"), + ("gcp", "GCP"), + ("kubernetes", "Kubernetes"), + ("m365", "M365"), + ("github", "GitHub"), + ("mongodbatlas", "MongoDB Atlas"), + ("iac", "IaC"), + ("oraclecloud", "Oracle Cloud Infrastructure"), + ("alibabacloud", "Alibaba Cloud"), + ("cloudflare", "Cloudflare"), + ("openstack", "OpenStack"), + ], + default="aws", + ), + ), + migrations.RunSQL( + "ALTER TYPE provider ADD VALUE IF NOT EXISTS 'openstack';", + reverse_sql=migrations.RunSQL.noop, + ), + ] diff --git a/api/src/backend/api/migrations/0077_remove_attackpathsscan_graph_database_indexes.py b/api/src/backend/api/migrations/0077_remove_attackpathsscan_graph_database_indexes.py new file mode 100644 index 0000000000..0498b66e92 --- /dev/null +++ b/api/src/backend/api/migrations/0077_remove_attackpathsscan_graph_database_indexes.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.15 on 2026-02-16 09:24 + +from django.contrib.postgres.operations import RemoveIndexConcurrently +from django.db import migrations + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("api", "0076_openstack_provider"), + ] + + operations = [ + RemoveIndexConcurrently( + model_name="attackpathsscan", + name="aps_active_graph_idx", + ), + RemoveIndexConcurrently( + model_name="attackpathsscan", + name="aps_completed_graph_idx", + ), + ] diff --git a/api/src/backend/api/migrations/0078_remove_attackpathsscan_graph_database_fields.py b/api/src/backend/api/migrations/0078_remove_attackpathsscan_graph_database_fields.py new file mode 100644 index 0000000000..89c9558817 --- /dev/null +++ b/api/src/backend/api/migrations/0078_remove_attackpathsscan_graph_database_fields.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.15 on 2026-02-16 09:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0077_remove_attackpathsscan_graph_database_indexes"), + ] + + operations = [ + migrations.RemoveField( + model_name="attackpathsscan", + name="graph_database", + ), + migrations.RemoveField( + model_name="attackpathsscan", + name="is_graph_database_deleted", + ), + ] diff --git a/api/src/backend/api/migrations/0079_attackpathsscan_graph_data_ready.py b/api/src/backend/api/migrations/0079_attackpathsscan_graph_data_ready.py new file mode 100644 index 0000000000..1c9429c046 --- /dev/null +++ b/api/src/backend/api/migrations/0079_attackpathsscan_graph_data_ready.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.15 on 2026-02-16 13:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0078_remove_attackpathsscan_graph_database_fields"), + ] + + operations = [ + migrations.AddField( + model_name="attackpathsscan", + name="graph_data_ready", + field=models.BooleanField(default=False), + ), + ] diff --git a/api/src/backend/api/migrations/0080_backfill_attack_paths_graph_data_ready.py b/api/src/backend/api/migrations/0080_backfill_attack_paths_graph_data_ready.py new file mode 100644 index 0000000000..f780059bd2 --- /dev/null +++ b/api/src/backend/api/migrations/0080_backfill_attack_paths_graph_data_ready.py @@ -0,0 +1,26 @@ +# Separate from 0079 because psqlextra's schema editor runs AddField DDL and DML +# on different database connections, causing a deadlock when combined with RunPython +# in the same migration. + +from django.db import migrations + +from api.db_router import MainRouter + + +def backfill_graph_data_ready(apps, schema_editor): + """Set graph_data_ready=True for all completed AttackPathsScan rows.""" + AttackPathsScan = apps.get_model("api", "AttackPathsScan") + AttackPathsScan.objects.using(MainRouter.admin_db).filter( + state="completed", + graph_data_ready=False, + ).update(graph_data_ready=True) + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0079_attackpathsscan_graph_data_ready"), + ] + + operations = [ + migrations.RunPython(backfill_graph_data_ready, migrations.RunPython.noop), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 4bda51761c..5597963216 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -288,6 +288,7 @@ class Provider(RowLevelSecurityProtectedModel): ORACLECLOUD = "oraclecloud", _("Oracle Cloud Infrastructure") ALIBABACLOUD = "alibabacloud", _("Alibaba Cloud") CLOUDFLARE = "cloudflare", _("Cloudflare") + OPENSTACK = "openstack", _("OpenStack") @staticmethod def validate_aws_uid(value): @@ -326,10 +327,13 @@ class Provider(RowLevelSecurityProtectedModel): @staticmethod def validate_gcp_uid(value): - if not re.match(r"^[a-z][a-z0-9-]{5,29}$", value): + # Standard format: 6-30 chars, starts with letter, lowercase + digits + hyphens + # Legacy App Engine format: domain.com:project-id + if not re.match(r"^([a-z][a-z0-9.-]*:)?[a-z][a-z0-9-]{5,29}$", value): raise ModelValidationError( - detail="GCP provider ID must be 6 to 30 characters, start with a letter, and contain only lowercase " - "letters, numbers, and hyphens.", + detail="GCP provider ID must be a valid project ID: 6 to 30 characters, start with a letter, " + "and contain only lowercase letters, numbers, and hyphens. " + "Legacy App Engine project IDs with a domain prefix (e.g., example.com:my-project) are also accepted.", code="gcp-uid", pointer="/data/attributes/uid", ) @@ -410,6 +414,15 @@ class Provider(RowLevelSecurityProtectedModel): pointer="/data/attributes/uid", ) + @staticmethod + def validate_openstack_uid(value): + if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$", value): + raise ModelValidationError( + detail="OpenStack provider ID must be a valid project ID (UUID or project name).", + code="openstack-uid", + pointer="/data/attributes/uid", + ) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) inserted_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) @@ -645,6 +658,7 @@ class AttackPathsScan(RowLevelSecurityProtectedModel): state = StateEnumField(choices=StateChoices.choices, default=StateChoices.AVAILABLE) progress = models.IntegerField(default=0) + graph_data_ready = models.BooleanField(default=False) # Timing started_at = models.DateTimeField(null=True, blank=True) @@ -681,8 +695,6 @@ class AttackPathsScan(RowLevelSecurityProtectedModel): update_tag = models.BigIntegerField( null=True, blank=True, help_text="Cartography update tag (epoch)" ) - graph_database = models.CharField(max_length=63, null=True, blank=True) - is_graph_database_deleted = models.BooleanField(default=False) ingestion_exceptions = models.JSONField(default=dict, null=True, blank=True) class Meta(RowLevelSecurityProtectedModel.Meta): @@ -709,21 +721,6 @@ class AttackPathsScan(RowLevelSecurityProtectedModel): fields=["tenant_id", "scan_id"], name="aps_scan_lookup_idx", ), - models.Index( - fields=["tenant_id", "provider_id"], - name="aps_active_graph_idx", - include=["graph_database", "id"], - condition=Q(is_graph_database_deleted=False), - ), - models.Index( - fields=["tenant_id", "provider_id", "-completed_at"], - name="aps_completed_graph_idx", - include=["graph_database", "id"], - condition=Q( - state=StateChoices.COMPLETED, - is_graph_database_deleted=False, - ), - ), ] class JSONAPIMeta: diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 432528c562..0a2b4327ca 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -296,6 +296,7 @@ paths: enum: - state - progress + - graph_data_ready - provider - provider_alias - provider_type @@ -355,7 +356,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -367,6 +368,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -380,13 +382,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -398,6 +401,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -413,6 +417,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -567,6 +572,7 @@ paths: enum: - state - progress + - graph_data_ready - provider - provider_alias - provider_type @@ -616,7 +622,7 @@ paths: operationId: attack_paths_scans_queries_retrieve description: Retrieve the catalog of Attack Paths queries available for this Attack Paths scan. - summary: List attack paths queries + summary: List Attack Paths queries parameters: - in: query name: fields[attack-paths-scans] @@ -627,6 +633,7 @@ paths: enum: - state - progress + - graph_data_ready - provider - provider_alias - provider_type @@ -714,7 +721,7 @@ paths: description: Bad request (e.g., Unknown Attack Paths query for the selected provider) '404': - description: No attack paths found for the given query and parameters + description: No Attack Paths found for the given query and parameters '500': description: Attack Paths query execution failed due to a database error /api/v1/compliance-overviews: @@ -1336,7 +1343,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -1348,6 +1355,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -1361,13 +1369,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -1379,6 +1388,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -1394,6 +1404,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -1926,7 +1937,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -1938,6 +1949,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -1951,13 +1963,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -1969,6 +1982,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -1984,6 +1998,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -2424,7 +2439,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -2436,6 +2451,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -2449,13 +2465,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -2467,6 +2484,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -2482,6 +2500,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -2920,7 +2939,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -2932,6 +2951,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -2945,13 +2965,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -2963,6 +2984,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -2978,6 +3000,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -3404,7 +3427,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -3416,6 +3439,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -3429,13 +3453,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -3447,6 +3472,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -3462,6 +3488,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -5229,7 +5256,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -5241,6 +5268,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -5254,13 +5282,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -5272,6 +5301,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -5287,6 +5317,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - name: filter[search] @@ -5392,7 +5423,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -5404,6 +5435,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -5417,13 +5449,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -5435,6 +5468,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -5450,6 +5484,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - name: filter[search] @@ -5556,6 +5591,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -5569,6 +5605,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: @@ -5586,6 +5623,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -5601,6 +5639,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - name: filter[search] @@ -5727,7 +5766,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -5739,6 +5778,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -5752,13 +5792,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -5770,6 +5811,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -5785,6 +5827,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -5924,7 +5967,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -5936,6 +5979,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -5949,13 +5993,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -5967,6 +6012,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -5982,6 +6028,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -6127,6 +6174,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -6140,6 +6188,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: @@ -6157,6 +6206,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -6172,6 +6222,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - name: filter[search] @@ -6347,7 +6398,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -6359,6 +6410,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -6372,13 +6424,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -6390,6 +6443,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -6405,6 +6459,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -6509,7 +6564,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -6521,6 +6576,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -6534,13 +6590,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -6552,6 +6609,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -6567,6 +6625,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -6695,7 +6754,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -6707,6 +6766,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -6720,13 +6780,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -6738,6 +6799,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -6753,6 +6815,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -7522,7 +7585,7 @@ paths: name: filter[provider] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -7534,6 +7597,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -7547,13 +7611,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -7565,6 +7630,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -7580,13 +7646,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -7598,6 +7665,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -7611,13 +7679,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -7629,6 +7698,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -7644,6 +7714,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - name: filter[search] @@ -8273,7 +8344,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -8285,6 +8356,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -8298,13 +8370,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -8316,6 +8389,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -8331,6 +8405,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -8775,7 +8850,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -8787,6 +8862,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -8800,13 +8876,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -8818,6 +8895,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -8833,6 +8911,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -9090,7 +9169,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -9102,6 +9181,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -9115,13 +9195,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -9133,6 +9214,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -9148,6 +9230,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -9411,7 +9494,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -9423,6 +9506,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -9436,13 +9520,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -9454,6 +9539,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -9469,6 +9555,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -10266,7 +10353,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -10278,6 +10365,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- * `aws` - AWS @@ -10291,13 +10379,14 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 enum: - alibabacloud - aws @@ -10309,6 +10398,7 @@ paths: - kubernetes - m365 - mongodbatlas + - openstack - oraclecloud description: |- Multiple values may be separated by commas. @@ -10324,6 +10414,7 @@ paths: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack explode: false style: form - in: query @@ -10659,6 +10750,72 @@ paths: description: CSV file containing the compliance report '404': description: Compliance report not found + /api/v1/scans/{id}/csa: + get: + operationId: scans_csa_retrieve + description: Download CSA Cloud Controls Matrix (CCM) v4.0 compliance report + as a PDF file. + summary: Retrieve CSA CCM compliance report + parameters: + - in: query + name: fields[scans] + schema: + type: array + items: + type: string + enum: + - name + - trigger + - state + - unique_resource_count + - progress + - duration + - provider + - task + - inserted_at + - started_at + - completed_at + - scheduled_at + - next_scan_at + - processor + - url + description: endpoint return only specific fields in the response on a per-type + basis by including a fields[TYPE] query parameter. + explode: false + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this scan. + required: true + - in: query + name: include + schema: + type: array + items: + type: string + enum: + - provider + description: include query parameter to allow the client to customize which + related resources should be returned. + explode: false + tags: + - Scan + security: + - JWT or API Key: [] + responses: + '200': + description: PDF file containing the CSA CCM compliance report + '202': + description: The task is in progress + '401': + description: API key missing or user not Authenticated + '403': + description: There is a problem with credentials + '404': + description: The scan has no CSA CCM reports, or the CSA CCM report generation + task has not started yet /api/v1/scans/{id}/ens: get: operationId: scans_ens_retrieve @@ -12438,8 +12595,14 @@ components: type: string name: type: string + short_description: + type: string description: type: string + attribution: + allOf: + - $ref: '#/components/schemas/AttackPathsQueryAttribution' + nullable: true provider: type: string parameters: @@ -12449,9 +12612,35 @@ components: required: - id - name + - short_description - description - provider - parameters + AttackPathsQueryAttribution: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + type: string + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + enum: + - attack-paths-query-attributions + id: {} + attributes: + type: object + properties: + text: + type: string + link: + type: string + required: + - text + - link AttackPathsQueryParameter: type: object required: @@ -12617,6 +12806,8 @@ components: type: integer maximum: 2147483647 minimum: -2147483648 + graph_data_ready: + type: boolean provider_alias: type: string readOnly: true @@ -17316,6 +17507,20 @@ components: required: - api_key - api_email + - type: object + title: OpenStack clouds.yaml Credentials + properties: + clouds_yaml_content: + type: string + description: The full content of a clouds.yaml configuration + file. + clouds_yaml_cloud: + type: string + description: The name of the cloud to use from the clouds.yaml + file. + required: + - clouds_yaml_content + - clouds_yaml_cloud writeOnly: true required: - secret @@ -18315,6 +18520,7 @@ components: - oraclecloud - alibabacloud - cloudflare + - openstack type: string description: |- * `aws` - AWS @@ -18328,7 +18534,8 @@ components: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare - x-spec-enum-id: 2d8d323e9cc0044b + * `openstack` - OpenStack + x-spec-enum-id: 4b8815b179aa7216 uid: type: string title: Unique identifier for the provider, set by the provider @@ -18445,8 +18652,9 @@ components: - oraclecloud - alibabacloud - cloudflare + - openstack type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 description: |- Type of provider to create. @@ -18461,6 +18669,7 @@ components: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack uid: type: string title: Unique identifier for the provider, set by the provider @@ -18509,8 +18718,9 @@ components: - oraclecloud - alibabacloud - cloudflare + - openstack type: string - x-spec-enum-id: 2d8d323e9cc0044b + x-spec-enum-id: 4b8815b179aa7216 description: |- Type of provider to create. @@ -18525,6 +18735,7 @@ components: * `oraclecloud` - Oracle Cloud Infrastructure * `alibabacloud` - Alibaba Cloud * `cloudflare` - Cloudflare + * `openstack` - OpenStack uid: type: string minLength: 3 @@ -19346,6 +19557,19 @@ components: required: - api_key - api_email + - type: object + title: OpenStack clouds.yaml Credentials + properties: + clouds_yaml_content: + type: string + description: The full content of a clouds.yaml configuration file. + clouds_yaml_cloud: + type: string + description: The name of the cloud to use from the clouds.yaml + file. + required: + - clouds_yaml_content + - clouds_yaml_cloud writeOnly: true required: - secret_type @@ -19732,6 +19956,20 @@ components: required: - api_key - api_email + - type: object + title: OpenStack clouds.yaml Credentials + properties: + clouds_yaml_content: + type: string + description: The full content of a clouds.yaml configuration + file. + clouds_yaml_cloud: + type: string + description: The name of the cloud to use from the clouds.yaml + file. + required: + - clouds_yaml_content + - clouds_yaml_cloud writeOnly: true required: - secret_type @@ -20130,6 +20368,19 @@ components: required: - api_key - api_email + - type: object + title: OpenStack clouds.yaml Credentials + properties: + clouds_yaml_content: + type: string + description: The full content of a clouds.yaml configuration file. + clouds_yaml_cloud: + type: string + description: The name of the cloud to use from the clouds.yaml + file. + required: + - clouds_yaml_content + - clouds_yaml_cloud writeOnly: true required: - secret diff --git a/api/src/backend/api/tests/test_attack_paths.py b/api/src/backend/api/tests/test_attack_paths.py index 2c4e1484f8..1b6e765164 100644 --- a/api/src/backend/api/tests/test_attack_paths.py +++ b/api/src/backend/api/tests/test_attack_paths.py @@ -83,12 +83,12 @@ def test_execute_attack_paths_query_serializes_graph( definition = attack_paths_query_definition_factory( id="aws-rds", name="RDS", + short_description="Short desc", description="", cypher="MATCH (n) RETURN n", parameters=[], ) parameters = {"provider_uid": "123"} - attack_paths_scan = SimpleNamespace(graph_database="tenant-db") node = attack_paths_graph_stub_classes.Node( element_id="node-1", @@ -122,15 +122,17 @@ def test_execute_attack_paths_query_serializes_graph( session_ctx.__enter__.return_value = session session_ctx.__exit__.return_value = False + database_name = "db-tenant-test-tenant-id" + with patch( "api.attack_paths.views_helpers.graph_database.get_session", return_value=session_ctx, ) as mock_get_session: result = views_helpers.execute_attack_paths_query( - attack_paths_scan, definition, parameters + database_name, definition, parameters ) - mock_get_session.assert_called_once_with("tenant-db") + mock_get_session.assert_called_once_with(database_name) session.run.assert_called_once_with(definition.cypher, parameters) assert result["nodes"][0]["id"] == "node-1" assert result["nodes"][0]["properties"]["complex"]["items"][0] == "value" @@ -143,11 +145,12 @@ def test_execute_attack_paths_query_wraps_graph_errors( definition = attack_paths_query_definition_factory( id="aws-rds", name="RDS", + short_description="Short desc", description="", cypher="MATCH (n) RETURN n", parameters=[], ) - attack_paths_scan = SimpleNamespace(graph_database="tenant-db") + database_name = "db-tenant-test-tenant-id" parameters = {"provider_uid": "123"} class ExplodingContext: @@ -166,7 +169,7 @@ def test_execute_attack_paths_query_wraps_graph_errors( ): with pytest.raises(APIException): views_helpers.execute_attack_paths_query( - attack_paths_scan, definition, parameters + database_name, definition, parameters ) mock_logger.error.assert_called_once() diff --git a/api/src/backend/api/tests/test_utils.py b/api/src/backend/api/tests/test_utils.py index 56f98a5401..236cbf87a2 100644 --- a/api/src/backend/api/tests/test_utils.py +++ b/api/src/backend/api/tests/test_utils.py @@ -27,6 +27,7 @@ from prowler.providers.iac.iac_provider import IacProvider from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider from prowler.providers.m365.m365_provider import M365Provider from prowler.providers.mongodbatlas.mongodbatlas_provider import MongodbatlasProvider +from prowler.providers.openstack.openstack_provider import OpenstackProvider from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider @@ -120,6 +121,7 @@ class TestReturnProwlerProvider: (Provider.ProviderChoices.IAC.value, IacProvider), (Provider.ProviderChoices.ALIBABACLOUD.value, AlibabacloudProvider), (Provider.ProviderChoices.CLOUDFLARE.value, CloudflareProvider), + (Provider.ProviderChoices.OPENSTACK.value, OpenstackProvider), ], ) def test_return_prowler_provider(self, provider_type, expected_provider): @@ -227,6 +229,10 @@ class TestGetProwlerProviderKwargs: Provider.ProviderChoices.CLOUDFLARE.value, {"filter_accounts": ["provider_uid"]}, ), + ( + Provider.ProviderChoices.OPENSTACK.value, + {}, + ), ], ) def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs): diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 4bb7509bc7..c7f2e5533d 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -1079,6 +1079,11 @@ class TestProviderViewSet: [ {"provider": "aws", "uid": "111111111111", "alias": "test"}, {"provider": "gcp", "uid": "a12322-test54321", "alias": "test"}, + { + "provider": "gcp", + "uid": "example.com:my-project-123456", + "alias": "legacy-gcp", + }, { "provider": "kubernetes", "uid": "kubernetes-test-123456789", @@ -1179,6 +1184,11 @@ class TestProviderViewSet: "uid": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "alias": "Cloudflare Account", }, + { + "provider": "openstack", + "uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "alias": "OpenStack Project", + }, ] ), ) @@ -1198,6 +1208,11 @@ class TestProviderViewSet: [ {"provider": "aws", "uid": "111111111111", "alias": "test"}, {"provider": "gcp", "uid": "a12322-test54321", "alias": "test"}, + { + "provider": "gcp", + "uid": "example.com:my-project-123456", + "alias": "legacy-gcp", + }, { "provider": "kubernetes", "uid": "kubernetes-test-123456789", @@ -1598,6 +1613,26 @@ class TestProviderViewSet: "cloudflare-uid", "uid", ), + # OpenStack UID validation - starts with special character + ( + { + "provider": "openstack", + "uid": "-invalid-project", + "alias": "test", + }, + "openstack-uid", + "uid", + ), + # OpenStack UID validation - too short (below min_length) + ( + { + "provider": "openstack", + "uid": "ab", + "alias": "test", + }, + "min_length", + "uid", + ), ] ), ) @@ -1771,21 +1806,21 @@ class TestProviderViewSet: ( "uid.icontains", "1", - 9, + 10, ), ("alias", "aws_testing_1", 1), ("alias.icontains", "aws", 2), - ("inserted_at", TODAY, 10), + ("inserted_at", TODAY, 11), ( "inserted_at.gte", "2024-01-01", - 10, + 11, ), ("inserted_at.lte", "2024-01-01", 0), ( "updated_at.gte", "2024-01-01", - 10, + 11, ), ("updated_at.lte", "2024-01-01", 0), ] @@ -2392,6 +2427,15 @@ class TestProviderSecretViewSet: "api_email": "user@example.com", }, ), + # OpenStack with clouds.yaml content + ( + Provider.ProviderChoices.OPENSTACK.value, + ProviderSecret.TypeChoices.STATIC, + { + "clouds_yaml_content": "clouds:\n mycloud:\n auth:\n auth_url: https://openstack.example.com:5000/v3\n", + "clouds_yaml_cloud": "mycloud", + }, + ), ], ) def test_provider_secrets_create_valid( @@ -3830,6 +3874,7 @@ class TestAttackPathsScanViewSet: AttackPathsQueryDefinition( id="aws-rds", name="RDS inventory", + short_description="List account RDS assets.", description="List account RDS assets", provider=provider.provider, cypher="MATCH (n) RETURN n", @@ -3887,11 +3932,12 @@ class TestAttackPathsScanViewSet: attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], - graph_database="tenant-db", + graph_data_ready=True, ) query_definition = AttackPathsQueryDefinition( id="aws-rds", name="RDS inventory", + short_description="List account RDS assets.", description="List account RDS assets", provider=provider.provider, cypher="MATCH (n) RETURN n", @@ -3917,10 +3963,16 @@ class TestAttackPathsScanViewSet: ], } + expected_db_name = f"db-tenant-{attack_paths_scan.provider.tenant_id}" + with ( patch( "api.v1.views.get_query_by_id", return_value=query_definition ) as mock_get_query, + patch( + "api.v1.views.graph_database.get_database_name", + return_value=expected_db_name, + ) as mock_get_db_name, patch( "api.v1.views.attack_paths_views_helpers.prepare_query_parameters", return_value=prepared_parameters, @@ -3942,23 +3994,24 @@ class TestAttackPathsScanViewSet: assert response.status_code == status.HTTP_200_OK mock_get_query.assert_called_once_with("aws-rds") + mock_get_db_name.assert_called_once_with(attack_paths_scan.provider.tenant_id) mock_prepare.assert_called_once_with( query_definition, {}, attack_paths_scan.provider.uid, ) mock_execute.assert_called_once_with( - attack_paths_scan, + expected_db_name, query_definition, prepared_parameters, ) - mock_clear_cache.assert_called_once_with(attack_paths_scan.graph_database) + mock_clear_cache.assert_called_once_with(expected_db_name) result = response.json()["data"] attributes = result["attributes"] assert attributes["nodes"] == graph_payload["nodes"] assert attributes["relationships"] == graph_payload["relationships"] - def test_run_attack_paths_query_requires_completed_scan( + def test_run_attack_paths_query_blocks_when_graph_data_not_ready( self, authenticated_client, providers_fixture, @@ -3970,6 +4023,7 @@ class TestAttackPathsScanViewSet: provider, scan=scans_fixture[0], state=StateChoices.EXECUTING, + graph_data_ready=False, ) response = authenticated_client.post( @@ -3981,9 +4035,9 @@ class TestAttackPathsScanViewSet: ) assert response.status_code == status.HTTP_400_BAD_REQUEST - assert "must be completed" in response.json()["errors"][0]["detail"] + assert "not available" in response.json()["errors"][0]["detail"] - def test_run_attack_paths_query_requires_graph_database( + def test_run_attack_paths_query_allows_executing_scan_when_graph_data_ready( self, authenticated_client, providers_fixture, @@ -3994,19 +4048,100 @@ class TestAttackPathsScanViewSet: attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], - graph_database=None, + state=StateChoices.EXECUTING, + graph_data_ready=True, + ) + query_definition = AttackPathsQueryDefinition( + id="aws-test", + name="Test", + short_description="Test query.", + description="Test query", + provider=provider.provider, + cypher="MATCH (n) RETURN n", + parameters=[], ) - response = authenticated_client.post( - reverse( - "attack-paths-scans-queries-run", kwargs={"pk": attack_paths_scan.id} + with ( + patch("api.v1.views.get_query_by_id", return_value=query_definition), + patch( + "api.v1.views.attack_paths_views_helpers.prepare_query_parameters", + return_value={"provider_uid": provider.uid}, ), - data=self._run_payload(), - content_type=API_JSON_CONTENT_TYPE, + patch( + "api.v1.views.attack_paths_views_helpers.execute_attack_paths_query", + return_value={ + "nodes": [{"id": "n1", "labels": ["AWSAccount"], "properties": {}}], + "relationships": [], + }, + ), + patch("api.v1.views.graph_database.clear_cache"), + patch( + "api.v1.views.graph_database.get_database_name", return_value="db-test" + ), + ): + response = authenticated_client.post( + reverse( + "attack-paths-scans-queries-run", + kwargs={"pk": attack_paths_scan.id}, + ), + data=self._run_payload("aws-test"), + content_type=API_JSON_CONTENT_TYPE, + ) + + assert response.status_code == status.HTTP_200_OK + + def test_run_attack_paths_query_allows_failed_scan_when_graph_data_ready( + self, + authenticated_client, + providers_fixture, + scans_fixture, + create_attack_paths_scan, + ): + provider = providers_fixture[0] + attack_paths_scan = create_attack_paths_scan( + provider, + scan=scans_fixture[0], + state=StateChoices.FAILED, + graph_data_ready=True, + ) + query_definition = AttackPathsQueryDefinition( + id="aws-test", + name="Test", + short_description="Test query.", + description="Test query", + provider=provider.provider, + cypher="MATCH (n) RETURN n", + parameters=[], ) - assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR - assert "does not reference a graph database" in str(response.json()) + with ( + patch("api.v1.views.get_query_by_id", return_value=query_definition), + patch( + "api.v1.views.attack_paths_views_helpers.prepare_query_parameters", + return_value={"provider_uid": provider.uid}, + ), + patch( + "api.v1.views.attack_paths_views_helpers.execute_attack_paths_query", + return_value={ + "nodes": [{"id": "n1", "labels": ["AWSAccount"], "properties": {}}], + "relationships": [], + }, + ), + patch("api.v1.views.graph_database.clear_cache"), + patch( + "api.v1.views.graph_database.get_database_name", return_value="db-test" + ), + ): + response = authenticated_client.post( + reverse( + "attack-paths-scans-queries-run", + kwargs={"pk": attack_paths_scan.id}, + ), + data=self._run_payload("aws-test"), + content_type=API_JSON_CONTENT_TYPE, + ) + + assert response.status_code == status.HTTP_200_OK def test_run_attack_paths_query_unknown_query( self, @@ -4019,6 +4154,7 @@ class TestAttackPathsScanViewSet: attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], + graph_data_ready=True, ) with patch("api.v1.views.get_query_by_id", return_value=None): @@ -4045,10 +4181,12 @@ class TestAttackPathsScanViewSet: attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], + graph_data_ready=True, ) query_definition = AttackPathsQueryDefinition( id="aws-empty", name="empty", + short_description="", description="", provider=provider.provider, cypher="MATCH (n) RETURN n", @@ -10841,25 +10979,20 @@ class TestTenantFinishACSView: assert "sso_saml_failed=true" in response.url def test_dispatch_skips_role_mapping_when_single_manage_account_user( - self, create_test_user, tenants_fixture, saml_setup, settings, monkeypatch + self, + create_test_user, + tenants_fixture, + admin_role_fixture, + saml_setup, + settings, + monkeypatch, ): """Test that role mapping is skipped when tenant has only one user with MANAGE_ACCOUNT role""" monkeypatch.setenv("SAML_SSO_CALLBACK_URL", "http://localhost/sso-complete") user = create_test_user tenant = tenants_fixture[0] - # Create a single role with manage_account=True for the user - admin_role = Role.objects.using(MainRouter.admin_db).create( - name="admin", - tenant=tenant, - manage_account=True, - manage_users=True, - manage_billing=True, - manage_providers=True, - manage_integrations=True, - manage_scans=True, - unlimited_visibility=True, - ) + admin_role = admin_role_fixture UserRoleRelationship.objects.using(MainRouter.admin_db).create( user=user, role=admin_role, tenant_id=tenant.id ) @@ -10930,35 +11063,26 @@ class TestTenantFinishACSView: .exists() ) - def test_dispatch_applies_role_mapping_when_multiple_manage_account_users( - self, create_test_user, tenants_fixture, saml_setup, settings, monkeypatch + def test_dispatch_skips_role_mapping_when_last_manage_account_user_maps_to_existing_role( + self, + create_test_user, + tenants_fixture, + admin_role_fixture, + roles_fixture, + saml_setup, + settings, + monkeypatch, ): - """Test that role mapping is applied when tenant has multiple users with MANAGE_ACCOUNT role""" + """Test that role mapping is skipped when it would remove the last MANAGE_ACCOUNT user""" monkeypatch.setenv("SAML_SSO_CALLBACK_URL", "http://localhost/sso-complete") user = create_test_user tenant = tenants_fixture[0] - # Create a second user with manage_account=True - second_admin = User.objects.using(MainRouter.admin_db).create( - email="admin2@prowler.com", name="Second Admin" - ) - admin_role = Role.objects.using(MainRouter.admin_db).create( - name="admin", - tenant=tenant, - manage_account=True, - manage_users=True, - manage_billing=True, - manage_providers=True, - manage_integrations=True, - manage_scans=True, - unlimited_visibility=True, - ) + admin_role = admin_role_fixture + viewer_role = roles_fixture[3] UserRoleRelationship.objects.using(MainRouter.admin_db).create( user=user, role=admin_role, tenant_id=tenant.id ) - UserRoleRelationship.objects.using(MainRouter.admin_db).create( - user=second_admin, role=admin_role, tenant_id=tenant.id - ) social_account = SocialAccount( user=user, @@ -10967,7 +11091,7 @@ class TestTenantFinishACSView: "firstName": ["John"], "lastName": ["Doe"], "organization": ["testing_company"], - "userType": ["viewer"], # This SHOULD be applied + "userType": [viewer_role.name], }, ) @@ -11005,10 +11129,91 @@ class TestTenantFinishACSView: assert response.status_code == 302 - # Verify the viewer role was created and assigned (role mapping was applied) - viewer_role = Role.objects.using(MainRouter.admin_db).get( - name="viewer", tenant=tenant + assert ( + UserRoleRelationship.objects.using(MainRouter.admin_db) + .filter(user=user, role=admin_role, tenant_id=tenant.id) + .exists() ) + assert not ( + UserRoleRelationship.objects.using(MainRouter.admin_db) + .filter(user=user, role=viewer_role, tenant_id=tenant.id) + .exists() + ) + + def test_dispatch_applies_role_mapping_when_multiple_manage_account_users( + self, + create_test_user, + tenants_fixture, + admin_role_fixture, + roles_fixture, + saml_setup, + settings, + monkeypatch, + ): + """Test that role mapping is applied when tenant has multiple users with MANAGE_ACCOUNT role""" + monkeypatch.setenv("SAML_SSO_CALLBACK_URL", "http://localhost/sso-complete") + user = create_test_user + tenant = tenants_fixture[0] + + # Create a second user with manage_account=True + second_admin = User.objects.using(MainRouter.admin_db).create( + email="admin2@prowler.com", name="Second Admin" + ) + admin_role = admin_role_fixture + viewer_role = roles_fixture[3] + UserRoleRelationship.objects.using(MainRouter.admin_db).create( + user=user, role=admin_role, tenant_id=tenant.id + ) + UserRoleRelationship.objects.using(MainRouter.admin_db).create( + user=second_admin, role=admin_role, tenant_id=tenant.id + ) + + social_account = SocialAccount( + user=user, + provider="saml", + extra_data={ + "firstName": ["John"], + "lastName": ["Doe"], + "organization": ["testing_company"], + "userType": [viewer_role.name], # This SHOULD be applied + }, + ) + + request = RequestFactory().get( + reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"}) + ) + request.user = user + request.session = {} + + with ( + patch( + "allauth.socialaccount.providers.saml.views.get_app_or_404" + ) as mock_get_app_or_404, + patch( + "allauth.socialaccount.models.SocialApp.objects.get" + ) as mock_socialapp_get, + patch( + "allauth.socialaccount.models.SocialAccount.objects.get" + ) as mock_sa_get, + patch("api.models.SAMLDomainIndex.objects.get") as mock_saml_domain_get, + patch("api.models.SAMLConfiguration.objects.get") as mock_saml_config_get, + patch("api.models.User.objects.get") as mock_user_get, + ): + mock_get_app_or_404.return_value = MagicMock( + provider="saml", client_id="testtenant", name="Test App", settings={} + ) + mock_sa_get.return_value = social_account + mock_socialapp_get.return_value = MagicMock(provider_id="saml") + mock_saml_domain_get.return_value = SimpleNamespace(tenant_id=tenant.id) + mock_saml_config_get.return_value = MagicMock() + mock_user_get.return_value = user + + view = TenantFinishACSView.as_view() + response = view(request, organization_slug="testtenant") + + assert response.status_code == 302 + + # Verify the viewer role was assigned (role mapping was applied) assert ( UserRoleRelationship.objects.using(MainRouter.admin_db) .filter(user=user, role=viewer_role, tenant_id=tenant.id) @@ -11022,6 +11227,86 @@ class TestTenantFinishACSView: .exists() ) + def test_dispatch_applies_role_mapping_for_non_admin_user_with_single_admin( + self, + create_test_user, + tenants_fixture, + admin_role_fixture, + roles_fixture, + saml_setup, + settings, + monkeypatch, + ): + """Test that role mapping is applied for a non-admin user when a single admin exists""" + monkeypatch.setenv("SAML_SSO_CALLBACK_URL", "http://localhost/sso-complete") + admin_user = create_test_user + tenant = tenants_fixture[0] + non_admin_user = User.objects.using(MainRouter.admin_db).create( + email="viewer@prowler.com", name="Viewer" + ) + + admin_role = admin_role_fixture + viewer_role = roles_fixture[3] + UserRoleRelationship.objects.using(MainRouter.admin_db).create( + user=admin_user, role=admin_role, tenant_id=tenant.id + ) + + social_account = SocialAccount( + user=non_admin_user, + provider="saml", + extra_data={ + "firstName": ["Jane"], + "lastName": ["Doe"], + "organization": ["testing_company"], + "userType": [viewer_role.name], + }, + ) + + request = RequestFactory().get( + reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"}) + ) + request.user = non_admin_user + request.session = {} + + with ( + patch( + "allauth.socialaccount.providers.saml.views.get_app_or_404" + ) as mock_get_app_or_404, + patch( + "allauth.socialaccount.models.SocialApp.objects.get" + ) as mock_socialapp_get, + patch( + "allauth.socialaccount.models.SocialAccount.objects.get" + ) as mock_sa_get, + patch("api.models.SAMLDomainIndex.objects.get") as mock_saml_domain_get, + patch("api.models.SAMLConfiguration.objects.get") as mock_saml_config_get, + patch("api.models.User.objects.get") as mock_user_get, + ): + mock_get_app_or_404.return_value = MagicMock( + provider="saml", client_id="testtenant", name="Test App", settings={} + ) + mock_sa_get.return_value = social_account + mock_socialapp_get.return_value = MagicMock(provider_id="saml") + mock_saml_domain_get.return_value = SimpleNamespace(tenant_id=tenant.id) + mock_saml_config_get.return_value = MagicMock() + mock_user_get.return_value = non_admin_user + + view = TenantFinishACSView.as_view() + response = view(request, organization_slug="testtenant") + + assert response.status_code == 302 + + assert ( + UserRoleRelationship.objects.using(MainRouter.admin_db) + .filter(user=non_admin_user, role=viewer_role, tenant_id=tenant.id) + .exists() + ) + assert ( + UserRoleRelationship.objects.using(MainRouter.admin_db) + .filter(user=admin_user, role=admin_role, tenant_id=tenant.id) + .exists() + ) + @pytest.mark.django_db class TestLighthouseConfigViewSet: diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index 343537efa9..355c3b6e03 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -33,6 +33,7 @@ if TYPE_CHECKING: from prowler.providers.mongodbatlas.mongodbatlas_provider import ( MongodbatlasProvider, ) + from prowler.providers.openstack.openstack_provider import OpenstackProvider from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider @@ -78,12 +79,14 @@ def return_prowler_provider( AlibabacloudProvider | AwsProvider | AzureProvider + | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider + | OpenstackProvider | OraclecloudProvider ): """Return the Prowler provider class based on the given provider type. @@ -92,7 +95,7 @@ def return_prowler_provider( provider (Provider): The provider object containing the provider type and associated secrets. Returns: - AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OraclecloudProvider: The corresponding provider class. + AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider: The corresponding provider class. Raises: ValueError: If the provider type specified in `provider.provider` is not supported. @@ -152,6 +155,10 @@ def return_prowler_provider( ) prowler_provider = CloudflareProvider + case Provider.ProviderChoices.OPENSTACK.value: + from prowler.providers.openstack.openstack_provider import OpenstackProvider + + prowler_provider = OpenstackProvider case _: raise ValueError(f"Provider type {provider.provider} not supported") return prowler_provider @@ -208,6 +215,12 @@ def get_prowler_provider_kwargs( **prowler_provider_kwargs, "filter_accounts": [provider.uid], } + elif provider.provider == Provider.ProviderChoices.OPENSTACK.value: + # No extra kwargs needed: clouds_yaml_content and clouds_yaml_cloud from the + # secret are sufficient. Validating project_id (provider.uid) against the + # clouds.yaml is not feasible because not all auth methods include it and the + # Keystone API is unavailable on public clouds. + pass if mutelist_processor: mutelist_content = mutelist_processor.configuration.get("Mutelist", {}) @@ -232,6 +245,7 @@ def initialize_prowler_provider( | KubernetesProvider | M365Provider | MongodbatlasProvider + | OpenstackProvider | OraclecloudProvider ): """Initialize a Prowler provider instance based on the given provider type. @@ -241,7 +255,7 @@ def initialize_prowler_provider( mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration. Returns: - AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OraclecloudProvider: An instance of the corresponding provider class + AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider: An instance of the corresponding provider class initialized with the provider's secrets. """ prowler_provider = return_prowler_provider(provider) @@ -276,6 +290,13 @@ def prowler_provider_connection_test(provider: Provider) -> Connection: if "access_token" in prowler_provider_kwargs: iac_test_kwargs["access_token"] = prowler_provider_kwargs["access_token"] return prowler_provider.test_connection(**iac_test_kwargs) + elif provider.provider == Provider.ProviderChoices.OPENSTACK.value: + openstack_kwargs = { + "clouds_yaml_content": prowler_provider_kwargs["clouds_yaml_content"], + "clouds_yaml_cloud": prowler_provider_kwargs["clouds_yaml_cloud"], + "raise_on_exception": False, + } + return prowler_provider.test_connection(**openstack_kwargs) else: return prowler_provider.test_connection( **prowler_provider_kwargs, diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index 7b34b06d2b..83ea942793 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -373,6 +373,21 @@ from rest_framework_json_api import serializers }, "required": ["api_key", "api_email"], }, + { + "type": "object", + "title": "OpenStack clouds.yaml Credentials", + "properties": { + "clouds_yaml_content": { + "type": "string", + "description": "The full content of a clouds.yaml configuration file.", + }, + "clouds_yaml_cloud": { + "type": "string", + "description": "The name of the cloud to use from the clouds.yaml file.", + }, + }, + "required": ["clouds_yaml_content", "clouds_yaml_cloud"], + }, ] } ) diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index e2ae7fc8d2..8cb7ef49cb 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1145,6 +1145,7 @@ class AttackPathsScanSerializer(RLSSerializer): "id", "state", "progress", + "graph_data_ready", "provider", "provider_alias", "provider_type", @@ -1176,6 +1177,14 @@ class AttackPathsScanSerializer(RLSSerializer): return provider.uid if provider else None +class AttackPathsQueryAttributionSerializer(BaseSerializerV1): + text = serializers.CharField() + link = serializers.CharField() + + class JSONAPIMeta: + resource_name = "attack-paths-query-attributions" + + class AttackPathsQueryParameterSerializer(BaseSerializerV1): name = serializers.CharField() label = serializers.CharField() @@ -1190,7 +1199,9 @@ class AttackPathsQueryParameterSerializer(BaseSerializerV1): class AttackPathsQuerySerializer(BaseSerializerV1): id = serializers.CharField() name = serializers.CharField() + short_description = serializers.CharField() description = serializers.CharField() + attribution = AttackPathsQueryAttributionSerializer(allow_null=True, required=False) provider = serializers.CharField() parameters = AttackPathsQueryParameterSerializer(many=True) @@ -1515,6 +1526,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer): "or both 'api_key' and 'api_email'." } ) + elif provider_type == Provider.ProviderChoices.OPENSTACK.value: + serializer = OpenStackCloudsYamlProviderSecret(data=secret) else: raise serializers.ValidationError( {"provider": f"Provider type not supported {provider_type}"} @@ -1681,6 +1694,14 @@ class CloudflareApiKeyProviderSecret(serializers.Serializer): resource_name = "provider-secrets" +class OpenStackCloudsYamlProviderSecret(serializers.Serializer): + clouds_yaml_content = serializers.CharField() + clouds_yaml_cloud = serializers.CharField() + + class Meta: + resource_name = "provider-secrets" + + class AlibabaCloudProviderSecret(serializers.Serializer): access_key_id = serializers.CharField() access_key_secret = serializers.CharField() diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 7ddcfe2461..365b9c45e0 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -763,27 +763,40 @@ class TenantFinishACSView(FinishACSView): .tenant ) - # Check if tenant has only one user with MANAGE_ACCOUNT role - users_with_manage_account = ( + role_name = ( + extra.get("userType", ["no_permissions"])[0].strip() + if extra.get("userType") + else "no_permissions" + ) + role = ( + Role.objects.using(MainRouter.admin_db) + .filter(name=role_name, tenant=tenant) + .first() + ) + + # Only skip mapping if it would remove the last MANAGE_ACCOUNT user + remaining_manage_account_users = ( UserRoleRelationship.objects.using(MainRouter.admin_db) .filter(role__manage_account=True, tenant_id=tenant.id) + .exclude(user_id=user_id) .values("user") .distinct() .count() ) + user_has_manage_account = ( + UserRoleRelationship.objects.using(MainRouter.admin_db) + .filter(role__manage_account=True, tenant_id=tenant.id, user_id=user_id) + .exists() + ) + role_manage_account = role.manage_account if role else False + would_remove_last_manage_account = ( + user_has_manage_account + and remaining_manage_account_users == 0 + and not role_manage_account + ) - # Only apply role mapping from userType if tenant does NOT have exactly one user with MANAGE_ACCOUNT - if users_with_manage_account != 1: - role_name = ( - extra.get("userType", ["no_permissions"])[0].strip() - if extra.get("userType") - else "no_permissions" - ) - try: - role = Role.objects.using(MainRouter.admin_db).get( - name=role_name, tenant=tenant - ) - except Role.DoesNotExist: + if not would_remove_last_manage_account: + if role is None: role = Role.objects.using(MainRouter.admin_db).create( name=role_name, tenant=tenant, @@ -1746,6 +1759,25 @@ class ProviderViewSet(DisablePaginationMixin, BaseRLSViewSet): ), }, ), + csa=extend_schema( + tags=["Scan"], + summary="Retrieve CSA CCM compliance report", + description="Download CSA Cloud Controls Matrix (CCM) v4.0 compliance report as a PDF file.", + request=None, + responses={ + 200: OpenApiResponse( + description="PDF file containing the CSA CCM compliance report" + ), + 202: OpenApiResponse(description="The task is in progress"), + 401: OpenApiResponse( + description="API key missing or user not Authenticated" + ), + 403: OpenApiResponse(description="There is a problem with credentials"), + 404: OpenApiResponse( + description="The scan has no CSA CCM reports, or the CSA CCM report generation task has not started yet" + ), + }, + ), ) @method_decorator(CACHE_DECORATOR, name="list") @method_decorator(CACHE_DECORATOR, name="retrieve") @@ -1811,6 +1843,9 @@ class ScanViewSet(BaseRLSViewSet): elif self.action == "nis2": if hasattr(self, "response_serializer_class"): return self.response_serializer_class + elif self.action == "csa": + if hasattr(self, "response_serializer_class"): + return self.response_serializer_class return super().get_serializer_class() def partial_update(self, request, *args, **kwargs): @@ -2172,6 +2207,45 @@ class ScanViewSet(BaseRLSViewSet): content, filename = loader return self._serve_file(content, filename, "application/pdf") + @action( + detail=True, + methods=["get"], + url_name="csa", + ) + def csa(self, request, pk=None): + scan = self.get_object() + running_resp = self._get_task_status(scan) + if running_resp: + return running_resp + + if not scan.output_location: + return Response( + { + "detail": "The scan has no reports, or the CSA CCM report generation task has not started yet." + }, + status=status.HTTP_404_NOT_FOUND, + ) + + if scan.output_location.startswith("s3://"): + bucket = env.str("DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET", "") + key_prefix = scan.output_location.removeprefix(f"s3://{bucket}/") + prefix = os.path.join( + os.path.dirname(key_prefix), + "csa", + "*_csa_report.pdf", + ) + loader = self._load_file(prefix, s3=True, bucket=bucket, list_objects=True) + else: + base = os.path.dirname(scan.output_location) + pattern = os.path.join(base, "csa", "*_csa_report.pdf") + loader = self._load_file(pattern, s3=False) + + if isinstance(loader, Response): + return loader + + content, filename = loader + return self._serve_file(content, filename, "application/pdf") + def create(self, request, *args, **kwargs): input_serializer = self.get_serializer(data=request.data) input_serializer.is_valid(raise_exception=True) @@ -2408,22 +2482,13 @@ class AttackPathsScanViewSet(BaseRLSViewSet): def run_attack_paths_query(self, request, pk=None): attack_paths_scan = self.get_object() - if attack_paths_scan.state != StateChoices.COMPLETED: + if not attack_paths_scan.graph_data_ready: raise ValidationError( { - "detail": "The Attack Paths scan must be completed before running Attack Paths queries" + "detail": "Attack Paths data is not available for querying - a scan must complete at least once before queries can be run" } ) - if not attack_paths_scan.graph_database: - logger.error( - f"The Attack Paths Scan {attack_paths_scan.id} does not reference a graph database" - ) - return Response( - {"detail": "The Attack Paths scan does not reference a graph database"}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - payload = attack_paths_views_helpers.normalize_run_payload(request.data) serializer = AttackPathsQueryRunRequestSerializer(data=payload) serializer.is_valid(raise_exception=True) @@ -2437,6 +2502,9 @@ class AttackPathsScanViewSet(BaseRLSViewSet): {"id": "Unknown Attack Paths query for the selected provider"} ) + database_name = graph_database.get_database_name( + attack_paths_scan.provider.tenant_id + ) parameters = attack_paths_views_helpers.prepare_query_parameters( query_definition, serializer.validated_data.get("parameters", {}), @@ -2444,9 +2512,9 @@ class AttackPathsScanViewSet(BaseRLSViewSet): ) graph = attack_paths_views_helpers.execute_attack_paths_query( - attack_paths_scan, query_definition, parameters + database_name, query_definition, parameters ) - graph_database.clear_cache(attack_paths_scan.graph_database) + graph_database.clear_cache(database_name) status_code = status.HTTP_200_OK if not graph.get("nodes"): diff --git a/api/src/backend/config/django/testing.py b/api/src/backend/config/django/testing.py index 5289f067fa..75779f5a68 100644 --- a/api/src/backend/config/django/testing.py +++ b/api/src/backend/config/django/testing.py @@ -18,6 +18,10 @@ DATABASES = { DATABASE_ROUTERS = [] TESTING = True +# Override page size for testing to a value only slightly above the current fixture count. +# We explicitly set PAGE_SIZE to 15 (round number just above fixture) to avoid masking pagination bugs, while not setting it excessively high. +# If you add more providers to the fixture, please review that the total value is below the current one and update this value if needed. +REST_FRAMEWORK["PAGE_SIZE"] = 15 # noqa: F405 SECRETS_ENCRYPTION_KEY = "ZMiYVo7m4Fbe2eXXPyrwxdJss2WSalXSv3xHBcJkPl0=" # DRF Simple API Key settings diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index e50f92e1ab..189d5a31d5 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -537,6 +537,12 @@ def providers_fixture(tenants_fixture): alias="cloudflare_testing", tenant_id=tenant.id, ) + provider11 = Provider.objects.create( + provider="openstack", + uid="a1b2c3d4-e5f6-7890-abcd-ef1234567890", + alias="openstack_testing", + tenant_id=tenant.id, + ) return ( provider1, @@ -549,6 +555,7 @@ def providers_fixture(tenants_fixture): provider8, provider9, provider10, + provider11, ) @@ -1618,7 +1625,6 @@ def create_attack_paths_scan(): scan=None, state=StateChoices.COMPLETED, progress=0, - graph_database="tenant-db", **extra_fields, ): scan_instance = scan or Scan.objects.create( @@ -1635,7 +1641,6 @@ def create_attack_paths_scan(): "scan": scan_instance, "state": state, "progress": progress, - "graph_database": graph_database, } payload.update(extra_fields) @@ -1663,6 +1668,7 @@ def attack_paths_query_definition_factory(): definition_payload = { "id": "aws-test", "name": "Attack Paths Test Query", + "short_description": "Synthetic short description for tests.", "description": "Synthetic Attack Paths definition for tests.", "provider": "aws", "cypher": "RETURN 1", diff --git a/api/src/backend/tasks/jobs/attack_paths/config.py b/api/src/backend/tasks/jobs/attack_paths/config.py index d39e6fa81f..af8094e172 100644 --- a/api/src/backend/tasks/jobs/attack_paths/config.py +++ b/api/src/backend/tasks/jobs/attack_paths/config.py @@ -12,8 +12,10 @@ BATCH_SIZE = env.int("ATTACK_PATHS_BATCH_SIZE", 1000) # Neo4j internal labels (Prowler-specific, not provider-specific) # - `ProwlerFinding`: Label for finding nodes created by Prowler and linked to cloud resources. # - `ProviderResource`: Added to ALL synced nodes for provider isolation and drop/query ops. +# - `Internet`: Singleton node representing external internet access for exposed-resource queries. PROWLER_FINDING_LABEL = "ProwlerFinding" PROVIDER_RESOURCE_LABEL = "ProviderResource" +INTERNET_NODE_LABEL = "Internet" @dataclass(frozen=True) diff --git a/api/src/backend/tasks/jobs/attack_paths/db_utils.py b/api/src/backend/tasks/jobs/attack_paths/db_utils.py index c9bbf5afdf..9fb52b0ead 100644 --- a/api/src/backend/tasks/jobs/attack_paths/db_utils.py +++ b/api/src/backend/tasks/jobs/attack_paths/db_utils.py @@ -2,7 +2,9 @@ from datetime import datetime, timezone from typing import Any from cartography.config import Config as CartographyConfig +from celery.utils.log import get_task_logger +from api.attack_paths import database as graph_database from api.db_utils import rls_transaction from api.models import ( AttackPathsScan as ProwlerAPIAttackPathsScan, @@ -11,6 +13,8 @@ from api.models import ( ) from tasks.jobs.attack_paths.config import is_provider_available +logger = get_task_logger(__name__) + def can_provider_run_attack_paths_scan(tenant_id: str, provider_id: int) -> bool: with rls_transaction(tenant_id): @@ -28,12 +32,21 @@ def create_attack_paths_scan( return None with rls_transaction(tenant_id): + # Inherit graph_data_ready from the previous scan for this provider, + # so queries remain available while the new scan runs. + previous_data_ready = ProwlerAPIAttackPathsScan.objects.filter( + tenant_id=tenant_id, + provider_id=provider_id, + graph_data_ready=True, + ).exists() + attack_paths_scan = ProwlerAPIAttackPathsScan.objects.create( tenant_id=tenant_id, provider_id=provider_id, scan_id=scan_id, state=StateChoices.SCHEDULED, started_at=datetime.now(tz=timezone.utc), + graph_data_ready=previous_data_ready, ) attack_paths_scan.save() @@ -66,7 +79,6 @@ def starting_attack_paths_scan( attack_paths_scan.state = StateChoices.EXECUTING attack_paths_scan.started_at = datetime.now(tz=timezone.utc) attack_paths_scan.update_tag = cartography_config.update_tag - attack_paths_scan.graph_database = cartography_config.neo4j_database attack_paths_scan.save( update_fields=[ @@ -74,7 +86,6 @@ def starting_attack_paths_scan( "state", "started_at", "update_tag", - "graph_database", ] ) @@ -86,7 +97,11 @@ def finish_attack_paths_scan( ) -> None: with rls_transaction(attack_paths_scan.tenant_id): now = datetime.now(tz=timezone.utc) - duration = int((now - attack_paths_scan.started_at).total_seconds()) + duration = ( + int((now - attack_paths_scan.started_at).total_seconds()) + if attack_paths_scan.started_at + else 0 + ) attack_paths_scan.state = state attack_paths_scan.progress = 100 @@ -114,33 +129,59 @@ def update_attack_paths_scan_progress( attack_paths_scan.save(update_fields=["progress"]) -def get_old_attack_paths_scans( - tenant_id: str, - provider_id: str, - attack_paths_scan_id: str, -) -> list[ProwlerAPIAttackPathsScan]: - """ - An `old_attack_paths_scan` is any `completed` Attack Paths scan for the same provider, - with its graph database not deleted, excluding the current Attack Paths scan. - """ - - with rls_transaction(tenant_id): - completed_scans_qs = ( - ProwlerAPIAttackPathsScan.objects.filter( - provider_id=provider_id, - state=StateChoices.COMPLETED, - is_graph_database_deleted=False, - ) - .exclude(id=attack_paths_scan_id) - .all() - ) - - return list(completed_scans_qs) - - -def update_old_attack_paths_scan( - old_attack_paths_scan: ProwlerAPIAttackPathsScan, +def set_graph_data_ready( + attack_paths_scan: ProwlerAPIAttackPathsScan, + ready: bool, ) -> None: - with rls_transaction(old_attack_paths_scan.tenant_id): - old_attack_paths_scan.is_graph_database_deleted = True - old_attack_paths_scan.save(update_fields=["is_graph_database_deleted"]) + with rls_transaction(attack_paths_scan.tenant_id): + attack_paths_scan.graph_data_ready = ready + attack_paths_scan.save(update_fields=["graph_data_ready"]) + + +def set_provider_graph_data_ready( + attack_paths_scan: ProwlerAPIAttackPathsScan, + ready: bool, +) -> None: + """ + Set `graph_data_ready` for ALL scans of the same provider. + + Used before drop/sync so that older scan IDs cannot bypass the query gate while the graph is being replaced. + """ + with rls_transaction(attack_paths_scan.tenant_id): + ProwlerAPIAttackPathsScan.objects.filter( + tenant_id=attack_paths_scan.tenant_id, + provider_id=attack_paths_scan.provider_id, + ).update(graph_data_ready=ready) + attack_paths_scan.refresh_from_db(fields=["graph_data_ready"]) + + +def fail_attack_paths_scan( + tenant_id: str, + scan_id: str, + error: str, +) -> None: + """ + Mark the `AttackPathsScan` row as `FAILED` unless it's already `COMPLETED` or `FAILED`. + Used as a safety net when the Celery task fails outside the job's own error handling. + """ + attack_paths_scan = retrieve_attack_paths_scan(tenant_id, scan_id) + if attack_paths_scan and attack_paths_scan.state not in ( + StateChoices.COMPLETED, + StateChoices.FAILED, + ): + tmp_db_name = graph_database.get_database_name( + attack_paths_scan.id, temporary=True + ) + try: + graph_database.drop_database(tmp_db_name) + + except Exception: + logger.exception( + f"Failed to drop temp database {tmp_db_name} during failure handling" + ) + + finish_attack_paths_scan( + attack_paths_scan, + StateChoices.FAILED, + {"global_error": error}, + ) diff --git a/api/src/backend/tasks/jobs/attack_paths/indexes.py b/api/src/backend/tasks/jobs/attack_paths/indexes.py index 708e9c7f84..9ccd8cab04 100644 --- a/api/src/backend/tasks/jobs/attack_paths/indexes.py +++ b/api/src/backend/tasks/jobs/attack_paths/indexes.py @@ -6,6 +6,7 @@ from cartography.client.core.tx import run_write_query from celery.utils.log import get_task_logger from tasks.jobs.attack_paths.config import ( + INTERNET_NODE_LABEL, PROWLER_FINDING_LABEL, PROVIDER_RESOURCE_LABEL, ) @@ -30,6 +31,8 @@ FINDINGS_INDEX_STATEMENTS = [ f"CREATE INDEX prowler_finding_provider_uid IF NOT EXISTS FOR (n:{PROWLER_FINDING_LABEL}) ON (n.provider_uid);", f"CREATE INDEX prowler_finding_lastupdated IF NOT EXISTS FOR (n:{PROWLER_FINDING_LABEL}) ON (n.lastupdated);", f"CREATE INDEX prowler_finding_status IF NOT EXISTS FOR (n:{PROWLER_FINDING_LABEL}) ON (n.status);", + # Internet node index for MERGE lookups + f"CREATE INDEX internet_id IF NOT EXISTS FOR (n:{INTERNET_NODE_LABEL}) ON (n.id);", ] # Indexes for provider resource sync operations diff --git a/api/src/backend/tasks/jobs/attack_paths/internet.py b/api/src/backend/tasks/jobs/attack_paths/internet.py new file mode 100644 index 0000000000..83517bc903 --- /dev/null +++ b/api/src/backend/tasks/jobs/attack_paths/internet.py @@ -0,0 +1,67 @@ +""" +Internet node enrichment for Attack Paths graph. + +Creates a real Internet node and CAN_ACCESS relationships to +internet-exposed resources (EC2Instance, LoadBalancer, LoadBalancerV2) +in the temporary scan database before sync. +""" + +import neo4j + +from cartography.config import Config as CartographyConfig +from celery.utils.log import get_task_logger + +from api.models import Provider +from prowler.config import config as ProwlerConfig +from tasks.jobs.attack_paths.config import get_root_node_label +from tasks.jobs.attack_paths.queries import ( + CREATE_CAN_ACCESS_RELATIONSHIPS_TEMPLATE, + CREATE_INTERNET_NODE, + render_cypher_template, +) + +logger = get_task_logger(__name__) + + +def analysis( + neo4j_session: neo4j.Session, + prowler_api_provider: Provider, + config: CartographyConfig, +) -> int: + """ + Create Internet node and CAN_ACCESS relationships to exposed resources. + + Args: + neo4j_session: Active Neo4j session (temp database). + prowler_api_provider: The Prowler API provider instance. + config: Cartography configuration with update_tag. + + Returns: + Number of CAN_ACCESS relationships created. + """ + provider_uid = str(prowler_api_provider.uid) + + parameters = { + "provider_uid": provider_uid, + "last_updated": config.update_tag, + "prowler_version": ProwlerConfig.prowler_version, + } + + logger.info(f"Creating Internet node for provider {provider_uid}") + neo4j_session.run(CREATE_INTERNET_NODE, parameters) + + query = render_cypher_template( + CREATE_CAN_ACCESS_RELATIONSHIPS_TEMPLATE, + {"__ROOT_LABEL__": get_root_node_label(prowler_api_provider.provider)}, + ) + + logger.info( + f"Creating CAN_ACCESS relationships from Internet to exposed resources for {provider_uid}" + ) + result = neo4j_session.run(query, parameters) + relationships_merged = result.single().get("relationships_merged", 0) + + logger.info( + f"Created {relationships_merged} CAN_ACCESS relationships for provider {provider_uid}" + ) + return relationships_merged diff --git a/api/src/backend/tasks/jobs/attack_paths/queries.py b/api/src/backend/tasks/jobs/attack_paths/queries.py index bf935cc9ca..75ef9ec5b3 100644 --- a/api/src/backend/tasks/jobs/attack_paths/queries.py +++ b/api/src/backend/tasks/jobs/attack_paths/queries.py @@ -1,5 +1,6 @@ # Cypher query templates for Attack Paths operations from tasks.jobs.attack_paths.config import ( + INTERNET_NODE_LABEL, PROWLER_FINDING_LABEL, PROVIDER_RESOURCE_LABEL, ) @@ -91,6 +92,37 @@ CLEANUP_FINDINGS_TEMPLATE = f""" RETURN COUNT(finding) AS deleted_findings_count """ +# Internet queries (used by internet.py) +# --------------------------------------- + +CREATE_INTERNET_NODE = f""" + MERGE (internet:{INTERNET_NODE_LABEL} {{id: 'Internet'}}) + ON CREATE SET + internet.name = 'Internet', + internet.firstseen = timestamp(), + internet.lastupdated = $last_updated, + internet._module_name = 'cartography:prowler', + internet._module_version = $prowler_version + ON MATCH SET + internet.lastupdated = $last_updated +""" + +CREATE_CAN_ACCESS_RELATIONSHIPS_TEMPLATE = f""" + MATCH (account:__ROOT_LABEL__ {{id: $provider_uid}})-->(resource) + WHERE resource.exposed_internet = true + WITH resource + MATCH (internet:{INTERNET_NODE_LABEL} {{id: 'Internet'}}) + MERGE (internet)-[r:CAN_ACCESS]->(resource) + ON CREATE SET + r.firstseen = timestamp(), + r.lastupdated = $last_updated, + r._module_name = 'cartography:prowler', + r._module_version = $prowler_version + ON MATCH SET + r.lastupdated = $last_updated + RETURN COUNT(r) AS relationships_merged +""" + # Sync queries (used by sync.py) # ------------------------------- diff --git a/api/src/backend/tasks/jobs/attack_paths/scan.py b/api/src/backend/tasks/jobs/attack_paths/scan.py index 1ffcbf55e7..da70b77383 100644 --- a/api/src/backend/tasks/jobs/attack_paths/scan.py +++ b/api/src/backend/tasks/jobs/attack_paths/scan.py @@ -16,7 +16,7 @@ from api.models import ( StateChoices, ) from api.utils import initialize_prowler_provider -from tasks.jobs.attack_paths import db_utils, findings, sync, utils +from tasks.jobs.attack_paths import db_utils, findings, internet, sync, utils from tasks.jobs.attack_paths.config import get_cartography_ingestion_function # Without this Celery goes crazy with Cartography logging @@ -135,7 +135,15 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]: cartography_analysis.run(tmp_neo4j_session, tmp_cartography_config) db_utils.update_attack_paths_scan_progress(attack_paths_scan, 96) - # Adding Prowler nodes and relationships + # Creating Internet node and CAN_ACCESS relationships + logger.info( + f"Creating Internet graph for AWS account {prowler_api_provider.uid}" + ) + internet.analysis( + tmp_neo4j_session, prowler_api_provider, tmp_cartography_config + ) + + # Adding Prowler Finding nodes and relationships logger.info( f"Syncing Prowler analysis for AWS account {prowler_api_provider.uid}" ) @@ -161,6 +169,7 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]: sync.create_sync_indexes(tenant_neo4j_session) logger.info(f"Deleting existing provider graph in {tenant_database_name}") + db_utils.set_provider_graph_data_ready(attack_paths_scan, False) graph_database.drop_subgraph( database=tenant_database_name, provider_id=str(prowler_api_provider.id), @@ -175,6 +184,7 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]: target_database=tenant_database_name, provider_id=str(prowler_api_provider.id), ) + db_utils.set_graph_data_ready(attack_paths_scan, True) db_utils.update_attack_paths_scan_progress(attack_paths_scan, 99) logger.info(f"Clearing Neo4j cache for database {tenant_database_name}") @@ -185,30 +195,6 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]: f"{prowler_api_provider.provider.upper()} provider {prowler_api_provider.id}" ) - # TODO - # This piece of code delete old Neo4j databases for this tenant's provider - # When we clean all of these databases we need to: - # - Delete this block - # - Delete function from `db_utils` the functions get_old_attack_paths_scans` & `update_old_attack_paths_scan` - # - Remove `graph_database` & `is_graph_database_deleted` from the AttackPathsScan model: - # - Check indexes - # - Create migration - # - The use of `attack_paths_scan.graph_database` on `views` and `views_helpers` - # - Tests - old_attack_paths_scans = db_utils.get_old_attack_paths_scans( - prowler_api_provider.tenant_id, - prowler_api_provider.id, - attack_paths_scan.id, - ) - for old_attack_paths_scan in old_attack_paths_scans: - old_graph_database = old_attack_paths_scan.graph_database - if old_graph_database and old_graph_database != tenant_database_name: - logger.info( - f"Dropping old Neo4j database {old_graph_database} for provider {prowler_api_provider.id}" - ) - graph_database.drop_database(old_graph_database) - db_utils.update_old_attack_paths_scan(old_attack_paths_scan) - logger.info(f"Dropping temporary Neo4j database {tmp_database_name}") graph_database.drop_database(tmp_database_name) @@ -220,10 +206,17 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]: except Exception as e: exception_message = utils.stringify_exception(e, "Cartography failed") logger.error(exception_message) - ingestion_exceptions["global_cartography_error"] = exception_message + ingestion_exceptions["global_error"] = exception_message # Handling databases changes - graph_database.drop_database(tmp_cartography_config.neo4j_database) + try: + graph_database.drop_database(tmp_cartography_config.neo4j_database) + + except Exception: + logger.exception( + f"Failed to drop temporary Neo4j database {tmp_cartography_config.neo4j_database} during cleanup" + ) + db_utils.finish_attack_paths_scan( attack_paths_scan, StateChoices.FAILED, ingestion_exceptions ) diff --git a/api/src/backend/tasks/jobs/deletion.py b/api/src/backend/tasks/jobs/deletion.py index ba59eaeb5f..21c9031bf5 100644 --- a/api/src/backend/tasks/jobs/deletion.py +++ b/api/src/backend/tasks/jobs/deletion.py @@ -27,23 +27,24 @@ def delete_provider(tenant_id: str, pk: str): Returns: dict: A dictionary with the count of deleted objects per model, - including related models. - - Raises: - Provider.DoesNotExist: If no instance with the provided primary key exists. + including related models. Returns an empty dict if the provider + was already deleted. """ - # Delete the Attack Paths' graph data related to the provider - tenant_database_name = graph_database.get_database_name(tenant_id) - try: - graph_database.drop_subgraph(tenant_database_name, str(pk)) - except graph_database.GraphDatabaseQueryException as gdb_error: - logger.error(f"Error deleting Provider graph data: {gdb_error}") - raise - - # Get all provider related data and delete them in batches + # Get all provider related data to delete them in batches with rls_transaction(tenant_id): - instance = Provider.all_objects.get(pk=pk) + try: + instance = Provider.all_objects.get(pk=pk) + except Provider.DoesNotExist: + logger.info(f"Provider `{pk}` already deleted, skipping") + return {} + + attack_paths_scan_ids = list( + AttackPathsScan.all_objects.filter(provider=instance).values_list( + "id", flat=True + ) + ) + deletion_steps = [ ("Scan Summaries", ScanSummary.all_objects.filter(scan__provider=instance)), ("Findings", Finding.all_objects.filter(scan__provider=instance)), @@ -52,6 +53,25 @@ def delete_provider(tenant_id: str, pk: str): ("AttackPathsScans", AttackPathsScan.all_objects.filter(provider=instance)), ] + # Drop orphaned temporary Neo4j databases + for aps_id in attack_paths_scan_ids: + tmp_db_name = graph_database.get_database_name(aps_id, temporary=True) + try: + graph_database.drop_database(tmp_db_name) + + except graph_database.GraphDatabaseQueryException: + logger.warning(f"Failed to drop temp database {tmp_db_name}, continuing") + + # Delete the Attack Paths' graph data related to the provider from the tenant database + tenant_database_name = graph_database.get_database_name(tenant_id) + try: + graph_database.drop_subgraph(tenant_database_name, str(pk)) + + except graph_database.GraphDatabaseQueryException as gdb_error: + logger.error(f"Error deleting Provider graph data: {gdb_error}") + raise + + # Delete related data in batches deletion_summary = {} for step_name, queryset in deletion_steps: try: @@ -61,6 +81,7 @@ def delete_provider(tenant_id: str, pk: str): logger.error(f"Error deleting {step_name}: {db_error}") raise + # Delete the provider instance itself try: with rls_transaction(tenant_id): _, provider_summary = instance.delete() @@ -85,7 +106,9 @@ def delete_tenant(pk: str): """ deletion_summary = {} - for provider in Provider.objects.using(MainRouter.admin_db).filter(tenant_id=pk): + for provider in Provider.all_objects.using(MainRouter.admin_db).filter( + tenant_id=pk + ): summary = delete_provider(pk, provider.id) deletion_summary.update(summary) diff --git a/api/src/backend/tasks/jobs/export.py b/api/src/backend/tasks/jobs/export.py index 883d7d0dbe..c2a947d249 100644 --- a/api/src/backend/tasks/jobs/export.py +++ b/api/src/backend/tasks/jobs/export.py @@ -35,6 +35,11 @@ from prowler.lib.outputs.compliance.cis.cis_github import GithubCIS 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.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 +from prowler.lib.outputs.compliance.csa.csa_gcp import GCPCSA +from prowler.lib.outputs.compliance.csa.csa_oraclecloud import OracleCloudCSA from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS @@ -90,6 +95,7 @@ COMPLIANCE_CLASS_MAP = { (lambda name: name == "prowler_threatscore_aws", ProwlerThreatScoreAWS), (lambda name: name == "ccc_aws", CCC_AWS), (lambda name: name.startswith("c5_"), AWSC5), + (lambda name: name.startswith("csa_"), AWSCSA), ], "azure": [ (lambda name: name.startswith("cis_"), AzureCIS), @@ -99,6 +105,7 @@ COMPLIANCE_CLASS_MAP = { (lambda name: name == "ccc_azure", CCC_Azure), (lambda name: name == "prowler_threatscore_azure", ProwlerThreatScoreAzure), (lambda name: name == "c5_azure", AzureC5), + (lambda name: name.startswith("csa_"), AzureCSA), ], "gcp": [ (lambda name: name.startswith("cis_"), GCPCIS), @@ -108,6 +115,7 @@ COMPLIANCE_CLASS_MAP = { (lambda name: name == "prowler_threatscore_gcp", ProwlerThreatScoreGCP), (lambda name: name == "ccc_gcp", CCC_GCP), (lambda name: name == "c5_gcp", GCPC5), + (lambda name: name.startswith("csa_"), GCPCSA), ], "kubernetes": [ (lambda name: name.startswith("cis_"), KubernetesCIS), @@ -131,9 +139,11 @@ COMPLIANCE_CLASS_MAP = { ], "oraclecloud": [ (lambda name: name.startswith("cis_"), OracleCloudCIS), + (lambda name: name.startswith("csa_"), OracleCloudCSA), ], "alibabacloud": [ (lambda name: name.startswith("cis_"), AlibabaCloudCIS), + (lambda name: name.startswith("csa_"), AlibabaCloudCSA), ( lambda name: name == "prowler_threatscore_alibabacloud", ProwlerThreatScoreAlibaba, diff --git a/api/src/backend/tasks/jobs/report.py b/api/src/backend/tasks/jobs/report.py index de022e9afd..a41a8d6292 100644 --- a/api/src/backend/tasks/jobs/report.py +++ b/api/src/backend/tasks/jobs/report.py @@ -6,6 +6,7 @@ from config.django.base import DJANGO_TMP_OUTPUT_DIRECTORY from tasks.jobs.export import _generate_compliance_output_directory, _upload_to_s3 from tasks.jobs.reports import ( FRAMEWORK_REGISTRY, + CSAReportGenerator, ENSReportGenerator, NIS2ReportGenerator, ThreatScoreReportGenerator, @@ -147,6 +148,49 @@ def generate_nis2_report( ) +def generate_csa_report( + tenant_id: str, + scan_id: str, + compliance_id: str, + output_path: str, + provider_id: str, + only_failed: bool = True, + include_manual: bool = False, + provider_obj: Provider | None = None, + requirement_statistics: dict[str, dict[str, int]] | None = None, + findings_cache: dict[str, list[FindingOutput]] | None = None, +) -> None: + """ + Generate a PDF compliance report for CSA Cloud Controls Matrix (CCM) v4.0. + + Args: + tenant_id: The tenant ID for Row-Level Security context. + scan_id: ID of the scan executed by Prowler. + compliance_id: ID of the compliance framework (e.g., "csa_ccm_4.0_aws"). + output_path: Output PDF file path. + provider_id: Provider ID for the scan. + only_failed: If True, only include failed requirements in detailed section. + include_manual: If True, include manual requirements in detailed section. + provider_obj: Pre-fetched Provider object to avoid duplicate queries. + requirement_statistics: Pre-aggregated requirement statistics. + findings_cache: Cache of already loaded findings to avoid duplicate queries. + """ + generator = CSAReportGenerator(FRAMEWORK_REGISTRY["csa_ccm"]) + + generator.generate( + tenant_id=tenant_id, + scan_id=scan_id, + compliance_id=compliance_id, + output_path=output_path, + provider_id=provider_id, + provider_obj=provider_obj, + requirement_statistics=requirement_statistics, + findings_cache=findings_cache, + only_failed=only_failed, + include_manual=include_manual, + ) + + def generate_compliance_reports( tenant_id: str, scan_id: str, @@ -154,11 +198,14 @@ def generate_compliance_reports( generate_threatscore: bool = True, generate_ens: bool = True, generate_nis2: bool = True, + generate_csa: bool = True, only_failed_threatscore: bool = True, min_risk_level_threatscore: int = 4, include_manual_ens: bool = True, include_manual_nis2: bool = False, only_failed_nis2: bool = True, + only_failed_csa: bool = True, + include_manual_csa: bool = False, ) -> dict[str, dict[str, bool | str]]: """ Generate multiple compliance reports with shared database queries. @@ -175,23 +222,27 @@ def generate_compliance_reports( generate_threatscore: Whether to generate ThreatScore report. generate_ens: Whether to generate ENS report. generate_nis2: Whether to generate NIS2 report. + generate_csa: Whether to generate CSA CCM report. only_failed_threatscore: For ThreatScore, only include failed requirements. min_risk_level_threatscore: Minimum risk level for ThreatScore critical requirements. include_manual_ens: For ENS, include manual requirements. include_manual_nis2: For NIS2, include manual requirements. only_failed_nis2: For NIS2, only include failed requirements. + only_failed_csa: For CSA CCM, only include failed requirements. + include_manual_csa: For CSA CCM, include manual requirements. Returns: Dictionary with results for each report type. """ logger.info( "Generating compliance reports for scan %s with provider %s" - " (ThreatScore: %s, ENS: %s, NIS2: %s)", + " (ThreatScore: %s, ENS: %s, NIS2: %s, CSA: %s)", scan_id, provider_id, generate_threatscore, generate_ens, generate_nis2, + generate_csa, ) results = {} @@ -206,6 +257,8 @@ def generate_compliance_reports( results["ens"] = {"upload": False, "path": ""} if generate_nis2: results["nis2"] = {"upload": False, "path": ""} + if generate_csa: + results["csa"] = {"upload": False, "path": ""} return results provider_obj = Provider.objects.get(id=provider_id) @@ -235,7 +288,23 @@ def generate_compliance_reports( results["nis2"] = {"upload": False, "path": ""} generate_nis2 = False - if not generate_threatscore and not generate_ens and not generate_nis2: + if generate_csa and provider_type not in [ + "aws", + "azure", + "gcp", + "oraclecloud", + "alibabacloud", + ]: + logger.info("Provider %s not supported for CSA CCM report", provider_type) + results["csa"] = {"upload": False, "path": ""} + generate_csa = False + + if ( + not generate_threatscore + and not generate_ens + and not generate_nis2 + and not generate_csa + ): return results # Aggregate requirement statistics once @@ -274,6 +343,13 @@ def generate_compliance_reports( scan_id, compliance_framework="nis2", ) + csa_path = _generate_compliance_output_directory( + DJANGO_TMP_OUTPUT_DIRECTORY, + provider_uid, + tenant_id, + scan_id, + compliance_framework="csa", + ) out_dir = str(Path(threatscore_path).parent.parent) except Exception as e: logger.error("Error generating output directory: %s", e) @@ -284,6 +360,8 @@ def generate_compliance_reports( results["ens"] = error_dict.copy() if generate_nis2: results["nis2"] = error_dict.copy() + if generate_csa: + results["csa"] = error_dict.copy() return results # Generate ThreatScore report @@ -456,6 +534,41 @@ def generate_compliance_reports( logger.error("Error generating NIS2 report: %s", e) results["nis2"] = {"upload": False, "path": "", "error": str(e)} + # Generate CSA CCM report + if generate_csa: + compliance_id_csa = f"csa_ccm_4.0_{provider_type}" + pdf_path_csa = f"{csa_path}_csa_report.pdf" + logger.info("Generating CSA CCM report with compliance %s", compliance_id_csa) + + try: + generate_csa_report( + tenant_id=tenant_id, + scan_id=scan_id, + compliance_id=compliance_id_csa, + output_path=pdf_path_csa, + provider_id=provider_id, + only_failed=only_failed_csa, + include_manual=include_manual_csa, + provider_obj=provider_obj, + requirement_statistics=requirement_statistics, + findings_cache=findings_cache, + ) + + upload_uri_csa = _upload_to_s3( + tenant_id, scan_id, pdf_path_csa, f"csa/{Path(pdf_path_csa).name}" + ) + + if upload_uri_csa: + results["csa"] = {"upload": True, "path": upload_uri_csa} + logger.info("CSA CCM report uploaded to %s", upload_uri_csa) + else: + results["csa"] = {"upload": False, "path": out_dir} + logger.warning("CSA CCM report saved locally at %s", out_dir) + + except Exception as e: + logger.error("Error generating CSA CCM report: %s", e) + results["csa"] = {"upload": False, "path": "", "error": str(e)} + # Clean up temporary files if all reports were uploaded successfully all_uploaded = all( result.get("upload", False) @@ -481,6 +594,7 @@ def generate_compliance_reports_job( generate_threatscore: bool = True, generate_ens: bool = True, generate_nis2: bool = True, + generate_csa: bool = True, ) -> dict[str, dict[str, bool | str]]: """ Celery task wrapper for generate_compliance_reports. @@ -492,6 +606,7 @@ def generate_compliance_reports_job( generate_threatscore: Whether to generate ThreatScore report. generate_ens: Whether to generate ENS report. generate_nis2: Whether to generate NIS2 report. + generate_csa: Whether to generate CSA CCM report. Returns: Dictionary with results for each report type. @@ -503,4 +618,5 @@ def generate_compliance_reports_job( generate_threatscore=generate_threatscore, generate_ens=generate_ens, generate_nis2=generate_nis2, + generate_csa=generate_csa, ) diff --git a/api/src/backend/tasks/jobs/reports/__init__.py b/api/src/backend/tasks/jobs/reports/__init__.py index 60602b93ab..1fc475a467 100644 --- a/api/src/backend/tasks/jobs/reports/__init__.py +++ b/api/src/backend/tasks/jobs/reports/__init__.py @@ -71,6 +71,8 @@ from .config import ( COLOR_PROWLER_DARK_GREEN, COLOR_SAFE, COLOR_WHITE, + CSA_CCM_SECTION_SHORT_NAMES, + CSA_CCM_SECTIONS, DIMENSION_KEYS, DIMENSION_MAPPING, DIMENSION_NAMES, @@ -90,6 +92,7 @@ from .config import ( ) # Framework-specific generators +from .csa import CSAReportGenerator from .ens import ENSReportGenerator from .nis2 import NIS2ReportGenerator from .threatscore import ThreatScoreReportGenerator @@ -105,6 +108,7 @@ __all__ = [ "ThreatScoreReportGenerator", "ENSReportGenerator", "NIS2ReportGenerator", + "CSAReportGenerator", # Configuration "FrameworkConfig", "FRAMEWORK_REGISTRY", @@ -147,6 +151,8 @@ __all__ = [ "THREATSCORE_SECTIONS", "NIS2_SECTIONS", "NIS2_SECTION_TITLES", + "CSA_CCM_SECTIONS", + "CSA_CCM_SECTION_SHORT_NAMES", # Layout constants "COL_WIDTH_SMALL", "COL_WIDTH_MEDIUM", diff --git a/api/src/backend/tasks/jobs/reports/base.py b/api/src/backend/tasks/jobs/reports/base.py index 42776681fb..f348c6d0d2 100644 --- a/api/src/backend/tasks/jobs/reports/base.py +++ b/api/src/backend/tasks/jobs/reports/base.py @@ -662,6 +662,9 @@ class BaseComplianceReportGenerator(ABC): elements.append(create_status_badge(req.status)) elements.append(Spacer(1, 0.1 * inch)) + # Hook for subclasses to add extra detail (e.g., CSA attributes) + elements.extend(self._render_requirement_detail_extras(req, data)) + # Findings for this requirement for check_id in req.checks: elements.append(Paragraph(f"Check: {check_id}", self.styles["h2"])) @@ -701,6 +704,24 @@ class BaseComplianceReportGenerator(ABC): return page_text, "Powered by Prowler" + def _render_requirement_detail_extras( + self, req: RequirementData, data: ComplianceData + ) -> list: + """Hook for subclasses to render extra content in detailed findings. + + Called after the status badge for each requirement in the detailed + findings section. Override in subclasses to add framework-specific + metadata (e.g., CSA CCM attributes). + + Args: + req: The requirement being rendered. + data: Aggregated compliance data. + + Returns: + List of ReportLab elements (empty by default). + """ + return [] + # ========================================================================= # Private Helper Methods # ========================================================================= diff --git a/api/src/backend/tasks/jobs/reports/config.py b/api/src/backend/tasks/jobs/reports/config.py index 0785505820..fe0326980d 100644 --- a/api/src/backend/tasks/jobs/reports/config.py +++ b/api/src/backend/tasks/jobs/reports/config.py @@ -143,6 +143,36 @@ NIS2_SECTION_TITLES = { "12": "12. Asset Management", } +# CSA CCM sections (Cloud Controls Matrix v4.0 domains) +CSA_CCM_SECTIONS = [ + "Application & Interface Security", + "Audit & Assurance", + "Business Continuity Management and Operational Resilience", + "Change Control and Configuration Management", + "Cryptography, Encryption & Key Management", + "Data Security and Privacy Lifecycle Management", + "Datacenter Security", + "Governance, Risk and Compliance", + "Identity & Access Management", + "Infrastructure & Virtualization Security", + "Interoperability & Portability", + "Logging and Monitoring", + "Security Incident Management, E-Discovery, & Cloud Forensics", + "Threat & Vulnerability Management", + "Universal Endpoint Management", +] + +# Short names for CSA CCM sections (used in chart labels) +CSA_CCM_SECTION_SHORT_NAMES = { + "Application & Interface Security": "App & Interface Security", + "Business Continuity Management and Operational Resilience": "Business Continuity", + "Change Control and Configuration Management": "Change Control & Config", + "Cryptography, Encryption & Key Management": "Cryptography & Encryption", + "Data Security and Privacy Lifecycle Management": "Data Security & Privacy", + "Security Incident Management, E-Discovery, & Cloud Forensics": "Incident Mgmt & Forensics", + "Infrastructure & Virtualization Security": "Infrastructure & Virtualization", +} + # Table column widths COL_WIDTH_SMALL = 0.4 * inch COL_WIDTH_MEDIUM = 0.9 * inch @@ -261,6 +291,28 @@ FRAMEWORK_REGISTRY: dict[str, FrameworkConfig] = { has_niveles=False, has_weight=False, ), + "csa_ccm": FrameworkConfig( + name="csa_ccm", + display_name="CSA Cloud Controls Matrix (CCM)", + logo_filename=None, + primary_color=COLOR_BLUE, + secondary_color=COLOR_LIGHT_BLUE, + bg_color=COLOR_BG_BLUE, + attribute_fields=[ + "Section", + "CCMLite", + "IaaS", + "PaaS", + "SaaS", + "ScopeApplicability", + ], + sections=CSA_CCM_SECTIONS, + language="en", + has_risk_levels=False, + has_dimensions=False, + has_niveles=False, + has_weight=False, + ), } @@ -282,5 +334,7 @@ def get_framework_config(compliance_id: str) -> FrameworkConfig | None: return FRAMEWORK_REGISTRY["ens"] if "nis2" in compliance_lower: return FRAMEWORK_REGISTRY["nis2"] + if "csa" in compliance_lower or "ccm" in compliance_lower: + return FRAMEWORK_REGISTRY["csa_ccm"] return None diff --git a/api/src/backend/tasks/jobs/reports/csa.py b/api/src/backend/tasks/jobs/reports/csa.py new file mode 100644 index 0000000000..c55ed198de --- /dev/null +++ b/api/src/backend/tasks/jobs/reports/csa.py @@ -0,0 +1,474 @@ +from collections import defaultdict + +from celery.utils.log import get_task_logger +from reportlab.lib.units import inch +from reportlab.platypus import Image, PageBreak, Paragraph, Spacer, Table, TableStyle + +from api.models import StatusChoices + +from .base import ( + BaseComplianceReportGenerator, + ComplianceData, + get_requirement_metadata, +) +from .charts import create_horizontal_bar_chart, get_chart_color_for_percentage +from .config import ( + COLOR_BG_BLUE, + COLOR_BLUE, + COLOR_BORDER_GRAY, + COLOR_DARK_GRAY, + COLOR_GRID_GRAY, + COLOR_HIGH_RISK, + COLOR_SAFE, + COLOR_WHITE, + CSA_CCM_SECTION_SHORT_NAMES, + CSA_CCM_SECTIONS, +) + +logger = get_task_logger(__name__) + + +class CSAReportGenerator(BaseComplianceReportGenerator): + """ + PDF report generator for CSA Cloud Controls Matrix (CCM) v4.0. + + This generator creates comprehensive PDF reports containing: + - Cover page with Prowler logo + - Executive summary with overall compliance score + - Section analysis with horizontal bar chart + - Section breakdown table + - Requirements index organized by section + - Detailed findings for failed requirements + """ + + def create_executive_summary(self, data: ComplianceData) -> list: + """ + Create the executive summary with compliance metrics. + + Args: + data: Aggregated compliance data. + + Returns: + List of ReportLab elements. + """ + elements = [] + + elements.append(Paragraph("Executive Summary", self.styles["h1"])) + elements.append(Spacer(1, 0.1 * inch)) + + # Calculate statistics + total = len(data.requirements) + passed = sum(1 for r in data.requirements if r.status == StatusChoices.PASS) + failed = sum(1 for r in data.requirements if r.status == StatusChoices.FAIL) + manual = sum(1 for r in data.requirements if r.status == StatusChoices.MANUAL) + + logger.info( + "CSA CCM Executive Summary: total=%d, passed=%d, failed=%d, manual=%d", + total, + passed, + failed, + manual, + ) + + # Log sample of requirements for debugging + for req in data.requirements[:5]: + logger.info( + " Requirement %s: status=%s, passed_findings=%d, total_findings=%d", + req.id, + req.status, + req.passed_findings, + req.total_findings, + ) + + # Calculate compliance excluding manual + evaluated = passed + failed + overall_compliance = (passed / evaluated * 100) if evaluated > 0 else 100 + + # Summary statistics table + summary_data = [ + ["Metric", "Value"], + ["Total Requirements", str(total)], + ["Passed \u2713", str(passed)], + ["Failed \u2717", str(failed)], + ["Manual \u2299", str(manual)], + ["Overall Compliance", f"{overall_compliance:.1f}%"], + ] + + summary_table = Table(summary_data, colWidths=[3 * inch, 2 * inch]) + summary_table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), COLOR_BLUE), + ("TEXTCOLOR", (0, 0), (-1, 0), COLOR_WHITE), + ("BACKGROUND", (0, 2), (0, 2), COLOR_SAFE), + ("TEXTCOLOR", (0, 2), (0, 2), COLOR_WHITE), + ("BACKGROUND", (0, 3), (0, 3), COLOR_HIGH_RISK), + ("TEXTCOLOR", (0, 3), (0, 3), COLOR_WHITE), + ("BACKGROUND", (0, 4), (0, 4), COLOR_DARK_GRAY), + ("TEXTCOLOR", (0, 4), (0, 4), COLOR_WHITE), + ("ALIGN", (0, 0), (-1, -1), "CENTER"), + ("FONTNAME", (0, 0), (-1, 0), "PlusJakartaSans"), + ("FONTSIZE", (0, 0), (-1, 0), 12), + ("FONTSIZE", (0, 1), (-1, -1), 10), + ("BOTTOMPADDING", (0, 0), (-1, 0), 10), + ("GRID", (0, 0), (-1, -1), 0.5, COLOR_BORDER_GRAY), + ( + "ROWBACKGROUNDS", + (1, 1), + (1, -1), + [COLOR_WHITE, COLOR_BG_BLUE], + ), + ] + ) + ) + elements.append(summary_table) + + return elements + + def create_charts_section(self, data: ComplianceData) -> list: + """ + Create the charts section with section analysis. + + Args: + data: Aggregated compliance data. + + Returns: + List of ReportLab elements. + """ + elements = [] + + # Section chart + elements.append(Paragraph("Compliance by Section", self.styles["h1"])) + elements.append(Spacer(1, 0.1 * inch)) + elements.append( + Paragraph( + "The following chart shows compliance percentage for each domain " + "of the CSA Cloud Controls Matrix:", + self.styles["normal_center"], + ) + ) + elements.append(Spacer(1, 0.1 * inch)) + + chart_buffer = self._create_section_chart(data) + chart_buffer.seek(0) + chart_image = Image(chart_buffer, width=6.5 * inch, height=5 * inch) + elements.append(chart_image) + elements.append(PageBreak()) + + # Section breakdown table + elements.append(Paragraph("Section Breakdown", self.styles["h1"])) + elements.append(Spacer(1, 0.1 * inch)) + + section_table = self._create_section_table(data) + elements.append(section_table) + + return elements + + def create_requirements_index(self, data: ComplianceData) -> list: + """ + Create the requirements index organized by section. + + Args: + data: Aggregated compliance data. + + Returns: + List of ReportLab elements. + """ + elements = [] + + elements.append(Paragraph("Requirements Index", self.styles["h1"])) + elements.append(Spacer(1, 0.1 * inch)) + + # Organize by section + sections = {} + for req in data.requirements: + m = get_requirement_metadata(req.id, data.attributes_by_requirement_id) + if m: + section = getattr(m, "Section", "Other") + + if section not in sections: + sections[section] = [] + + sections[section].append( + { + "id": req.id, + "description": req.description, + "status": req.status, + } + ) + + # Sort by CSA CCM section order + for section in CSA_CCM_SECTIONS: + if section not in sections: + continue + + elements.append(Paragraph(section, self.styles["h2"])) + + for req in sections[section]: + status_indicator = ( + "\u2713" if req["status"] == StatusChoices.PASS else "\u2717" + ) + if req["status"] == StatusChoices.MANUAL: + status_indicator = "\u2299" + + desc = ( + req["description"][:80] + "..." + if len(req["description"]) > 80 + else req["description"] + ) + elements.append( + Paragraph( + f"{status_indicator} {req['id']}: {desc}", + self.styles["normal"], + ) + ) + + elements.append(Spacer(1, 0.1 * inch)) + + return elements + + def _render_requirement_detail_extras(self, req, data: ComplianceData) -> list: + """ + Render CSA CCM attributes in the detailed findings view. + + Shows CCMLite flag, IaaS/PaaS/SaaS applicability, and + cross-framework references after the status badge for each requirement. + + Args: + req: The requirement being rendered. + data: Aggregated compliance data. + + Returns: + List of ReportLab elements. + """ + m = get_requirement_metadata(req.id, data.attributes_by_requirement_id) + if not m: + return [] + return self._format_requirement_attributes(m) + + def _format_requirement_attributes(self, m) -> list: + """ + Format CSA CCM requirement attributes as compact PDF elements. + + Displays CCMLite flag, IaaS/PaaS/SaaS applicability, and + cross-framework references from ScopeApplicability. + + Args: + m: Requirement metadata (CSA_CCM_Requirement_Attribute). + + Returns: + List of ReportLab elements. + """ + elements = [] + + # Applicability line: CCMLite | IaaS | PaaS | SaaS + ccm_lite = getattr(m, "CCMLite", "") + iaas = getattr(m, "IaaS", "") + paas = getattr(m, "PaaS", "") + saas = getattr(m, "SaaS", "") + + applicability_parts = [] + if ccm_lite: + applicability_parts.append(f"CCMLite: {ccm_lite}") + if iaas: + applicability_parts.append(f"IaaS: {iaas}") + if paas: + applicability_parts.append(f"PaaS: {paas}") + if saas: + applicability_parts.append(f"SaaS: {saas}") + + if applicability_parts: + elements.append( + Paragraph( + f"" + f"{'  |  '.join(applicability_parts)}" + f"", + self._attr_style(), + ) + ) + + # ScopeApplicability references (compact) + scope_list = getattr(m, "ScopeApplicability", []) + if scope_list: + refs = [] + for scope in scope_list: + ref_id = scope.get("ReferenceId", "") if isinstance(scope, dict) else "" + identifiers = ( + scope.get("Identifiers", []) if isinstance(scope, dict) else [] + ) + if ref_id and identifiers: + ids_str = ", ".join(str(i) for i in identifiers[:4]) + if len(identifiers) > 4: + ids_str += "..." + refs.append(f"{ref_id}: {ids_str}") + + if refs: + refs_text = "  |  ".join(refs) + elements.append( + Paragraph( + f"{refs_text}", + self._attr_style(), + ) + ) + + return elements + + def _attr_style(self): + """ + Return a compact style for attribute text lines. + + Returns: + ParagraphStyle for attribute display. + """ + from reportlab.lib.styles import ParagraphStyle + + return ParagraphStyle( + "AttrLine", + parent=self.styles["normal"], + fontSize=10, + spaceBefore=2, + spaceAfter=2, + leftIndent=30, + leading=13, + ) + + def _create_section_chart(self, data: ComplianceData): + """ + Create the section compliance chart. + + Args: + data: Aggregated compliance data. + + Returns: + BytesIO buffer containing the chart image. + """ + section_scores = defaultdict(lambda: {"passed": 0, "total": 0}) + + no_metadata_count = 0 + for req in data.requirements: + if req.status == StatusChoices.MANUAL: + continue + + m = get_requirement_metadata(req.id, data.attributes_by_requirement_id) + if m: + section = getattr(m, "Section", "Other") + section_scores[section]["total"] += 1 + if req.status == StatusChoices.PASS: + section_scores[section]["passed"] += 1 + else: + no_metadata_count += 1 + + if no_metadata_count > 0: + logger.warning( + "CSA CCM chart: %d requirements had no metadata", no_metadata_count + ) + + logger.info("CSA CCM section scores:") + for section in CSA_CCM_SECTIONS: + if section in section_scores: + scores = section_scores[section] + pct = ( + (scores["passed"] / scores["total"] * 100) + if scores["total"] > 0 + else 0 + ) + logger.info( + " %s: %d/%d (%.1f%%)", + section, + scores["passed"], + scores["total"], + pct, + ) + + # Build labels and values in CSA CCM section order + labels = [] + values = [] + for section in CSA_CCM_SECTIONS: + if section in section_scores and section_scores[section]["total"] > 0: + scores = section_scores[section] + pct = (scores["passed"] / scores["total"]) * 100 + # Use short name if available + label = CSA_CCM_SECTION_SHORT_NAMES.get(section, section) + labels.append(label) + values.append(pct) + + return create_horizontal_bar_chart( + labels=labels, + values=values, + xlabel="Compliance (%)", + color_func=get_chart_color_for_percentage, + ) + + def _create_section_table(self, data: ComplianceData) -> Table: + """ + Create the section breakdown table. + + Args: + data: Aggregated compliance data. + + Returns: + ReportLab Table element. + """ + section_scores = defaultdict(lambda: {"passed": 0, "failed": 0, "manual": 0}) + + for req in data.requirements: + m = get_requirement_metadata(req.id, data.attributes_by_requirement_id) + if m: + section = getattr(m, "Section", "Other") + + if req.status == StatusChoices.PASS: + section_scores[section]["passed"] += 1 + elif req.status == StatusChoices.FAIL: + section_scores[section]["failed"] += 1 + else: + section_scores[section]["manual"] += 1 + + table_data = [["Section", "Passed", "Failed", "Manual", "Compliance"]] + for section in CSA_CCM_SECTIONS: + if section not in section_scores: + continue + scores = section_scores[section] + total = scores["passed"] + scores["failed"] + pct = (scores["passed"] / total * 100) if total > 0 else 100 + # Use short name if available + label = CSA_CCM_SECTION_SHORT_NAMES.get(section, section) + table_data.append( + [ + label, + str(scores["passed"]), + str(scores["failed"]), + str(scores["manual"]), + f"{pct:.1f}%", + ] + ) + + table = Table( + table_data, + colWidths=[2.4 * inch, 0.9 * inch, 0.9 * inch, 0.9 * inch, 1.2 * inch], + ) + table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), COLOR_BLUE), + ("TEXTCOLOR", (0, 0), (-1, 0), COLOR_WHITE), + ("FONTNAME", (0, 0), (-1, 0), "FiraCode"), + ("FONTSIZE", (0, 0), (-1, 0), 10), + ("ALIGN", (0, 0), (-1, -1), "CENTER"), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("FONTSIZE", (0, 1), (-1, -1), 9), + ("GRID", (0, 0), (-1, -1), 0.5, COLOR_GRID_GRAY), + ("LEFTPADDING", (0, 0), (-1, -1), 6), + ("RIGHTPADDING", (0, 0), (-1, -1), 6), + ("TOPPADDING", (0, 0), (-1, -1), 4), + ("BOTTOMPADDING", (0, 0), (-1, -1), 4), + ( + "ROWBACKGROUNDS", + (0, 1), + (-1, -1), + [COLOR_WHITE, COLOR_BG_BLUE], + ), + ] + ) + ) + + return table diff --git a/api/src/backend/tasks/jobs/threatscore_utils.py b/api/src/backend/tasks/jobs/threatscore_utils.py index c46c279bf4..7d0f2b6ec3 100644 --- a/api/src/backend/tasks/jobs/threatscore_utils.py +++ b/api/src/backend/tasks/jobs/threatscore_utils.py @@ -114,6 +114,11 @@ def _calculate_requirements_data_from_statistics( requirement_status = StatusChoices.PASS else: requirement_status = StatusChoices.FAIL + elif requirement_checks: + # Requirement has checks but none produced findings — consistent + # with the dashboard's scan processing which treats this as PASS + # (no failed checks means the requirement is considered compliant). + requirement_status = StatusChoices.PASS else: requirement_status = StatusChoices.MANUAL diff --git a/api/src/backend/tasks/tasks.py b/api/src/backend/tasks/tasks.py index 0dd3b0905b..30cc0b09c4 100644 --- a/api/src/backend/tasks/tasks.py +++ b/api/src/backend/tasks/tasks.py @@ -11,6 +11,7 @@ from django_celery_beat.models import PeriodicTask from tasks.jobs.attack_paths import ( attack_paths_scan, can_provider_run_attack_paths_scan, + db_utils as attack_paths_db_utils, ) from tasks.jobs.backfill import ( backfill_compliance_summaries, @@ -359,8 +360,25 @@ def perform_scan_summary_task(tenant_id: str, scan_id: str): return aggregate_findings(tenant_id=tenant_id, scan_id=scan_id) +class AttackPathsScanRLSTask(RLSTask): + """ + RLS task that marks the `AttackPathsScan` DB row as `FAILED` when the Celery task fails. + + Covers failures that happen outside the job's own try/except (e.g. provider lookup, + SDK initialization, or Neo4j configuration errors during setup). + """ + + def on_failure(self, exc, task_id, args, kwargs, _einfo): + tenant_id = kwargs.get("tenant_id") + scan_id = kwargs.get("scan_id") + + if tenant_id and scan_id: + logger.error(f"Attack paths scan task {task_id} failed: {exc}") + attack_paths_db_utils.fail_attack_paths_scan(tenant_id, scan_id, str(exc)) + + @shared_task( - base=RLSTask, + base=AttackPathsScanRLSTask, bind=True, name="attack-paths-scan-perform", queue="attack-paths-scans", @@ -888,11 +906,11 @@ def jira_integration_task( @handle_provider_deletion def generate_compliance_reports_task(tenant_id: str, scan_id: str, provider_id: str): """ - Optimized task to generate ThreatScore, ENS, and NIS2 reports with shared queries. + Optimized task to generate ThreatScore, ENS, NIS2, and CSA CCM reports with shared queries. This task is more efficient than running separate report tasks because it reuses database queries: - - Provider object fetched once (instead of three times) - - Requirement statistics aggregated once (instead of three times) + - Provider object fetched once (instead of multiple times) + - Requirement statistics aggregated once (instead of multiple times) - Can reduce database load by up to 50-70% Args: @@ -910,6 +928,7 @@ def generate_compliance_reports_task(tenant_id: str, scan_id: str, provider_id: generate_threatscore=True, generate_ens=True, generate_nis2=True, + generate_csa=True, ) diff --git a/api/src/backend/tasks/tests/test_attack_paths_scan.py b/api/src/backend/tasks/tests/test_attack_paths_scan.py index ebee87c981..a883360f5e 100644 --- a/api/src/backend/tasks/tests/test_attack_paths_scan.py +++ b/api/src/backend/tasks/tests/test_attack_paths_scan.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, call, patch import pytest from tasks.jobs.attack_paths import findings as findings_module +from tasks.jobs.attack_paths import internet as internet_module from tasks.jobs.attack_paths.scan import run as attack_paths_run from api.models import ( @@ -27,16 +28,15 @@ class TestAttackPathsRun: "tasks.jobs.attack_paths.scan.utils.call_within_event_loop", side_effect=lambda fn, *a, **kw: fn(*a, **kw), ) - @patch( - "tasks.jobs.attack_paths.scan.db_utils.get_old_attack_paths_scans", - return_value=[], - ) + @patch("tasks.jobs.attack_paths.scan.db_utils.set_graph_data_ready") + @patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready") @patch("tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan") @patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress") @patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan") @patch("tasks.jobs.attack_paths.scan.sync.sync_graph") @patch("tasks.jobs.attack_paths.scan.graph_database.drop_subgraph") @patch("tasks.jobs.attack_paths.scan.sync.create_sync_indexes") + @patch("tasks.jobs.attack_paths.scan.internet.analysis") @patch("tasks.jobs.attack_paths.scan.findings.analysis") @patch("tasks.jobs.attack_paths.scan.findings.create_findings_indexes") @patch("tasks.jobs.attack_paths.scan.cartography_ontology.run") @@ -67,13 +67,15 @@ class TestAttackPathsRun: mock_cartography_ontology, mock_findings_indexes, mock_findings_analysis, + mock_internet_analysis, mock_sync_indexes, mock_drop_subgraph, mock_sync, mock_starting, mock_update_progress, mock_finish, - mock_get_old_scans, + mock_set_provider_graph_data_ready, + mock_set_graph_data_ready, mock_event_loop, mock_drop_db, tenants_fixture, @@ -139,6 +141,7 @@ class TestAttackPathsRun: # These use tmp_cartography_config (neo4j_database="db-scan-id") mock_cartography_analysis.assert_called_once() mock_cartography_ontology.assert_called_once() + mock_internet_analysis.assert_called_once() mock_findings_analysis.assert_called_once() mock_drop_subgraph.assert_called_once_with( database="tenant-db", @@ -160,9 +163,66 @@ class TestAttackPathsRun: mock_finish.assert_called_once_with( attack_paths_scan, StateChoices.COMPLETED, ingestion_result ) + mock_set_provider_graph_data_ready.assert_called_once_with( + attack_paths_scan, False + ) + mock_set_graph_data_ready.assert_called_once_with(attack_paths_scan, True) + @patch( + "tasks.jobs.attack_paths.scan.utils.stringify_exception", + return_value="Cartography failed: ingestion boom", + ) + @patch( + "tasks.jobs.attack_paths.scan.utils.call_within_event_loop", + side_effect=lambda fn, *a, **kw: fn(*a, **kw), + ) + @patch("tasks.jobs.attack_paths.scan.graph_database.drop_database") + @patch("tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan") + @patch("tasks.jobs.attack_paths.scan.db_utils.set_graph_data_ready") + @patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready") + @patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress") + @patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan") + @patch("tasks.jobs.attack_paths.scan.findings.analysis") + @patch("tasks.jobs.attack_paths.scan.internet.analysis") + @patch("tasks.jobs.attack_paths.scan.findings.create_findings_indexes") + @patch("tasks.jobs.attack_paths.scan.cartography_analysis.run") + @patch("tasks.jobs.attack_paths.scan.cartography_create_indexes.run") + @patch("tasks.jobs.attack_paths.scan.graph_database.create_database") + @patch( + "tasks.jobs.attack_paths.scan.graph_database.get_database_name", + return_value="db-scan-id", + ) + @patch("tasks.jobs.attack_paths.scan.graph_database.get_uri") + @patch( + "tasks.jobs.attack_paths.scan.initialize_prowler_provider", + return_value=MagicMock(_enabled_regions=["us-east-1"]), + ) + @patch( + "tasks.jobs.attack_paths.scan.rls_transaction", + new=lambda *args, **kwargs: nullcontext(), + ) def test_run_failure_marks_scan_failed( - self, tenants_fixture, providers_fixture, scans_fixture + self, + mock_init_provider, + mock_get_uri, + mock_get_db_name, + mock_create_db, + mock_cartography_indexes, + mock_cartography_analysis, + mock_findings_indexes, + mock_internet_analysis, + mock_findings_analysis, + mock_starting, + mock_update_progress, + mock_set_provider_graph_data_ready, + mock_set_graph_data_ready, + mock_finish, + mock_drop_db, + mock_event_loop, + mock_stringify, + tenants_fixture, + providers_fixture, + scans_fixture, ): tenant = tenants_fixture[0] provider = providers_fixture[0] @@ -186,52 +246,18 @@ class TestAttackPathsRun: ingestion_fn = MagicMock(side_effect=RuntimeError("ingestion boom")) with ( - patch( - "tasks.jobs.attack_paths.scan.rls_transaction", - new=lambda *args, **kwargs: nullcontext(), - ), - patch( - "tasks.jobs.attack_paths.scan.initialize_prowler_provider", - return_value=MagicMock(_enabled_regions=["us-east-1"]), - ), - patch("tasks.jobs.attack_paths.scan.graph_database.get_uri"), - patch( - "tasks.jobs.attack_paths.scan.graph_database.get_database_name", - return_value="db-scan-id", - ), - patch("tasks.jobs.attack_paths.scan.graph_database.create_database"), patch( "tasks.jobs.attack_paths.scan.graph_database.get_session", return_value=session_ctx, ), - patch("tasks.jobs.attack_paths.scan.cartography_create_indexes.run"), - patch("tasks.jobs.attack_paths.scan.cartography_analysis.run"), - patch("tasks.jobs.attack_paths.scan.findings.create_findings_indexes"), - patch("tasks.jobs.attack_paths.scan.findings.analysis"), patch( "tasks.jobs.attack_paths.scan.db_utils.retrieve_attack_paths_scan", return_value=attack_paths_scan, ), - patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan"), - patch( - "tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress" - ), - patch( - "tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan" - ) as mock_finish, - patch("tasks.jobs.attack_paths.scan.graph_database.drop_database"), patch( "tasks.jobs.attack_paths.scan.get_cartography_ingestion_function", return_value=ingestion_fn, ), - patch( - "tasks.jobs.attack_paths.scan.utils.call_within_event_loop", - side_effect=lambda fn, *a, **kw: fn(*a, **kw), - ), - patch( - "tasks.jobs.attack_paths.scan.utils.stringify_exception", - return_value="Cartography failed: ingestion boom", - ), ): with pytest.raises(RuntimeError, match="ingestion boom"): attack_paths_run(str(tenant.id), str(scan.id), "task-456") @@ -239,9 +265,109 @@ class TestAttackPathsRun: failure_args = mock_finish.call_args[0] assert failure_args[0] is attack_paths_scan assert failure_args[1] == StateChoices.FAILED - assert failure_args[2] == { - "global_cartography_error": "Cartography failed: ingestion boom" - } + assert failure_args[2] == {"global_error": "Cartography failed: ingestion boom"} + + @patch( + "tasks.jobs.attack_paths.scan.utils.stringify_exception", + return_value="Cartography failed: ingestion boom", + ) + @patch( + "tasks.jobs.attack_paths.scan.utils.call_within_event_loop", + side_effect=lambda fn, *a, **kw: fn(*a, **kw), + ) + @patch( + "tasks.jobs.attack_paths.scan.graph_database.drop_database", + side_effect=ConnectionError("neo4j down"), + ) + @patch("tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan") + @patch("tasks.jobs.attack_paths.scan.db_utils.set_graph_data_ready") + @patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready") + @patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress") + @patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan") + @patch("tasks.jobs.attack_paths.scan.findings.analysis") + @patch("tasks.jobs.attack_paths.scan.internet.analysis") + @patch("tasks.jobs.attack_paths.scan.findings.create_findings_indexes") + @patch("tasks.jobs.attack_paths.scan.cartography_analysis.run") + @patch("tasks.jobs.attack_paths.scan.cartography_create_indexes.run") + @patch("tasks.jobs.attack_paths.scan.graph_database.create_database") + @patch( + "tasks.jobs.attack_paths.scan.graph_database.get_database_name", + return_value="db-scan-id", + ) + @patch("tasks.jobs.attack_paths.scan.graph_database.get_uri") + @patch( + "tasks.jobs.attack_paths.scan.initialize_prowler_provider", + return_value=MagicMock(_enabled_regions=["us-east-1"]), + ) + @patch( + "tasks.jobs.attack_paths.scan.rls_transaction", + new=lambda *args, **kwargs: nullcontext(), + ) + def test_run_failure_marks_scan_failed_even_when_drop_database_fails( + self, + mock_init_provider, + mock_get_uri, + mock_get_db_name, + mock_create_db, + mock_cartography_indexes, + mock_cartography_analysis, + mock_findings_indexes, + mock_internet_analysis, + mock_findings_analysis, + mock_starting, + mock_update_progress, + mock_set_provider_graph_data_ready, + mock_set_graph_data_ready, + mock_finish, + mock_drop_db, + mock_event_loop, + mock_stringify, + tenants_fixture, + providers_fixture, + scans_fixture, + ): + tenant = tenants_fixture[0] + provider = providers_fixture[0] + provider.provider = Provider.ProviderChoices.AWS + provider.save() + scan = scans_fixture[0] + scan.provider = provider + scan.save() + + attack_paths_scan = AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider, + scan=scan, + state=StateChoices.SCHEDULED, + ) + + mock_session = MagicMock() + session_ctx = MagicMock() + session_ctx.__enter__.return_value = mock_session + session_ctx.__exit__.return_value = False + ingestion_fn = MagicMock(side_effect=RuntimeError("ingestion boom")) + + with ( + patch( + "tasks.jobs.attack_paths.scan.graph_database.get_session", + return_value=session_ctx, + ), + patch( + "tasks.jobs.attack_paths.scan.db_utils.retrieve_attack_paths_scan", + return_value=attack_paths_scan, + ), + patch( + "tasks.jobs.attack_paths.scan.get_cartography_ingestion_function", + return_value=ingestion_fn, + ), + ): + with pytest.raises(RuntimeError, match="ingestion boom"): + attack_paths_run(str(tenant.id), str(scan.id), "task-789") + + failure_args = mock_finish.call_args[0] + assert failure_args[0] is attack_paths_scan + assert failure_args[1] == StateChoices.FAILED + assert failure_args[2] == {"global_error": "Cartography failed: ingestion boom"} def test_run_returns_early_for_unsupported_provider(self, tenants_fixture): tenant = tenants_fixture[0] @@ -286,6 +412,194 @@ class TestAttackPathsRun: mock_retrieve.assert_called_once_with(str(tenant.id), str(scan.id)) +@pytest.mark.django_db +class TestFailAttackPathsScan: + def test_marks_executing_scan_as_failed( + self, tenants_fixture, providers_fixture, scans_fixture + ): + from tasks.jobs.attack_paths.db_utils import ( + fail_attack_paths_scan, + ) + + tenant = tenants_fixture[0] + provider = providers_fixture[0] + provider.provider = Provider.ProviderChoices.AWS + provider.save() + scan = scans_fixture[0] + scan.provider = provider + scan.save() + + attack_paths_scan = AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider, + scan=scan, + state=StateChoices.EXECUTING, + ) + + with ( + patch( + "tasks.jobs.attack_paths.db_utils.retrieve_attack_paths_scan", + return_value=attack_paths_scan, + ) as mock_retrieve, + patch( + "tasks.jobs.attack_paths.db_utils.graph_database.drop_database" + ) as mock_drop_db, + patch( + "tasks.jobs.attack_paths.db_utils.finish_attack_paths_scan" + ) as mock_finish, + ): + fail_attack_paths_scan(str(tenant.id), str(scan.id), "setup exploded") + + mock_retrieve.assert_called_once_with(str(tenant.id), str(scan.id)) + expected_tmp_db = f"db-tmp-scan-{str(attack_paths_scan.id).lower()}" + mock_drop_db.assert_called_once_with(expected_tmp_db) + mock_finish.assert_called_once_with( + attack_paths_scan, + StateChoices.FAILED, + {"global_error": "setup exploded"}, + ) + + def test_drops_temp_database_even_when_drop_fails( + self, tenants_fixture, providers_fixture, scans_fixture + ): + from tasks.jobs.attack_paths.db_utils import ( + fail_attack_paths_scan, + ) + + tenant = tenants_fixture[0] + provider = providers_fixture[0] + provider.provider = Provider.ProviderChoices.AWS + provider.save() + scan = scans_fixture[0] + scan.provider = provider + scan.save() + + attack_paths_scan = AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider, + scan=scan, + state=StateChoices.EXECUTING, + ) + + with ( + patch( + "tasks.jobs.attack_paths.db_utils.retrieve_attack_paths_scan", + return_value=attack_paths_scan, + ), + patch( + "tasks.jobs.attack_paths.db_utils.graph_database.drop_database", + side_effect=Exception("Neo4j unreachable"), + ), + patch( + "tasks.jobs.attack_paths.db_utils.finish_attack_paths_scan" + ) as mock_finish, + ): + fail_attack_paths_scan(str(tenant.id), str(scan.id), "setup exploded") + + mock_finish.assert_called_once_with( + attack_paths_scan, + StateChoices.FAILED, + {"global_error": "setup exploded"}, + ) + + def test_skips_already_failed_scan( + self, tenants_fixture, providers_fixture, scans_fixture + ): + from tasks.jobs.attack_paths.db_utils import ( + fail_attack_paths_scan, + ) + + tenant = tenants_fixture[0] + provider = providers_fixture[0] + provider.provider = Provider.ProviderChoices.AWS + provider.save() + scan = scans_fixture[0] + scan.provider = provider + scan.save() + + attack_paths_scan = AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider, + scan=scan, + state=StateChoices.FAILED, + ) + + with ( + patch( + "tasks.jobs.attack_paths.db_utils.retrieve_attack_paths_scan", + return_value=attack_paths_scan, + ), + patch( + "tasks.jobs.attack_paths.db_utils.graph_database.drop_database" + ) as mock_drop_db, + patch( + "tasks.jobs.attack_paths.db_utils.finish_attack_paths_scan" + ) as mock_finish, + ): + fail_attack_paths_scan(str(tenant.id), str(scan.id), "setup exploded") + + mock_drop_db.assert_not_called() + mock_finish.assert_not_called() + + def test_skips_when_no_scan_found(self, tenants_fixture): + from tasks.jobs.attack_paths.db_utils import ( + fail_attack_paths_scan, + ) + + tenant = tenants_fixture[0] + + with ( + patch( + "tasks.jobs.attack_paths.db_utils.retrieve_attack_paths_scan", + return_value=None, + ), + patch( + "tasks.jobs.attack_paths.db_utils.finish_attack_paths_scan" + ) as mock_finish, + ): + fail_attack_paths_scan(str(tenant.id), "nonexistent", "setup exploded") + + mock_finish.assert_not_called() + + +class TestAttackPathsScanRLSTaskOnFailure: + def test_on_failure_delegates_to_fail_attack_paths_scan(self): + from tasks.tasks import AttackPathsScanRLSTask + + task = AttackPathsScanRLSTask() + + with patch( + "tasks.tasks.attack_paths_db_utils.fail_attack_paths_scan" + ) as mock_fail: + task.on_failure( + exc=RuntimeError("boom"), + task_id="task-abc", + args=(), + kwargs={"tenant_id": "t-1", "scan_id": "s-1"}, + _einfo=None, + ) + + mock_fail.assert_called_once_with("t-1", "s-1", "boom") + + def test_on_failure_skips_when_missing_kwargs(self): + from tasks.tasks import AttackPathsScanRLSTask + + task = AttackPathsScanRLSTask() + + with patch( + "tasks.tasks.attack_paths_db_utils.fail_attack_paths_scan" + ) as mock_fail: + task.on_failure( + exc=RuntimeError("boom"), + task_id="task-abc", + args=(), + kwargs={}, + _einfo=None, + ) + + mock_fail.assert_not_called() + + @pytest.mark.django_db class TestAttackPathsFindingsHelpers: def test_create_findings_indexes_executes_all_statements(self): @@ -757,3 +1071,359 @@ class TestAttackPathsFindingsHelpers: findings_module.load_findings(mock_session, empty_gen(), provider, config) mock_session.run.assert_not_called() + + +class TestInternetAnalysis: + def _make_provider_and_config(self): + provider = MagicMock() + provider.provider = "aws" + provider.uid = "123456789012" + config = SimpleNamespace(update_tag=1234567890) + return provider, config + + def test_analysis_creates_node_and_relationships(self): + """Verify both Cypher statements are executed and relationship count returned.""" + mock_session = MagicMock() + mock_result = MagicMock() + mock_result.single.return_value = {"relationships_merged": 3} + mock_session.run.side_effect = [None, mock_result] + provider, config = self._make_provider_and_config() + + with patch( + "tasks.jobs.attack_paths.internet.get_root_node_label", + return_value="AWSAccount", + ): + result = internet_module.analysis(mock_session, provider, config) + + assert mock_session.run.call_count == 2 + assert result == 3 + + def test_analysis_zero_exposed_resources(self): + """When no resources are exposed, zero relationships are created.""" + mock_session = MagicMock() + mock_result = MagicMock() + mock_result.single.return_value = {"relationships_merged": 0} + mock_session.run.side_effect = [None, mock_result] + provider, config = self._make_provider_and_config() + + with patch( + "tasks.jobs.attack_paths.internet.get_root_node_label", + return_value="AWSAccount", + ): + result = internet_module.analysis(mock_session, provider, config) + + assert result == 0 + + +@pytest.mark.django_db +class TestAttackPathsDbUtilsGraphDataReady: + """Tests for db_utils functions related to graph_data_ready lifecycle.""" + + def test_create_attack_paths_scan_first_scan_defaults_to_false( + self, tenants_fixture, providers_fixture, scans_fixture + ): + from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan + + tenant = tenants_fixture[0] + provider = providers_fixture[0] + provider.provider = Provider.ProviderChoices.AWS + provider.save() + scan = scans_fixture[0] + scan.provider = provider + scan.save() + + with patch( + "tasks.jobs.attack_paths.db_utils.rls_transaction", + new=lambda *args, **kwargs: nullcontext(), + ): + attack_paths_scan = create_attack_paths_scan( + str(tenant.id), str(scan.id), provider.id + ) + + assert attack_paths_scan is not None + assert attack_paths_scan.graph_data_ready is False + + def test_create_attack_paths_scan_inherits_true_from_previous( + self, tenants_fixture, providers_fixture, scans_fixture + ): + from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan + + tenant = tenants_fixture[0] + provider = providers_fixture[0] + provider.provider = Provider.ProviderChoices.AWS + provider.save() + scan = scans_fixture[0] + scan.provider = provider + scan.save() + + AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider, + scan=scan, + state=StateChoices.COMPLETED, + graph_data_ready=True, + ) + + new_scan = Scan.objects.create( + name="New Scan", + provider=provider, + trigger=Scan.TriggerChoices.MANUAL, + state=StateChoices.AVAILABLE, + tenant_id=tenant.id, + ) + + with patch( + "tasks.jobs.attack_paths.db_utils.rls_transaction", + new=lambda *args, **kwargs: nullcontext(), + ): + attack_paths_scan = create_attack_paths_scan( + str(tenant.id), str(new_scan.id), provider.id + ) + + assert attack_paths_scan is not None + assert attack_paths_scan.graph_data_ready is True + + def test_create_attack_paths_scan_inherits_false_when_no_previous_ready( + self, tenants_fixture, providers_fixture, scans_fixture + ): + from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan + + tenant = tenants_fixture[0] + provider = providers_fixture[0] + provider.provider = Provider.ProviderChoices.AWS + provider.save() + scan = scans_fixture[0] + scan.provider = provider + scan.save() + + AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider, + scan=scan, + state=StateChoices.FAILED, + graph_data_ready=False, + ) + + new_scan = Scan.objects.create( + name="New Scan", + provider=provider, + trigger=Scan.TriggerChoices.MANUAL, + state=StateChoices.AVAILABLE, + tenant_id=tenant.id, + ) + + with patch( + "tasks.jobs.attack_paths.db_utils.rls_transaction", + new=lambda *args, **kwargs: nullcontext(), + ): + attack_paths_scan = create_attack_paths_scan( + str(tenant.id), str(new_scan.id), provider.id + ) + + assert attack_paths_scan is not None + assert attack_paths_scan.graph_data_ready is False + + def test_set_graph_data_ready_updates_field( + self, tenants_fixture, providers_fixture, scans_fixture + ): + from tasks.jobs.attack_paths.db_utils import set_graph_data_ready + + tenant = tenants_fixture[0] + provider = providers_fixture[0] + provider.provider = Provider.ProviderChoices.AWS + provider.save() + scan = scans_fixture[0] + scan.provider = provider + scan.save() + + attack_paths_scan = AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider, + scan=scan, + state=StateChoices.EXECUTING, + graph_data_ready=True, + ) + + with patch( + "tasks.jobs.attack_paths.db_utils.rls_transaction", + new=lambda *args, **kwargs: nullcontext(), + ): + set_graph_data_ready(attack_paths_scan, False) + + attack_paths_scan.refresh_from_db() + assert attack_paths_scan.graph_data_ready is False + + with patch( + "tasks.jobs.attack_paths.db_utils.rls_transaction", + new=lambda *args, **kwargs: nullcontext(), + ): + set_graph_data_ready(attack_paths_scan, True) + + attack_paths_scan.refresh_from_db() + assert attack_paths_scan.graph_data_ready is True + + def test_finish_attack_paths_scan_does_not_modify_graph_data_ready( + self, tenants_fixture, providers_fixture, scans_fixture + ): + from tasks.jobs.attack_paths.db_utils import finish_attack_paths_scan + + tenant = tenants_fixture[0] + provider = providers_fixture[0] + provider.provider = Provider.ProviderChoices.AWS + provider.save() + scan = scans_fixture[0] + scan.provider = provider + scan.save() + + attack_paths_scan = AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider, + scan=scan, + state=StateChoices.EXECUTING, + graph_data_ready=True, + ) + + with patch( + "tasks.jobs.attack_paths.db_utils.rls_transaction", + new=lambda *args, **kwargs: nullcontext(), + ): + finish_attack_paths_scan(attack_paths_scan, StateChoices.COMPLETED, {}) + + attack_paths_scan.refresh_from_db() + assert attack_paths_scan.state == StateChoices.COMPLETED + assert attack_paths_scan.graph_data_ready is True + + def test_finish_attack_paths_scan_preserves_graph_data_ready_on_failure( + self, tenants_fixture, providers_fixture, scans_fixture + ): + from tasks.jobs.attack_paths.db_utils import finish_attack_paths_scan + + tenant = tenants_fixture[0] + provider = providers_fixture[0] + provider.provider = Provider.ProviderChoices.AWS + provider.save() + scan = scans_fixture[0] + scan.provider = provider + scan.save() + + attack_paths_scan = AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider, + scan=scan, + state=StateChoices.EXECUTING, + graph_data_ready=True, + ) + + with patch( + "tasks.jobs.attack_paths.db_utils.rls_transaction", + new=lambda *args, **kwargs: nullcontext(), + ): + finish_attack_paths_scan( + attack_paths_scan, + StateChoices.FAILED, + {"global_error": "boom"}, + ) + + attack_paths_scan.refresh_from_db() + assert attack_paths_scan.state == StateChoices.FAILED + assert attack_paths_scan.graph_data_ready is True + + def test_set_provider_graph_data_ready_updates_all_scans_for_provider( + self, tenants_fixture, providers_fixture, scans_fixture + ): + from tasks.jobs.attack_paths.db_utils import set_provider_graph_data_ready + + tenant = tenants_fixture[0] + provider = providers_fixture[0] + provider.provider = Provider.ProviderChoices.AWS + provider.save() + + scan_a = scans_fixture[0] + scan_a.provider = provider + scan_a.save() + + scan_b = Scan.objects.create( + name="Second Scan", + provider=provider, + trigger=Scan.TriggerChoices.MANUAL, + state=StateChoices.AVAILABLE, + tenant_id=tenant.id, + ) + + old_ap_scan = AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider, + scan=scan_a, + state=StateChoices.COMPLETED, + graph_data_ready=True, + ) + new_ap_scan = AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider, + scan=scan_b, + state=StateChoices.EXECUTING, + graph_data_ready=True, + ) + + with patch( + "tasks.jobs.attack_paths.db_utils.rls_transaction", + new=lambda *args, **kwargs: nullcontext(), + ): + set_provider_graph_data_ready(new_ap_scan, False) + + old_ap_scan.refresh_from_db() + new_ap_scan.refresh_from_db() + assert old_ap_scan.graph_data_ready is False + assert new_ap_scan.graph_data_ready is False + + def test_set_provider_graph_data_ready_does_not_affect_other_providers( + self, tenants_fixture, providers_fixture, scans_fixture + ): + from tasks.jobs.attack_paths.db_utils import set_provider_graph_data_ready + + tenant = tenants_fixture[0] + provider_a = providers_fixture[0] + provider_a.provider = Provider.ProviderChoices.AWS + provider_a.save() + + provider_b = providers_fixture[1] + provider_b.provider = Provider.ProviderChoices.AWS + provider_b.save() + + scan_a = scans_fixture[0] + scan_a.provider = provider_a + scan_a.save() + + scan_b = Scan.objects.create( + name="Scan for provider B", + provider=provider_b, + trigger=Scan.TriggerChoices.MANUAL, + state=StateChoices.COMPLETED, + tenant_id=tenant.id, + ) + + ap_scan_a = AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider_a, + scan=scan_a, + state=StateChoices.EXECUTING, + graph_data_ready=True, + ) + ap_scan_b = AttackPathsScan.objects.create( + tenant_id=tenant.id, + provider=provider_b, + scan=scan_b, + state=StateChoices.COMPLETED, + graph_data_ready=True, + ) + + with patch( + "tasks.jobs.attack_paths.db_utils.rls_transaction", + new=lambda *args, **kwargs: nullcontext(), + ): + set_provider_graph_data_ready(ap_scan_a, False) + + ap_scan_a.refresh_from_db() + ap_scan_b.refresh_from_db() + assert ap_scan_a.graph_data_ready is False + assert ap_scan_b.graph_data_ready is True diff --git a/api/src/backend/tasks/tests/test_deletion.py b/api/src/backend/tasks/tests/test_deletion.py index 843ccb5df8..831ba20641 100644 --- a/api/src/backend/tasks/tests/test_deletion.py +++ b/api/src/backend/tasks/tests/test_deletion.py @@ -4,6 +4,7 @@ import pytest from django.core.exceptions import ObjectDoesNotExist +from api.attack_paths import database as graph_database from api.models import Provider, Tenant from tasks.jobs.deletion import delete_provider, delete_tenant @@ -47,14 +48,61 @@ class TestDeleteProvider: tenant_id = str(tenants_fixture[0].id) non_existent_pk = "babf6796-cfcc-4fd3-9dcf-88d012247645" - with pytest.raises(ObjectDoesNotExist): - delete_provider(tenant_id, non_existent_pk) + result = delete_provider(tenant_id, non_existent_pk) - mock_get_database_name.assert_called_once_with(tenant_id) - mock_drop_subgraph.assert_called_once_with( - "tenant-db", - non_existent_pk, - ) + assert result == {} + mock_get_database_name.assert_not_called() + mock_drop_subgraph.assert_not_called() + + def test_delete_provider_drops_temp_attack_paths_databases( + self, providers_fixture, create_attack_paths_scan + ): + instance = providers_fixture[0] + tenant_id = str(instance.tenant_id) + + aps1 = create_attack_paths_scan(instance) + aps2 = create_attack_paths_scan(instance) + + with ( + patch( + "tasks.jobs.deletion.graph_database.drop_subgraph", + ), + patch( + "tasks.jobs.deletion.graph_database.drop_database", + ) as mock_drop_database, + ): + result = delete_provider(tenant_id, instance.id) + + assert result + expected_tmp_calls = [ + call(f"db-tmp-scan-{str(aps1.id).lower()}"), + call(f"db-tmp-scan-{str(aps2.id).lower()}"), + ] + mock_drop_database.assert_has_calls(expected_tmp_calls, any_order=True) + + def test_delete_provider_continues_when_temp_db_drop_fails( + self, providers_fixture, create_attack_paths_scan + ): + instance = providers_fixture[0] + tenant_id = str(instance.tenant_id) + + create_attack_paths_scan(instance) + + with ( + patch( + "tasks.jobs.deletion.graph_database.drop_subgraph", + ), + patch( + "tasks.jobs.deletion.graph_database.drop_database", + side_effect=graph_database.GraphDatabaseQueryException( + "Neo4j unreachable" + ), + ), + ): + result = delete_provider(tenant_id, instance.id) + + assert result + assert not Provider.all_objects.filter(pk=instance.id).exists() @pytest.mark.django_db @@ -142,3 +190,56 @@ class TestDeleteTenant: mock_get_database_name.assert_called_once_with(tenant.id) mock_drop_subgraph.assert_not_called() mock_drop_database.assert_called_once_with("tenant-db") + + def test_delete_tenant_includes_soft_deleted_providers(self, tenants_fixture): + tenant = tenants_fixture[0] + provider = Provider.objects.create( + provider="aws", + uid="999999999999", + alias="soft_deleted_provider", + tenant_id=tenant.id, + ) + # Soft-delete the provider so ActiveProviderManager would skip it + Provider.all_objects.filter(pk=provider.id).update(is_deleted=True) + + with ( + patch( + "tasks.jobs.deletion.graph_database.get_database_name", + return_value="tenant-db", + ), + patch( + "tasks.jobs.deletion.graph_database.drop_subgraph" + ) as mock_drop_subgraph, + patch("tasks.jobs.deletion.graph_database.drop_database"), + ): + delete_tenant(tenant.id) + + mock_drop_subgraph.assert_any_call("tenant-db", str(provider.id)) + + def test_delete_tenant_handles_concurrently_deleted_provider(self, tenants_fixture): + tenant = tenants_fixture[0] + Provider.objects.create( + provider="aws", + uid="111111111111", + alias="vanishing_provider", + tenant_id=tenant.id, + ) + + def drop_subgraph_side_effect(_db_name, provider_id): + # Simulate concurrent deletion by another process + Provider.all_objects.filter(pk=provider_id).delete() + + with ( + patch( + "tasks.jobs.deletion.graph_database.get_database_name", + return_value="tenant-db", + ), + patch( + "tasks.jobs.deletion.graph_database.drop_subgraph", + side_effect=drop_subgraph_side_effect, + ), + patch("tasks.jobs.deletion.graph_database.drop_database"), + ): + deletion_summary = delete_tenant(tenant.id) + + assert deletion_summary is not None diff --git a/api/src/backend/tasks/tests/test_reports_csa.py b/api/src/backend/tasks/tests/test_reports_csa.py new file mode 100644 index 0000000000..602b9bb28e --- /dev/null +++ b/api/src/backend/tasks/tests/test_reports_csa.py @@ -0,0 +1,1085 @@ +import io +from unittest.mock import Mock + +import pytest +from reportlab.platypus import PageBreak, Paragraph, Table +from tasks.jobs.reports import FRAMEWORK_REGISTRY, ComplianceData, RequirementData +from tasks.jobs.reports.csa import CSAReportGenerator + + +# Use string status values directly to avoid Django DB initialization +# These match api.models.StatusChoices values +class StatusChoices: + """Mock StatusChoices to avoid Django DB initialization.""" + + PASS = "PASS" + FAIL = "FAIL" + MANUAL = "MANUAL" + + +# ============================================================================= +# Fixtures +# ============================================================================= + + +@pytest.fixture +def csa_generator(): + """Create a CSAReportGenerator instance for testing.""" + config = FRAMEWORK_REGISTRY["csa_ccm"] + return CSAReportGenerator(config) + + +@pytest.fixture +def mock_csa_requirement_attribute_iam(): + """Create a mock CSA CCM requirement attribute for Identity & Access Management.""" + mock = Mock() + mock.Section = "Identity & Access Management" + mock.CCMLite = "Yes" + mock.IaaS = "Yes" + mock.PaaS = "Yes" + mock.SaaS = "Yes" + mock.ScopeApplicability = [ + {"ReferenceId": "ISO 27001", "Identifiers": ["A.9.1.1", "A.9.2.3"]}, + {"ReferenceId": "NIST 800-53", "Identifiers": ["AC-2", "AC-3", "AC-6"]}, + ] + return mock + + +@pytest.fixture +def mock_csa_requirement_attribute_logging(): + """Create a mock CSA CCM requirement attribute for Logging and Monitoring.""" + mock = Mock() + mock.Section = "Logging and Monitoring" + mock.CCMLite = "Yes" + mock.IaaS = "Yes" + mock.PaaS = "No" + mock.SaaS = "No" + mock.ScopeApplicability = [ + {"ReferenceId": "ISO 27001", "Identifiers": ["A.12.4.1"]}, + ] + return mock + + +@pytest.fixture +def mock_csa_requirement_attribute_crypto(): + """Create a mock CSA CCM requirement attribute for Cryptography.""" + mock = Mock() + mock.Section = "Cryptography, Encryption & Key Management" + mock.CCMLite = "No" + mock.IaaS = "Yes" + mock.PaaS = "Yes" + mock.SaaS = "No" + mock.ScopeApplicability = [] + return mock + + +@pytest.fixture +def basic_csa_compliance_data(): + """Create basic ComplianceData for CSA CCM testing.""" + return ComplianceData( + tenant_id="tenant-123", + scan_id="scan-456", + provider_id="provider-789", + compliance_id="csa_ccm_4.0_aws", + framework="CSA-CCM", + name="CSA Cloud Controls Matrix v4.0", + version="4.0", + description="Cloud Security Alliance Cloud Controls Matrix", + ) + + +# ============================================================================= +# Generator Initialization Tests +# ============================================================================= + + +class TestCSAGeneratorInitialization: + """Test suite for CSA generator initialization.""" + + def test_generator_creation(self, csa_generator): + """Test that CSA generator is created correctly.""" + assert csa_generator is not None + assert csa_generator.config.name == "csa_ccm" + assert csa_generator.config.language == "en" + + def test_generator_no_niveles(self, csa_generator): + """Test that CSA config does not use niveles.""" + assert csa_generator.config.has_niveles is False + + def test_generator_no_dimensions(self, csa_generator): + """Test that CSA config does not use dimensions.""" + assert csa_generator.config.has_dimensions is False + + def test_generator_no_risk_levels(self, csa_generator): + """Test that CSA config does not use risk levels.""" + assert csa_generator.config.has_risk_levels is False + + def test_generator_no_weight(self, csa_generator): + """Test that CSA config does not use weight.""" + assert csa_generator.config.has_weight is False + + +# ============================================================================= +# Cover Page Tests +# ============================================================================= + + +class TestCSACoverPage: + """Test suite for CSA cover page generation.""" + + def test_cover_page_has_logo(self, csa_generator, basic_csa_compliance_data): + """Test that cover page contains the Prowler logo.""" + basic_csa_compliance_data.requirements = [] + basic_csa_compliance_data.attributes_by_requirement_id = {} + + elements = csa_generator.create_cover_page(basic_csa_compliance_data) + + assert len(elements) > 0 + + def test_cover_page_has_title(self, csa_generator, basic_csa_compliance_data): + """Test that cover page contains the CSA CCM title.""" + basic_csa_compliance_data.requirements = [] + basic_csa_compliance_data.attributes_by_requirement_id = {} + + elements = csa_generator.create_cover_page(basic_csa_compliance_data) + + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + content = " ".join(str(p.text) for p in paragraphs) + assert "CSA" in content or "CCM" in content or "Cloud Controls" in content + + def test_cover_page_has_metadata_table( + self, csa_generator, basic_csa_compliance_data + ): + """Test that cover page contains metadata table.""" + basic_csa_compliance_data.requirements = [] + basic_csa_compliance_data.attributes_by_requirement_id = {} + + elements = csa_generator.create_cover_page(basic_csa_compliance_data) + + tables = [e for e in elements if isinstance(e, Table)] + assert len(tables) >= 1 + + +# ============================================================================= +# Executive Summary Tests +# ============================================================================= + + +class TestCSAExecutiveSummary: + """Test suite for CSA executive summary generation.""" + + def test_executive_summary_has_english_title( + self, csa_generator, basic_csa_compliance_data + ): + """Test that executive summary has English title.""" + basic_csa_compliance_data.requirements = [] + basic_csa_compliance_data.attributes_by_requirement_id = {} + + elements = csa_generator.create_executive_summary(basic_csa_compliance_data) + + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + content = " ".join(str(p.text) for p in paragraphs) + assert "Executive Summary" in content + + def test_executive_summary_calculates_compliance( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test that executive summary calculates compliance percentage.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="Passed requirement", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + RequirementData( + id="REQ-002", + description="Failed requirement", + status=StatusChoices.FAIL, + passed_findings=0, + failed_findings=10, + total_findings=10, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + "REQ-002": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + } + + elements = csa_generator.create_executive_summary(basic_csa_compliance_data) + + # Should contain tables with metrics + tables = [e for e in elements if isinstance(e, Table)] + assert len(tables) >= 1 + + def test_executive_summary_shows_all_statuses( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test that executive summary shows passed, failed, and manual counts.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="Passed", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + RequirementData( + id="REQ-002", + description="Failed", + status=StatusChoices.FAIL, + passed_findings=0, + failed_findings=10, + total_findings=10, + ), + RequirementData( + id="REQ-003", + description="Manual", + status=StatusChoices.MANUAL, + passed_findings=0, + failed_findings=0, + total_findings=0, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + "REQ-002": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + "REQ-003": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + } + + elements = csa_generator.create_executive_summary(basic_csa_compliance_data) + + # Should have a summary table with all statuses + assert len(elements) > 0 + + def test_executive_summary_excludes_manual_from_percentage( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test that manual requirements are excluded from compliance percentage.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="Passed", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + RequirementData( + id="REQ-002", + description="Manual", + status=StatusChoices.MANUAL, + passed_findings=0, + failed_findings=0, + total_findings=0, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + "REQ-002": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + } + + elements = csa_generator.create_executive_summary(basic_csa_compliance_data) + + # Should calculate 100% (only 1 evaluated requirement that passed) + assert len(elements) > 0 + + +# ============================================================================= +# Charts Section Tests +# ============================================================================= + + +class TestCSAChartsSection: + """Test suite for CSA charts section generation.""" + + def test_charts_section_has_section_chart_title( + self, csa_generator, basic_csa_compliance_data + ): + """Test that charts section has section compliance title.""" + basic_csa_compliance_data.requirements = [] + basic_csa_compliance_data.attributes_by_requirement_id = {} + + elements = csa_generator.create_charts_section(basic_csa_compliance_data) + + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + content = " ".join(str(p.text) for p in paragraphs) + assert "Section" in content or "Compliance" in content + + def test_charts_section_has_page_break( + self, csa_generator, basic_csa_compliance_data + ): + """Test that charts section has page breaks.""" + basic_csa_compliance_data.requirements = [] + basic_csa_compliance_data.attributes_by_requirement_id = {} + + elements = csa_generator.create_charts_section(basic_csa_compliance_data) + + page_breaks = [e for e in elements if isinstance(e, PageBreak)] + assert len(page_breaks) >= 1 + + def test_charts_section_has_section_breakdown( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test that charts section includes section breakdown table.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="Test requirement", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + } + + elements = csa_generator.create_charts_section(basic_csa_compliance_data) + + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + content = " ".join(str(p.text) for p in paragraphs) + assert "Section" in content or "Breakdown" in content + + +# ============================================================================= +# Section Chart Tests +# ============================================================================= + + +class TestCSASectionChart: + """Test suite for CSA section compliance chart.""" + + def test_section_chart_creation( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test that section chart is created successfully.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="Test requirement", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + } + + chart_buffer = csa_generator._create_section_chart(basic_csa_compliance_data) + + assert isinstance(chart_buffer, io.BytesIO) + assert chart_buffer.getvalue() # Not empty + + def test_section_chart_excludes_manual( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test that manual requirements are excluded from section chart.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="Auto requirement", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + RequirementData( + id="REQ-002", + description="Manual requirement", + status=StatusChoices.MANUAL, + passed_findings=0, + failed_findings=0, + total_findings=0, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + "REQ-002": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + } + + # Should not raise any errors + chart_buffer = csa_generator._create_section_chart(basic_csa_compliance_data) + assert isinstance(chart_buffer, io.BytesIO) + + def test_section_chart_multiple_sections( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + mock_csa_requirement_attribute_logging, + mock_csa_requirement_attribute_crypto, + ): + """Test section chart with multiple sections.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="IAM requirement", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + RequirementData( + id="REQ-002", + description="Logging requirement", + status=StatusChoices.FAIL, + passed_findings=0, + failed_findings=10, + total_findings=10, + ), + RequirementData( + id="REQ-003", + description="Crypto requirement", + status=StatusChoices.PASS, + passed_findings=5, + failed_findings=0, + total_findings=5, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + "REQ-002": { + "attributes": { + "req_attributes": [mock_csa_requirement_attribute_logging] + } + }, + "REQ-003": { + "attributes": { + "req_attributes": [mock_csa_requirement_attribute_crypto] + } + }, + } + + chart_buffer = csa_generator._create_section_chart(basic_csa_compliance_data) + assert isinstance(chart_buffer, io.BytesIO) + + +# ============================================================================= +# Section Table Tests +# ============================================================================= + + +class TestCSASectionTable: + """Test suite for CSA section breakdown table.""" + + def test_section_table_creation( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test that section table is created successfully.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="Test requirement", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + } + + table = csa_generator._create_section_table(basic_csa_compliance_data) + + assert isinstance(table, Table) + + def test_section_table_counts_statuses( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test that section table counts passed, failed, and manual.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="Passed", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + RequirementData( + id="REQ-002", + description="Failed", + status=StatusChoices.FAIL, + passed_findings=0, + failed_findings=10, + total_findings=10, + ), + RequirementData( + id="REQ-003", + description="Manual", + status=StatusChoices.MANUAL, + passed_findings=0, + failed_findings=0, + total_findings=0, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + "REQ-002": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + "REQ-003": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + } + + table = csa_generator._create_section_table(basic_csa_compliance_data) + assert isinstance(table, Table) + + +# ============================================================================= +# Requirements Index Tests +# ============================================================================= + + +class TestCSARequirementsIndex: + """Test suite for CSA requirements index generation.""" + + def test_requirements_index_has_title( + self, csa_generator, basic_csa_compliance_data + ): + """Test that requirements index has English title.""" + basic_csa_compliance_data.requirements = [] + basic_csa_compliance_data.attributes_by_requirement_id = {} + + elements = csa_generator.create_requirements_index(basic_csa_compliance_data) + + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + content = " ".join(str(p.text) for p in paragraphs) + assert "Requirements Index" in content + + def test_requirements_index_organized_by_section( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + mock_csa_requirement_attribute_logging, + ): + """Test that requirements index is organized by section.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="IAM requirement", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + RequirementData( + id="REQ-002", + description="Logging requirement", + status=StatusChoices.PASS, + passed_findings=5, + failed_findings=0, + total_findings=5, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + "REQ-002": { + "attributes": { + "req_attributes": [mock_csa_requirement_attribute_logging] + } + }, + } + + elements = csa_generator.create_requirements_index(basic_csa_compliance_data) + + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + content = " ".join(str(p.text) for p in paragraphs) + # Should have section headers + assert "Identity" in content or "Logging" in content or "Access" in content + + def test_requirements_index_shows_status_indicators( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test that requirements index shows pass/fail/manual indicators.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="Passed requirement", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + RequirementData( + id="REQ-002", + description="Failed requirement", + status=StatusChoices.FAIL, + passed_findings=0, + failed_findings=10, + total_findings=10, + ), + RequirementData( + id="REQ-003", + description="Manual requirement", + status=StatusChoices.MANUAL, + passed_findings=0, + failed_findings=0, + total_findings=0, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + "REQ-002": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + "REQ-003": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + } + + elements = csa_generator.create_requirements_index(basic_csa_compliance_data) + + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + content = " ".join(str(p.text) for p in paragraphs) + # Should have status indicators + assert "\u2713" in content or "\u2717" in content or "\u2299" in content + + def test_requirements_index_truncates_long_descriptions( + self, csa_generator, basic_csa_compliance_data + ): + """Test that long descriptions are truncated.""" + mock_attr = Mock() + mock_attr.Section = "Identity & Access Management" + mock_attr.CCMLite = "Yes" + mock_attr.IaaS = "Yes" + mock_attr.PaaS = "Yes" + mock_attr.SaaS = "Yes" + mock_attr.ScopeApplicability = [] + + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="A" * 100, + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": {"attributes": {"req_attributes": [mock_attr]}}, + } + + # Should not raise errors + elements = csa_generator.create_requirements_index(basic_csa_compliance_data) + assert len(elements) > 0 + + +# ============================================================================= +# Requirement Attributes Tests +# ============================================================================= + + +class TestCSARequirementAttributes: + """Test suite for CSA requirement attributes display.""" + + def test_format_attributes_applicability_line( + self, csa_generator, mock_csa_requirement_attribute_iam + ): + """Test that applicability attributes (CCMLite, IaaS, PaaS, SaaS) are rendered.""" + elements = csa_generator._format_requirement_attributes( + mock_csa_requirement_attribute_iam + ) + + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + content = " ".join(str(p.text) for p in paragraphs) + assert "CCMLite: Yes" in content + assert "IaaS: Yes" in content + assert "PaaS: Yes" in content + assert "SaaS: Yes" in content + + def test_format_attributes_partial_applicability( + self, csa_generator, mock_csa_requirement_attribute_logging + ): + """Test attributes when some applicability fields are 'No'.""" + elements = csa_generator._format_requirement_attributes( + mock_csa_requirement_attribute_logging + ) + + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + content = " ".join(str(p.text) for p in paragraphs) + assert "IaaS: Yes" in content + assert "PaaS: No" in content + + def test_format_attributes_scope_applicability_refs( + self, csa_generator, mock_csa_requirement_attribute_iam + ): + """Test that ScopeApplicability references are displayed.""" + elements = csa_generator._format_requirement_attributes( + mock_csa_requirement_attribute_iam + ) + + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + content = " ".join(str(p.text) for p in paragraphs) + assert "ISO 27001" in content + assert "NIST 800-53" in content + + def test_format_attributes_empty_scope( + self, csa_generator, mock_csa_requirement_attribute_crypto + ): + """Test that empty ScopeApplicability produces no reference line.""" + elements = csa_generator._format_requirement_attributes( + mock_csa_requirement_attribute_crypto + ) + + # Should have applicability line but no scope reference line + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + assert len(paragraphs) == 1 # Only the applicability line + + def test_format_attributes_no_applicability(self, csa_generator): + """Test attributes when all applicability fields are empty.""" + mock = Mock() + mock.CCMLite = "" + mock.IaaS = "" + mock.PaaS = "" + mock.SaaS = "" + mock.ScopeApplicability = [] + + elements = csa_generator._format_requirement_attributes(mock) + + assert len(elements) == 0 + + def test_format_attributes_truncates_long_identifiers(self, csa_generator): + """Test that ScopeApplicability with many identifiers is truncated.""" + mock = Mock() + mock.CCMLite = "Yes" + mock.IaaS = "Yes" + mock.PaaS = "Yes" + mock.SaaS = "Yes" + mock.ScopeApplicability = [ + { + "ReferenceId": "NIST 800-53", + "Identifiers": ["AC-1", "AC-2", "AC-3", "AC-4", "AC-5", "AC-6"], + }, + ] + + elements = csa_generator._format_requirement_attributes(mock) + + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + content = " ".join(str(p.text) for p in paragraphs) + # Should show first 4 and ellipsis + assert "AC-1" in content + assert "AC-4" in content + assert "..." in content + + def test_attr_style_returns_paragraph_style(self, csa_generator): + """Test that _attr_style returns a valid ParagraphStyle.""" + from reportlab.lib.styles import ParagraphStyle + + style = csa_generator._attr_style() + assert isinstance(style, ParagraphStyle) + assert style.fontSize == 10 + assert style.leftIndent == 30 + + def test_render_requirement_detail_extras( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test that detail extras hook renders CSA attributes.""" + req = RequirementData( + id="REQ-001", + description="IAM requirement", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ) + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + } + + elements = csa_generator._render_requirement_detail_extras( + req, basic_csa_compliance_data + ) + + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + content = " ".join(str(p.text) for p in paragraphs) + assert "CCMLite" in content + assert "ISO 27001" in content + + def test_render_requirement_detail_extras_no_metadata( + self, + csa_generator, + basic_csa_compliance_data, + ): + """Test that detail extras returns empty when no metadata found.""" + req = RequirementData( + id="REQ-UNKNOWN", + description="No metadata", + status=StatusChoices.PASS, + passed_findings=0, + failed_findings=0, + total_findings=0, + ) + basic_csa_compliance_data.attributes_by_requirement_id = {} + + elements = csa_generator._render_requirement_detail_extras( + req, basic_csa_compliance_data + ) + + assert elements == [] + + +# ============================================================================= +# Empty Data Tests +# ============================================================================= + + +class TestCSAEmptyData: + """Test suite for CSA with empty or minimal data.""" + + def test_executive_summary_empty_requirements( + self, csa_generator, basic_csa_compliance_data + ): + """Test executive summary with no requirements.""" + basic_csa_compliance_data.requirements = [] + basic_csa_compliance_data.attributes_by_requirement_id = {} + + elements = csa_generator.create_executive_summary(basic_csa_compliance_data) + + assert len(elements) > 0 + + def test_charts_section_empty_requirements( + self, csa_generator, basic_csa_compliance_data + ): + """Test charts section with no requirements.""" + basic_csa_compliance_data.requirements = [] + basic_csa_compliance_data.attributes_by_requirement_id = {} + + elements = csa_generator.create_charts_section(basic_csa_compliance_data) + + assert len(elements) > 0 + + def test_requirements_index_empty(self, csa_generator, basic_csa_compliance_data): + """Test requirements index with no requirements.""" + basic_csa_compliance_data.requirements = [] + basic_csa_compliance_data.attributes_by_requirement_id = {} + + elements = csa_generator.create_requirements_index(basic_csa_compliance_data) + + # Should at least have the title + assert len(elements) >= 1 + + +# ============================================================================= +# All Pass / All Fail Tests +# ============================================================================= + + +class TestCSAEdgeCases: + """Test suite for CSA edge cases.""" + + def test_all_requirements_pass( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test with all requirements passing.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id=f"REQ-{i:03d}", + description=f"Passing requirement {i}", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ) + for i in range(1, 6) + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + f"REQ-{i:03d}": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + } + for i in range(1, 6) + } + + elements = csa_generator.create_executive_summary(basic_csa_compliance_data) + assert len(elements) > 0 + + def test_all_requirements_fail( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test with all requirements failing.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id=f"REQ-{i:03d}", + description=f"Failing requirement {i}", + status=StatusChoices.FAIL, + passed_findings=0, + failed_findings=10, + total_findings=10, + ) + for i in range(1, 6) + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + f"REQ-{i:03d}": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + } + for i in range(1, 6) + } + + elements = csa_generator.create_executive_summary(basic_csa_compliance_data) + assert len(elements) > 0 + + def test_all_requirements_manual( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + ): + """Test with all requirements being manual.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id=f"REQ-{i:03d}", + description=f"Manual requirement {i}", + status=StatusChoices.MANUAL, + passed_findings=0, + failed_findings=0, + total_findings=0, + ) + for i in range(1, 6) + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + f"REQ-{i:03d}": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + } + for i in range(1, 6) + } + + # Should handle gracefully - compliance should be 100% when no evaluated + elements = csa_generator.create_executive_summary(basic_csa_compliance_data) + assert len(elements) > 0 + + +# ============================================================================= +# Integration Tests +# ============================================================================= + + +class TestCSAIntegration: + """Integration tests for CSA report generation.""" + + def test_full_report_generation_flow( + self, + csa_generator, + basic_csa_compliance_data, + mock_csa_requirement_attribute_iam, + mock_csa_requirement_attribute_logging, + ): + """Test the complete report generation flow.""" + basic_csa_compliance_data.requirements = [ + RequirementData( + id="REQ-001", + description="IAM passed", + status=StatusChoices.PASS, + passed_findings=10, + failed_findings=0, + total_findings=10, + ), + RequirementData( + id="REQ-002", + description="Logging failed", + status=StatusChoices.FAIL, + passed_findings=0, + failed_findings=5, + total_findings=5, + ), + ] + basic_csa_compliance_data.attributes_by_requirement_id = { + "REQ-001": { + "attributes": {"req_attributes": [mock_csa_requirement_attribute_iam]} + }, + "REQ-002": { + "attributes": { + "req_attributes": [mock_csa_requirement_attribute_logging] + } + }, + } + + # Generate all sections + exec_summary = csa_generator.create_executive_summary(basic_csa_compliance_data) + charts = csa_generator.create_charts_section(basic_csa_compliance_data) + index = csa_generator.create_requirements_index(basic_csa_compliance_data) + + # All sections should generate without errors + assert len(exec_summary) > 0 + assert len(charts) > 0 + assert len(index) > 0 diff --git a/contrib/k8s/helm/prowler-app/.helmignore b/contrib/k8s/helm/prowler-app/.helmignore new file mode 100644 index 0000000000..7d250c5aa5 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/.helmignore @@ -0,0 +1,24 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +examples +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/contrib/k8s/helm/prowler-app/Chart.lock b/contrib/k8s/helm/prowler-app/Chart.lock new file mode 100644 index 0000000000..fe4af2f9e2 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/Chart.lock @@ -0,0 +1,12 @@ +dependencies: +- name: postgresql + repository: oci://registry-1.docker.io/bitnamicharts + version: 18.2.0 +- name: valkey + repository: https://valkey.io/valkey-helm/ + version: 0.9.3 +- name: neo4j + repository: https://helm.neo4j.com/neo4j + version: 2025.12.1 +digest: sha256:da19233c6832727345fcdb314d683d30aa347d349f270023f3a67149bffb009b +generated: "2026-01-26T12:00:06.798702+02:00" diff --git a/contrib/k8s/helm/prowler-app/Chart.yaml b/contrib/k8s/helm/prowler-app/Chart.yaml new file mode 100644 index 0000000000..5397d43569 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/Chart.yaml @@ -0,0 +1,33 @@ +apiVersion: v2 +name: prowler +description: Prowler is an Open Cloud Security tool for AWS, Azure, GCP and Kubernetes. It helps for continuous monitoring, security assessments and audits, incident response, compliance, hardening and forensics readiness. +type: application +version: 0.0.1 +appVersion: "5.17.0" +home: https://prowler.com +icon: https://cdn.prod.website-files.com/68c4ec3f9fb7b154fbcb6e36/68c5e0fea5d0059b9e05834b_Link.png +keywords: + - security + - aws + - azure + - gcp + - kubernetes +maintainers: + - name: Mihai + email: mihai.legat@gmail.com +dependencies: + # https://artifacthub.io/packages/helm/bitnami/postgresql + - name: postgresql + version: 18.2.0 + repository: oci://registry-1.docker.io/bitnamicharts + condition: postgresql.enabled + # https://valkey.io/valkey-helm/ + - name: valkey + version: 0.9.3 + repository: https://valkey.io/valkey-helm/ + condition: valkey.enabled + # https://helm.neo4j.com/neo4j + - name: neo4j + version: 2025.12.1 + repository: https://helm.neo4j.com/neo4j + condition: neo4j.enabled diff --git a/contrib/k8s/helm/prowler-app/README.md b/contrib/k8s/helm/prowler-app/README.md new file mode 100644 index 0000000000..544b5f39a7 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/README.md @@ -0,0 +1,143 @@ + + +# Prowler App Helm Chart + +![Version: 0.0.1](https://img.shields.io/badge/Version-0.0.1-informational?style=flat-square) +![AppVersion: 5.17.0](https://img.shields.io/badge/AppVersion-5.17.0-informational?style=flat-square) + +Prowler is an Open Cloud Security tool for AWS, Azure, GCP and Kubernetes. It helps for continuous monitoring, security assessments and audits, incident response, compliance, hardening and forensics readiness. Includes CIS, NIST 800, NIST CSF, CISA, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, Well-Architected Security, ENS and more. + +## Architecture + +The Prowler App consists of three main components: + +- **Prowler UI**: A user-friendly web interface for running Prowler and viewing results, powered by Next.js. +- **Prowler API**: The backend API that executes Prowler scans and stores the results, built with Django REST Framework. +- **Prowler SDK**: A Python SDK that integrates with the Prowler CLI for advanced functionality. + +The app leverages the following supporting infrastructure: + +- **PostgreSQL**: Used for persistent storage of scan results. +- **Celery Workers**: Facilitate asynchronous execution of Prowler scans. +- **Valkey**: An in-memory database serving as a message broker for the Celery workers. +- **Neo4j**: Graph Database +- **Keda**: Kubernetes Event-driven Autoscaling (Keda) automatically scales the number of Celery worker pods based on the workload, ensuring efficient resource utilization and responsiveness. + +## Setup + +This guide walks you through installing Prowler App using Helm. For a minimal installation example, see the [minimal installation example](./examples/minimal-installation/). + +### Prerequisites + +- Kubernetes cluster (1.24+) +- Helm 3.x installed +- `kubectl` configured to access your cluster +- Access to the Prowler Helm chart repository (or local chart) + +### Step 1: Create Required Secrets + +Before installing the Helm chart, you must create a Kubernetes Secret containing the required authentication keys and secrets. + +1. **Generate the required keys and secrets:** + + ```bash + # Generate Django token signing key (private key) + openssl genrsa -out private.pem 2048 + + # Generate Django token verifying key (public key) + openssl rsa -in private.pem -pubout -out public.pem + + # Generate Django secrets encryption key + openssl rand -base64 32 + + # Generate Auth secret + openssl rand -base64 32 + ``` + +2. **Create the secret file:** + + Create a file named `secrets.yaml` with the following structure: + + ```yaml + apiVersion: v1 + kind: Secret + type: Opaque + metadata: + name: prowler-secret + stringData: + DJANGO_TOKEN_SIGNING_KEY: | + -----BEGIN PRIVATE KEY----- + [paste your private key here] + -----END PRIVATE KEY----- + + DJANGO_TOKEN_VERIFYING_KEY: | + -----BEGIN PUBLIC KEY----- + [paste your public key here] + -----END PUBLIC KEY----- + + DJANGO_SECRETS_ENCRYPTION_KEY: "[paste your encryption key here]" + + AUTH_SECRET: "[paste your auth secret here]" + + NEO4J_PASSWORD: "[prowler-password]" + NEO4J_AUTH: "neo4j/[prowler-password]" + ``` + + > **Note:** You can use the [example secrets file](./examples/minimal-installation/secrets.yaml) as a template, but **always replace the placeholder values with your own secure keys** before applying. + +3. **Apply the secret to your cluster:** + + ```bash + kubectl apply -f secrets.yaml + ``` + +### Step 2: Configure Values + +Create a `values.yaml` file to customize your installation. At minimum, you need to configure the UI access method. + +**Option A: Using Ingress (Recommended for production)** + +```yaml +ui: + ingress: + enabled: true + hosts: + - host: prowler.example.com + paths: + - path: / + pathType: ImplementationSpecific +``` + +**Option B: Using authUrl (For proxy setups)** + +```yaml +ui: + authUrl: prowler.example.com +``` + +> **Note:** See the [minimal installation example](./examples/minimal-installation/values.yaml) for a complete reference. + +### Step 3: Install the Chart + +Install Prowler App using Helm: + +```bash +helm dependency update +helm install prowler prowler/prowler-app -f values.yaml +``` + +### Using Existing PostgreSQL and Valkey Instances + +By default, this Chart uses Bitnami's Charts to deploy [PostgreSQL](https://artifacthub.io/packages/helm/bitnami/postgresql), [Neo4j](https://helm.neo4j.com/neo4j) and [Valkey official helm chart](https://valkey.io/valkey-helm/). **Note:** This default setup is not production-ready. + +To connect to existing PostgreSQL, Neo4j and Valkey instances: + +1. Create a `Secret` containing the correct database and message broker credentials +2. Reference the secret in the [values.yaml](values.yaml) file api->secrets list + +## Contributing + +Feel free to contact the maintainer of this repository for any questions or concerns. Contributions are encouraged and appreciated. diff --git a/contrib/k8s/helm/prowler-app/examples/minimal-installation/README.md b/contrib/k8s/helm/prowler-app/examples/minimal-installation/README.md new file mode 100644 index 0000000000..e22f429b44 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/examples/minimal-installation/README.md @@ -0,0 +1,46 @@ +# Minimal Installation Example + +This example demonstrates a minimal installation of Prowler in a Kubernetes cluster. + +## Installation + +To install Prowler using this example: + +1. First, create the required secret: +```bash +# Edit secret.yaml and set secure values before applying +kubectl apply -f secret.yaml +``` + +1. Install the chart using the base values file: +```bash +# Basic installation +helm install prowler prowler/prowler-app -f values.yaml +``` + +## Configuration + +The example contains the following configuration files: + +### `secret.yaml` +Contains all required secrets for the Prowler installation. **Must be applied before installing the Helm chart**. Make sure to replace all placeholder values with secure values before applying. + +### `values.yaml` +```yaml +ui: + # Note: You should set either `authUrl` if you use prowler behind a proxy or enable `ingress`. + + # Example with authUrl: + # authUrl: example.prowler.com + + # Example with ingress: + ingress: + enabled: true + hosts: + - host: example.prowler.com + paths: + - path: / + pathType: ImplementationSpecific +``` + +Make sure to adjust the hostname in the values file to match your environment before installing. diff --git a/contrib/k8s/helm/prowler-app/examples/minimal-installation/secrets.yaml b/contrib/k8s/helm/prowler-app/examples/minimal-installation/secrets.yaml new file mode 100644 index 0000000000..2e379ef5c8 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/examples/minimal-installation/secrets.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: prowler-secret +stringData: + # openssl genrsa -out private.pem 2048 + DJANGO_TOKEN_SIGNING_KEY: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCIro0QiLAxw7rF + GO0NgAWJfkpYE5ysMGDCbId07HUrv+/SCoRjqKVzGJVIvmNP5oByzSehPgswW9v3 + 3dqe2r9sCS1JyMa+XO3qfZCR0uRDcPCwZjIyr0QQLpWAymdBa8baeHsU1/3Orjcb + Vrr+lNx4HQJOiSn094iXPReW/25hYeq/SXs79V2CR87PGdoZAhb8IllAxJgdfkeB + /iWohY/1vfRTmIuMweWGXk0aKzPsBdvE/DqG4HjiNVEPh18G3vid0YTZNmm7u8vO + Cue3x9NQWGHA4QtxNtLtxlHcOEryqZ9ChO2nC+ew0Xl/v706XFNyLFicjisIKNQo + qdkaMS33AgMBAAECggEAGdJIChCYoL4mYafk2MEPyrrWFq+V0J3PGcvhB0DInfxD + tT2RZzZsE0NYqIZ3Qpf8OjPxwa9z863W74u1Cn+u3B0bti29BieONteD4VijEO6c + OecEorijth7m1Y7nVN+kkI9kSTrI0yvsczi+WOwMfpCUZ/vXtlSxNEkxVLBqzPCo + 9VxAFIjgWOj2rpw8nxPedves36PUrC5ghLqrOTe1jmw/Di0++47AXG+DsTXc00sc + 5+oybopm3Kimsxrqbf9s8SZf2A8NiwqcbLj8OtP2j2g4TCEgZYLD5Zmt+JN/wN4B + WsQG/Hwp4KPPm9QTHEpuuoPFP1CZWZeq8gPcV4apYQKBgQC+TuXjJCYhZqNIttTZ + z/i3hkKUEKQLkzTZnXaDzL5wHyEMVqM2E/WkilO0C9ZZwh0ENPzkp+JsHf7LEhHy + wSHOti81VzUCjN/YpCBKlOlClqSiDlOonImrobLei8xgvmA0VmGtirCXZyyzZUoV + OyPr17WpK6G/M5piX59MvKQg0QKBgQC33NBoQFD8A6FjrTopYmWfK099k9uQh9NE + bvUYsNAPunSDslmc/0PPHQC7fRX5Ime2BinXAN1PYtB/Fsu3jv/+FCUM5hVil0Dd + KBvt13+RYSCJKlhcGP1EkWoIg1F2XXBOZKJrC8VQ+Vyl2t06UcWQqy5M9J4VZaqI + fruOLU/URwKBgE55GjJfZZnASPRi78IhD94dbra/ZeWf/dr+IzCV7LEvJOGBmCtk + b5Y5s+o6N1krwetKLj3bPHJ4q+fwu5XuLZKfbTgBjcpPbL5YbzhRzx22IIzye2y7 + n8k2FBvQaaY62lC6jeyRk9/am4Qd8D5w9I77k9z+MOQ20yJda8KoxsUBAoGBAIQ9 + 5QPmppjsf4ry0C9t30uhWhYnX7fPiYviBpVQrwVxBVan076Q9xOjd6BicohzT4bj + XfqPW546o12VZsbKqqLzmEZzwpPb2EJ5E8V4xv8ojb86Xr03GArWUB55XQE2aY1o + 4kz99VitUg7UoWPN5ryL8sxU8NLRAdwU0w+K1a0HAoGAZaU7O94u9IIPZ6Ohobs2 + Vjf/eV0brCKgX61b4z/YhuJdZsyTujhBZUihZwqR696kiFKuzmHx1ghE2ITvnPVN + q0iHxRZzBCnRQ+mQlS0trzphaCP0NVy3osFeAD9mJfnOnSmkU0ua4F81mkvke1eN + 6nnaoAdy2lmMr96/Tye2ty4= + -----END PRIVATE KEY----- + + # openssl rsa -in private.pem -pubout -out public.pem + DJANGO_TOKEN_VERIFYING_KEY: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiK6NEIiwMcO6xRjtDYAF + iX5KWBOcrDBgwmyHdOx1K7/v0gqEY6ilcxiVSL5jT+aAcs0noT4LMFvb993antq/ + bAktScjGvlzt6n2QkdLkQ3DwsGYyMq9EEC6VgMpnQWvG2nh7FNf9zq43G1a6/pTc + eB0CTokp9PeIlz0Xlv9uYWHqv0l7O/VdgkfOzxnaGQIW/CJZQMSYHX5Hgf4lqIWP + 9b30U5iLjMHlhl5NGisz7AXbxPw6huB44jVRD4dfBt74ndGE2TZpu7vLzgrnt8fT + UFhhwOELcTbS7cZR3DhK8qmfQoTtpwvnsNF5f7+9OlxTcixYnI4rCCjUKKnZGjEt + 9wIDAQAB + -----END PUBLIC KEY----- + + # openssl rand -base64 32 + DJANGO_SECRETS_ENCRYPTION_KEY: "qYAIWnRK52aBT5YQkBoMEw08j7j3+QIPZXS6+A8Su44=" + + # openssl rand -base64 32 + AUTH_SECRET: "CM9w3Nco2P1RdHaYmD+fmy2nJmSofusdHd4g7Z4KDG4=" + + # Unfortunatelly, we need to duplicate the password in two different keys because the Neo4j Helm Chart expects the password in the NEO4J_AUTH key and the application expects it in the NEO4J_PASSWORD key. + NEO4J_PASSWORD: "prowler-password-fake" + NEO4J_AUTH: "neo4j/prowler-password-fake" diff --git a/contrib/k8s/helm/prowler-app/examples/minimal-installation/values.yaml b/contrib/k8s/helm/prowler-app/examples/minimal-installation/values.yaml new file mode 100644 index 0000000000..9ac8dda9e9 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/examples/minimal-installation/values.yaml @@ -0,0 +1,11 @@ +ui: + ingress: + enabled: true + hosts: + - host: 127.0.0.1.nip.io + paths: + - path: / + pathType: ImplementationSpecific + +# or use authUrl if you use prowler behind a proxy +# authUrl: 127.0.0.1.nip.io diff --git a/contrib/k8s/helm/prowler-app/templates/_helpers.tpl b/contrib/k8s/helm/prowler-app/templates/_helpers.tpl new file mode 100644 index 0000000000..7698fbfa18 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/_helpers.tpl @@ -0,0 +1,134 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "prowler.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "prowler.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "prowler.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "prowler.labels" -}} +helm.sh/chart: {{ include "prowler.chart" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Django environment variables for api, worker, and worker_beat. +*/}} +{{- define "prowler.django.env" -}} +- name: DJANGO_TOKEN_SIGNING_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.djangoTokenSigningKey.secretKeyRef.name }} + key: {{ .Values.djangoTokenSigningKey.secretKeyRef.key }} +- name: DJANGO_TOKEN_VERIFYING_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.djangoTokenVerifyingKey.secretKeyRef.name }} + key: {{ .Values.djangoTokenVerifyingKey.secretKeyRef.key }} +- name: DJANGO_SECRETS_ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.djangoSecretsEncryptionKey.secretKeyRef.name }} + key: {{ .Values.djangoSecretsEncryptionKey.secretKeyRef.key }} +{{- end }} + + +{{/* +PostgreSQL environment variables for api, worker, and worker_beat. +Outputs nothing when postgresql.enabled is false. +*/}} +{{- define "prowler.postgresql.env" -}} +{{- if .Values.postgresql.enabled }} +{{- if .Values.postgresql.auth.username }} +- name: POSTGRES_USER + value: {{ .Values.postgresql.auth.username | quote }} +{{- end }} +- name: POSTGRES_PASSWORD +{{- if .Values.postgresql.auth.existingSecret }} + valueFrom: + secretKeyRef: + name: {{ .Values.postgresql.auth.existingSecret }} + key: {{ required "postgresql.auth.secretKeys.userPasswordKey is required when using an existing secret" .Values.postgresql.auth.secretKeys.userPasswordKey }} +{{- else if .Values.postgresql.auth.password }} + value: {{ .Values.postgresql.auth.password | quote }} +{{- else }} + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-postgresql + key: password +{{- end }} +- name: POSTGRES_DB + value: {{ .Values.postgresql.auth.database | quote }} +- name: POSTGRES_HOST + value: {{ .Release.Name }}-postgresql +- name: POSTGRES_PORT + value: "5432" +- name: POSTGRES_ADMIN_USER + value: postgres +- name: POSTGRES_ADMIN_PASSWORD +{{- if .Values.postgresql.auth.existingSecret }} + valueFrom: + secretKeyRef: + name: {{ .Values.postgresql.auth.existingSecret }} + key: {{ required "postgresql.auth.secretKeys.adminPasswordKey is required when using an existing secret" .Values.postgresql.auth.secretKeys.adminPasswordKey }} +{{- else if .Values.postgresql.auth.postgresPassword }} + value: {{ .Values.postgresql.auth.postgresPassword | quote }} +{{- else }} + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-postgresql + key: postgres-password +{{- end }} +{{- end }} +{{- end }} + +{{/* +Neo4j environment variables for api, worker, and worker_beat. +Outputs nothing when neo4j.enabled is false. +*/}} +{{- define "prowler.neo4j.env" -}} +{{- if .Values.neo4j.enabled }} +- name: NEO4J_HOST + value: {{ .Release.Name }} +- name: NEO4J_PORT + value: "7687" +- name: NEO4J_USER + value: "neo4j" +- name: NEO4J_PASSWORD + valueFrom: + secretKeyRef: + name: {{ required "neo4j.neo4j.passwordFromSecret is required" .Values.neo4j.neo4j.passwordFromSecret }} + key: NEO4J_PASSWORD +{{- end }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/api/_helpers.tpl b/contrib/k8s/helm/prowler-app/templates/api/_helpers.tpl new file mode 100644 index 0000000000..55ac97f0d8 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/api/_helpers.tpl @@ -0,0 +1,10 @@ +{{/* +Create the name of the service account to use +*/}} +{{- define "prowler.api.serviceAccountName" -}} +{{- if .Values.api.serviceAccount.create }} +{{- default (printf "%s-%s" (include "prowler.fullname" .) "api") .Values.api.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.api.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/api/configmap.yaml b/contrib/k8s/helm/prowler-app/templates/api/configmap.yaml new file mode 100644 index 0000000000..8e219a9271 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/api/configmap.yaml @@ -0,0 +1,10 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: {{ include "prowler.fullname" . }}-api + labels: + {{- include "prowler.labels" . | nindent 4 }} +data: + {{- range $key, $value := .Values.api.djangoConfig }} + {{ $key }}: {{ $value | quote }} + {{- end }} \ No newline at end of file diff --git a/contrib/k8s/helm/prowler-app/templates/api/deployment.yaml b/contrib/k8s/helm/prowler-app/templates/api/deployment.yaml new file mode 100644 index 0000000000..f7a16b66ae --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/api/deployment.yaml @@ -0,0 +1,105 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "prowler.fullname" . }}-api + labels: + {{- include "prowler.labels" . | nindent 4 }} +spec: + {{- if not .Values.api.autoscaling.enabled }} + replicas: {{ .Values.api.replicaCount }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "prowler.fullname" . }}-api + template: + metadata: + annotations: + secret-hash: "{{ printf "%s%s%s" (.Files.Get "templates/api/configmap.yaml" | sha256sum) (.Files.Get "templates/api/secret-valkey.yaml" | sha256sum) | sha256sum }}" + {{- with .Values.api.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "prowler.labels" . | nindent 8 }} + app.kubernetes.io/name: {{ include "prowler.fullname" . }}-api + {{- with .Values.api.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.api.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "prowler.api.serviceAccountName" . }} + {{- with .Values.api.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: api + {{- with .Values.api.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.api.image.pullPolicy }} + {{- with .Values.api.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.api.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.api.service.port }} + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "prowler.fullname" . }}-api + {{- if .Values.valkey.enabled }} + - secretRef: + name: {{ include "prowler.fullname" . }}-api-valkey + {{- end }} + {{- with .Values.api.secrets }} + {{- range $index, $secret := . }} + - secretRef: + name: {{ $secret }} + {{- end }} + {{- end }} + env: + {{- include "prowler.django.env" . | nindent 12 }} + {{- include "prowler.postgresql.env" . | nindent 12 }} + {{- include "prowler.neo4j.env" . | nindent 12 }} + {{- with .Values.api.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.api.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.api.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.api.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.api.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.api.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.api.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.api.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/api/hpa.yaml b/contrib/k8s/helm/prowler-app/templates/api/hpa.yaml new file mode 100644 index 0000000000..c3d77d7e44 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/api/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.api.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "prowler.fullname" . }}-api + labels: + {{- include "prowler.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "prowler.fullname" . }}-api + minReplicas: {{ .Values.api.autoscaling.minReplicas }} + maxReplicas: {{ .Values.api.autoscaling.maxReplicas }} + metrics: + {{- if .Values.api.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.api.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.api.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.api.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/api/ingress.yaml b/contrib/k8s/helm/prowler-app/templates/api/ingress.yaml new file mode 100644 index 0000000000..4118d9cd7a --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/api/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.api.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "prowler.fullname" . }}-api + labels: + {{- include "prowler.labels" . | nindent 4 }} + {{- with .Values.api.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.api.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.api.ingress.tls }} + tls: + {{- range .Values.api.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.api.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "prowler.fullname" $ }}-api + port: + number: {{ $.Values.api.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/api/role.yaml b/contrib/k8s/helm/prowler-app/templates/api/role.yaml new file mode 100644 index 0000000000..172b035076 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/api/role.yaml @@ -0,0 +1,29 @@ +# https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app/#step-44-kubernetes-credentials +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "prowler.fullname" . }}-api + labels: + {{- include "prowler.labels" . | nindent 4 }} +rules: +- apiGroups: [""] + resources: ["pods", "configmaps", "nodes", "namespaces"] + verbs: ["get", "list", "watch"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterrolebindings", "rolebindings", "clusterroles", "roles"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "prowler.fullname" . }}-api + labels: + {{- include "prowler.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "prowler.fullname" . }}-api +subjects: +- kind: ServiceAccount + name: {{ include "prowler.api.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} \ No newline at end of file diff --git a/contrib/k8s/helm/prowler-app/templates/api/secret-valkey.yaml b/contrib/k8s/helm/prowler-app/templates/api/secret-valkey.yaml new file mode 100644 index 0000000000..7778d06731 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/api/secret-valkey.yaml @@ -0,0 +1,13 @@ +{{- if .Values.valkey.enabled -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "prowler.fullname" . }}-api-valkey + labels: + {{- include "prowler.labels" . | nindent 4 }} +type: Opaque +stringData: + VALKEY_HOST: "{{ include "prowler.fullname" . }}-valkey" + VALKEY_PORT: "6379" + VALKEY_DB: "0" +{{- end -}} diff --git a/contrib/k8s/helm/prowler-app/templates/api/service.yaml b/contrib/k8s/helm/prowler-app/templates/api/service.yaml new file mode 100644 index 0000000000..9a42979306 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/api/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "prowler.fullname" . }}-api + labels: + {{- include "prowler.labels" . | nindent 4 }} +spec: + type: {{ .Values.api.service.type }} + ports: + - port: {{ .Values.api.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: {{ include "prowler.fullname" . }}-api diff --git a/contrib/k8s/helm/prowler-app/templates/api/serviceaccount.yaml b/contrib/k8s/helm/prowler-app/templates/api/serviceaccount.yaml new file mode 100644 index 0000000000..4d76d7f54e --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/api/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.api.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "prowler.api.serviceAccountName" . }} + labels: + {{- include "prowler.labels" . | nindent 4 }} + {{- with .Values.api.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.api.serviceAccount.automount }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/ui/_helpers.tpl b/contrib/k8s/helm/prowler-app/templates/ui/_helpers.tpl new file mode 100644 index 0000000000..8bdf93ba5f --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/ui/_helpers.tpl @@ -0,0 +1,10 @@ +{{/* +Create the name of the service account to use +*/}} +{{- define "prowler.ui.serviceAccountName" -}} +{{- if .Values.ui.serviceAccount.create }} +{{- default (printf "%s-%s" (include "prowler.fullname" .) "ui") .Values.ui.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.ui.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/ui/configmap.yaml b/contrib/k8s/helm/prowler-app/templates/ui/configmap.yaml new file mode 100644 index 0000000000..38d6e65ee3 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/ui/configmap.yaml @@ -0,0 +1,18 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: {{ include "prowler.fullname" . }}-ui +data: + PROWLER_UI_VERSION: "stable" + {{- if .Values.ui.ingress.enabled }} + {{- with (first .Values.ui.ingress.hosts) }} + AUTH_URL: "https://{{ .host }}" + {{- end }} + {{- else }} + AUTH_URL: {{ .Values.ui.authUrl | quote }} + {{- end }} + API_BASE_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1" + NEXT_PUBLIC_API_BASE_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1" + NEXT_PUBLIC_API_DOCS_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1/docs" + AUTH_TRUST_HOST: "true" + UI_PORT: {{ .Values.ui.service.port | quote }} diff --git a/contrib/k8s/helm/prowler-app/templates/ui/deployment.yaml b/contrib/k8s/helm/prowler-app/templates/ui/deployment.yaml new file mode 100644 index 0000000000..f7bf2c17fe --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/ui/deployment.yaml @@ -0,0 +1,95 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "prowler.fullname" . }}-ui + labels: + {{- include "prowler.labels" . | nindent 4 }} +spec: + {{- if not .Values.ui.autoscaling.enabled }} + replicas: {{ .Values.ui.replicaCount }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "prowler.fullname" . }}-ui + template: + metadata: + annotations: + secret-hash: {{ .Files.Get "templates/ui/configmap.yaml" | sha256sum }} + {{- with .Values.ui.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "prowler.labels" . | nindent 8 }} + app.kubernetes.io/name: {{ include "prowler.fullname" . }}-ui + {{- with .Values.ui.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.ui.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "prowler.ui.serviceAccountName" . }} + {{- with .Values.ui.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: ui + {{- with .Values.ui.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.ui.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.ui.service.port }} + protocol: TCP + env: + - name: AUTH_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.ui.authSecret.secretKeyRef.name }} + key: {{ .Values.ui.authSecret.secretKeyRef.key }} + envFrom: + - configMapRef: + name: {{ include "prowler.fullname" . }}-ui + {{- with .Values.ui.secrets }} + {{- range $index, $secret := . }} + - secretRef: + name: {{ $secret }} + {{- end }} + {{- end }} + {{- with .Values.ui.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.ui.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.ui.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.ui.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.ui.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/ui/hpa.yaml b/contrib/k8s/helm/prowler-app/templates/ui/hpa.yaml new file mode 100644 index 0000000000..7c6716ef1f --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/ui/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.ui.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "prowler.fullname" . }}-ui + labels: + {{- include "prowler.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "prowler.fullname" . }}-ui + minReplicas: {{ .Values.ui.autoscaling.minReplicas }} + maxReplicas: {{ .Values.ui.autoscaling.maxReplicas }} + metrics: + {{- if .Values.ui.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.ui.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.ui.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.ui.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/ui/ingress.yaml b/contrib/k8s/helm/prowler-app/templates/ui/ingress.yaml new file mode 100644 index 0000000000..74dcecabe1 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/ui/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ui.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "prowler.fullname" . }}-ui + labels: + {{- include "prowler.labels" . | nindent 4 }} + {{- with .Values.ui.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ui.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ui.ingress.tls }} + tls: + {{- range .Values.ui.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ui.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "prowler.fullname" $ }}-ui + port: + number: {{ $.Values.ui.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/ui/service.yaml b/contrib/k8s/helm/prowler-app/templates/ui/service.yaml new file mode 100644 index 0000000000..9b845e5b5f --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/ui/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "prowler.fullname" . }}-ui + labels: + {{- include "prowler.labels" . | nindent 4 }} +spec: + type: {{ .Values.ui.service.type }} + ports: + - port: {{ .Values.ui.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: {{ include "prowler.fullname" . }}-ui diff --git a/contrib/k8s/helm/prowler-app/templates/ui/serviceaccount.yaml b/contrib/k8s/helm/prowler-app/templates/ui/serviceaccount.yaml new file mode 100644 index 0000000000..91b176a64e --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/ui/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.ui.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "prowler.ui.serviceAccountName" . }} + labels: + {{- include "prowler.labels" . | nindent 4 }} + {{- with .Values.ui.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.ui.serviceAccount.automount }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/worker/_helpers.tpl b/contrib/k8s/helm/prowler-app/templates/worker/_helpers.tpl new file mode 100644 index 0000000000..3a99d42cb6 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/worker/_helpers.tpl @@ -0,0 +1,10 @@ +{{/* +Create the name of the service account to use +*/}} +{{- define "prowler.worker.serviceAccountName" -}} +{{- if .Values.worker.serviceAccount.create }} +{{- default (printf "%s-%s" (include "prowler.fullname" .) "worker") .Values.worker.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.worker.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/worker/deployment.yaml b/contrib/k8s/helm/prowler-app/templates/worker/deployment.yaml new file mode 100644 index 0000000000..6c11a28f9b --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/worker/deployment.yaml @@ -0,0 +1,101 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "prowler.fullname" . }}-worker + labels: + {{- include "prowler.labels" . | nindent 4 }} +spec: + {{- if not .Values.worker.autoscaling.enabled }} + replicas: {{ .Values.worker.replicaCount }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "prowler.fullname" . }}-worker + template: + metadata: + annotations: + secret-hash: "{{ printf "%s%s%s" (.Files.Get "templates/api/configmap.yaml" | sha256sum) (.Files.Get "templates/api/secret-valkey.yaml" | sha256sum) | sha256sum }}" + {{- with .Values.worker.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "prowler.labels" . | nindent 8 }} + app.kubernetes.io/name: {{ include "prowler.fullname" . }}-worker + {{- with .Values.worker.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.worker.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "prowler.worker.serviceAccountName" . }} + {{- with .Values.worker.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: worker + {{- with .Values.worker.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.worker.image.repository }}:{{ .Values.worker.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.worker.image.pullPolicy }} + {{- with .Values.worker.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.worker.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + envFrom: + - configMapRef: + name: {{ include "prowler.fullname" . }}-api + {{- if .Values.valkey.enabled }} + - secretRef: + name: {{ include "prowler.fullname" . }}-api-valkey + {{- end }} + {{- with .Values.api.secrets }} + {{- range $index, $secret := . }} + - secretRef: + name: {{ $secret }} + {{- end }} + {{- end }} + env: + {{- include "prowler.django.env" . | nindent 12 }} + {{- include "prowler.postgresql.env" . | nindent 12 }} + {{- include "prowler.neo4j.env" . | nindent 12 }} + {{- with .Values.worker.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.worker.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.worker.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.worker.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.worker.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.worker.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.worker.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.worker.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/worker/hpa.yaml b/contrib/k8s/helm/prowler-app/templates/worker/hpa.yaml new file mode 100644 index 0000000000..d77ab3f47e --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/worker/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.worker.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "prowler.fullname" . }}-worker + labels: + {{- include "prowler.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "prowler.fullname" . }}-worker + minReplicas: {{ .Values.worker.autoscaling.minReplicas }} + maxReplicas: {{ .Values.worker.autoscaling.maxReplicas }} + metrics: + {{- if .Values.worker.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.worker.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.worker.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.worker.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/worker/scaled-object.yaml b/contrib/k8s/helm/prowler-app/templates/worker/scaled-object.yaml new file mode 100644 index 0000000000..98ae3ae9d5 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/worker/scaled-object.yaml @@ -0,0 +1,32 @@ +{{- if .Values.worker.keda.enabled }} +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{ include "prowler.fullname" . }}-worker + namespace: {{ $.Release.Namespace }} + labels: + {{- include "prowler.labels" . | nindent 4 }} +spec: + scaleTargetRef: + name: {{ include "prowler.fullname" . }}-worker + envSourceContainerName: worker + kind: Deployment + minReplicaCount: {{ .Values.worker.keda.minReplicas }} + maxReplicaCount: {{ .Values.worker.keda.maxReplicas }} + pollingInterval: {{ .Values.worker.keda.pollingInterval }} + cooldownPeriod: {{ .Values.worker.keda.cooldownPeriod }} + triggers: + - type: {{ .Values.worker.keda.triggerType }} + metadata: + userName: "postgres" + passwordFromEnv: POSTGRES_ADMIN_PASSWORD + host: {{ .Release.Name }}-postgresql + port: {{ .Values.postgresql.port | quote }} + dbName: {{ .Values.postgresql.auth.database | quote }} + sslmode: disable + # Query for KEDA to count the number of scans that are in executing, available, or scheduled states, + # where the scheduled time is within the last 2 hours and is before NOW(). Used for scaling workers. + query: >- + SELECT COUNT(*) FROM scans WHERE ((state='executing' OR state='available' OR state='scheduled') and scheduled_at < NOW() and scheduled_at > NOW() - INTERVAL '2 hours') + targetQueryValue: "1" +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/worker/serviceaccount.yaml b/contrib/k8s/helm/prowler-app/templates/worker/serviceaccount.yaml new file mode 100644 index 0000000000..8974d3ce04 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/worker/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.worker.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "prowler.worker.serviceAccountName" . }} + labels: + {{- include "prowler.labels" . | nindent 4 }} + {{- with .Values.worker.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.worker.serviceAccount.automount }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/worker_beat/_helpers.tpl b/contrib/k8s/helm/prowler-app/templates/worker_beat/_helpers.tpl new file mode 100644 index 0000000000..b9ce287667 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/worker_beat/_helpers.tpl @@ -0,0 +1,10 @@ +{{/* +Create the name of the service account to use +*/}} +{{- define "prowler.worker_beat.serviceAccountName" -}} +{{- if .Values.worker_beat.serviceAccount.create }} +{{- default (printf "%s-%s" (include "prowler.fullname" .) "worker-beat") .Values.worker_beat.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.worker_beat.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/worker_beat/deployment.yaml b/contrib/k8s/helm/prowler-app/templates/worker_beat/deployment.yaml new file mode 100644 index 0000000000..749ea946fd --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/worker_beat/deployment.yaml @@ -0,0 +1,99 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "prowler.fullname" . }}-worker-beat + labels: + {{- include "prowler.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.worker_beat.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "prowler.fullname" . }}-worker-beat + template: + metadata: + annotations: + secret-hash: "{{ printf "%s%s%s" (.Files.Get "templates/api/configmap.yaml" | sha256sum) (.Files.Get "templates/api/secret-valkey.yaml" | sha256sum) | sha256sum }}" + {{- with .Values.worker.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "prowler.labels" . | nindent 8 }} + app.kubernetes.io/name: {{ include "prowler.fullname" . }}-worker-beat + {{- with .Values.worker_beat.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.worker_beat.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "prowler.worker_beat.serviceAccountName" . }} + {{- with .Values.worker_beat.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: worker-beat + {{- with .Values.worker_beat.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.worker_beat.image.repository }}:{{ .Values.worker_beat.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.worker_beat.image.pullPolicy }} + {{- with .Values.worker_beat.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.worker_beat.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + envFrom: + - configMapRef: + name: {{ include "prowler.fullname" . }}-api + {{- if .Values.valkey.enabled }} + - secretRef: + name: {{ include "prowler.fullname" . }}-api-valkey + {{- end }} + {{- with .Values.api.secrets }} + {{- range $index, $secret := . }} + - secretRef: + name: {{ $secret }} + {{- end }} + {{- end }} + env: + {{- include "prowler.django.env" . | nindent 12 }} + {{- include "prowler.postgresql.env" . | nindent 12 }} + {{- include "prowler.neo4j.env" . | nindent 12 }} + {{- with .Values.worker_beat.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.worker_beat.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.worker_beat.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.worker_beat.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.worker_beat.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.worker_beat.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.worker_beat.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.worker_beat.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/contrib/k8s/helm/prowler-app/templates/worker_beat/serviceaccount.yaml b/contrib/k8s/helm/prowler-app/templates/worker_beat/serviceaccount.yaml new file mode 100644 index 0000000000..9718686c2a --- /dev/null +++ b/contrib/k8s/helm/prowler-app/templates/worker_beat/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.worker_beat.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "prowler.worker_beat.serviceAccountName" . }} + labels: + {{- include "prowler.labels" . | nindent 4 }} + {{- with .Values.worker_beat.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.worker_beat.serviceAccount.automount }} +{{- end }} diff --git a/contrib/k8s/helm/prowler-app/values.yaml b/contrib/k8s/helm/prowler-app/values.yaml new file mode 100644 index 0000000000..46258351ad --- /dev/null +++ b/contrib/k8s/helm/prowler-app/values.yaml @@ -0,0 +1,566 @@ +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# Reference to the secret containing the API authentication secret. +# Used to inject the environment variable for the API container. +djangoTokenSigningKey: + secretKeyRef: + name: prowler-secret + key: DJANGO_TOKEN_SIGNING_KEY +djangoTokenVerifyingKey: + secretKeyRef: + name: prowler-secret + key: DJANGO_TOKEN_VERIFYING_KEY +djangoSecretsEncryptionKey: + secretKeyRef: + name: prowler-secret + key: DJANGO_SECRETS_ENCRYPTION_KEY + +ui: + # This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ + replicaCount: 1 + + # This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ + image: + repository: prowlercloud/prowler-ui + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + + # This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + imagePullSecrets: [] + + # Reference to the secret containing the UI authentication secret. + # Used to inject the environment variable for the UI container. + # By default, expects a Secret named 'prowler-secret' with a key 'AUTH_SECRET'. + authSecret: + secretKeyRef: + name: prowler-secret + key: AUTH_SECRET + + # Secret names to be used as env vars. + secrets: [] + # - "prowler-ui-secret" + + # This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ + serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + + # This is for setting Kubernetes Annotations to a Pod. + # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + podAnnotations: {} + # This is for setting Kubernetes Labels to a Pod. + # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + podLabels: {} + + podSecurityContext: {} + # fsGroup: 2000 + + securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + + # This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ + service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 3000 + + # The URL of the UI. This is only set if ingress is disabled. + authUrl: "" + + # This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ + ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + + resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + # This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + + # This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + + # Additional volumes on the output Deployment definition. + volumes: [] + # - name: foo + # secret: + # secretName: mysecret + # optional: false + + # Additional volumeMounts on the output Deployment definition. + volumeMounts: [] + # - name: foo + # mountPath: "/etc/foo" + # readOnly: true + + nodeSelector: {} + + tolerations: [] + + affinity: {} + +api: + # This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ + replicaCount: 1 + + # This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ + image: + repository: prowlercloud/prowler-api + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + + # Shared with celery-worker and celery-beat + djangoConfig: + # API scan settings + # The path to the directory where scan output should be stored + DJANGO_TMP_OUTPUT_DIRECTORY: "/tmp/prowler_api_output" + # The maximum number of findings to process in a single batch + DJANGO_FINDINGS_BATCH_SIZE: "1000" + # Django settings + DJANGO_ALLOWED_HOSTS: "*" + DJANGO_BIND_ADDRESS: "0.0.0.0" + DJANGO_PORT: "8080" + DJANGO_DEBUG: "False" + DJANGO_SETTINGS_MODULE: "config.django.production" + # Select one of [ndjson|human_readable] + DJANGO_LOGGING_FORMATTER: "ndjson" + # Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL] + # Applies to both Django and Celery Workers + DJANGO_LOGGING_LEVEL: "INFO" + # Defaults to the maximum available based on CPU cores if not set. + DJANGO_WORKERS: "4" + # Token lifetime is in minutes + DJANGO_ACCESS_TOKEN_LIFETIME: "30" + # Token lifetime is in minutes + DJANGO_REFRESH_TOKEN_LIFETIME: "1440" + DJANGO_CACHE_MAX_AGE: "3600" + DJANGO_STALE_WHILE_REVALIDATE: "60" + DJANGO_MANAGE_DB_PARTITIONS: "True" + DJANGO_BROKER_VISIBILITY_TIMEOUT: "86400" + + # Secret names to be used as env vars for api, worker, and worker_beat. + secrets: [] + # - "prowler-api-keys" + + command: + - /home/prowler/docker-entrypoint.sh + args: + - prod + + # This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + imagePullSecrets: [] + + # This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ + serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + + # This is for setting Kubernetes Annotations to a Pod. + # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + podAnnotations: {} + # This is for setting Kubernetes Labels to a Pod. + # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + podLabels: {} + + podSecurityContext: {} + # fsGroup: 2000 + + securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + + # This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ + service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 8080 + + # This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ + ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + + resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + # 3m30s to setup DB + # startupProbe: + # httpGet: + # path: /api/v1/docs + # port: http + + # This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + livenessProbe: + failureThreshold: 10 + httpGet: + path: /api/v1/docs + port: http + periodSeconds: 20 + readinessProbe: + failureThreshold: 10 + httpGet: + path: /api/v1/docs + port: http + periodSeconds: 20 + + # This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + + # Additional volumes on the output Deployment definition. + volumes: [] + # - name: foo + # secret: + # secretName: mysecret + # optional: false + + # Additional volumeMounts on the output Deployment definition. + volumeMounts: [] + # - name: foo + # mountPath: "/etc/foo" + # readOnly: true + + nodeSelector: {} + + tolerations: [] + + affinity: {} + +worker: + # This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ + replicaCount: 1 + + # This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ + image: + repository: prowlercloud/prowler-api + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + + command: + - /home/prowler/docker-entrypoint.sh + args: + - worker + + # This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + imagePullSecrets: [] + + # This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ + serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + + # This is for setting Kubernetes Annotations to a Pod. + # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + podAnnotations: {} + # This is for setting Kubernetes Labels to a Pod. + # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + podLabels: {} + + podSecurityContext: {} + # fsGroup: 2000 + + securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + + resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + # This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + livenessProbe: {} + readinessProbe: {} + + # This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 10 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + + # Additional volumes on the output Deployment definition. + volumes: [] + # - name: foo + # secret: + # secretName: mysecret + # optional: false + + # Additional volumeMounts on the output Deployment definition. + volumeMounts: [] + # - name: foo + # mountPath: "/etc/foo" + # readOnly: true + + nodeSelector: {} + + tolerations: [] + + affinity: {} + + # KEDA ScaledObject configuration + keda: + # -- Set to `true` to enable KEDA for the worker pods + # Note: When both KEDA and HPA are enabled, the deployment will fail. + enabled: false + # -- The minimum number of replicas to use for the worker pods + minReplicas: 1 + # -- The maximum number of replicas to use for the worker pods + maxReplicas: 2 + # -- The polling interval in seconds for checking metrics + pollingInterval: 30 + # -- The cooldown period in seconds for scaling + cooldownPeriod: 120 + # -- The trigger type for scaling (cpu or memory) + triggerType: "postgresql" + # -- The target utilization percentage for the worker pods + value: "50" + +worker_beat: + # This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ + replicaCount: 1 + + # This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ + image: + repository: prowlercloud/prowler-api + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + + command: + - ../docker-entrypoint.sh + args: + - beat + + # This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + imagePullSecrets: [] + + # This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ + serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + + # This is for setting Kubernetes Annotations to a Pod. + # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + podAnnotations: {} + # This is for setting Kubernetes Labels to a Pod. + # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + podLabels: {} + + podSecurityContext: {} + # fsGroup: 2000 + + securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + + resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + # This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + livenessProbe: {} + readinessProbe: {} + + # Additional volumes on the output Deployment definition. + volumes: [] + # - name: foo + # secret: + # secretName: mysecret + # optional: false + + # Additional volumeMounts on the output Deployment definition. + volumeMounts: [] + # - name: foo + # mountPath: "/etc/foo" + # readOnly: true + + nodeSelector: {} + + tolerations: [] + + affinity: {} + +postgresql: + # -- Enable PostgreSQL deployment (via Bitnami Helm Chart). If you want to use an external Postgres server (or a managed one), set this to false + # If enabled, it will create a Secret with the credentials. + # Otherwise, create a secret with the following and add it to the api deployment: + # - POSTGRES_HOST + # - POSTGRES_PORT + # - POSTGRES_ADMIN_USER - Existing user in charge of migrations, tables, permissions, RLS + # - POSTGRES_ADMIN_PASSWORD + # - POSTGRES_USER - Will be created by ADMIN_USER + # - POSTGRES_PASSWORD + # - POSTGRES_DB - Existing DB + enabled: true + image: + repository: "bitnami/postgresql" + auth: + database: prowler_db + username: prowler + +valkey: + # If enabled, it will create a Secret with the following. + # Otherwise, create a secret with + # - VALKEY_HOST + # - VALKEY_PORT + # - VALKEY_DB + enabled: true + +neo4j: + enabled: true + + neo4j: + name: prowler-neo4j + edition: community + + # The name of the secret containing the Neo4j password with the key NEO4J_PASSWORD + passwordFromSecret: prowler-secret + + # Disable lookups during helm template rendering (required for ArgoCD) + disableLookups: true + + volumes: + data: + mode: defaultStorageClass + + services: + neo4j: + enabled: false + + # Neo4j Configuration (yaml format) + config: + dbms_security_procedures_allowlist: "apoc.*" + dbms_security_procedures_unrestricted: "apoc.*" + + apoc_config: + apoc.export.file.enabled: "true" + apoc.import.file.enabled: "true" + apoc.import.file.use_neo4j_config: "true" diff --git a/dashboard/compliance/cis_3_1_oraclecloud.py b/dashboard/compliance/cis_3_1_oraclecloud.py new file mode 100644 index 0000000000..7d51acf0f4 --- /dev/null +++ b/dashboard/compliance/cis_3_1_oraclecloud.py @@ -0,0 +1,41 @@ +import warnings + +from dashboard.common_methods import get_section_containers_cis + +warnings.filterwarnings("ignore") + + +def get_table(data): + """ + Generate CIS OCI Foundations Benchmark v3.1 compliance table. + + Args: + data: DataFrame containing compliance check results with columns: + - REQUIREMENTS_ID: CIS requirement ID (e.g., "1.1", "2.1") + - REQUIREMENTS_DESCRIPTION: Description of the requirement + - REQUIREMENTS_ATTRIBUTES_SECTION: CIS section name + - CHECKID: Prowler check identifier + - STATUS: Check status (PASS/FAIL) + - REGION: OCI region + - ACCOUNTID: OCI tenancy OCID (renamed from TENANCYID) + - RESOURCEID: Resource OCID or identifier + + Returns: + Section containers organized by CIS sections for dashboard display + """ + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_DESCRIPTION", + "REQUIREMENTS_ATTRIBUTES_SECTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_cis( + aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION" + ) diff --git a/dashboard/compliance/csa_ccm_4_0_alibabacloud.py b/dashboard/compliance/csa_ccm_4_0_alibabacloud.py new file mode 100644 index 0000000000..346576729d --- /dev/null +++ b/dashboard/compliance/csa_ccm_4_0_alibabacloud.py @@ -0,0 +1,31 @@ +import warnings + +from dashboard.common_methods import get_section_containers_kisa_ismsp + +warnings.filterwarnings("ignore") + + +def get_table(data): + data["REQUIREMENTS_ID"] = ( + data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"] + ) + + data["REQUIREMENTS_ID"] = data["REQUIREMENTS_ID"].apply( + lambda x: x[:150] + "..." if len(str(x)) > 150 else x + ) + + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_ATTRIBUTES_SECTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_kisa_ismsp( + aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID" + ) diff --git a/dashboard/compliance/csa_ccm_4_0_aws.py b/dashboard/compliance/csa_ccm_4_0_aws.py new file mode 100644 index 0000000000..346576729d --- /dev/null +++ b/dashboard/compliance/csa_ccm_4_0_aws.py @@ -0,0 +1,31 @@ +import warnings + +from dashboard.common_methods import get_section_containers_kisa_ismsp + +warnings.filterwarnings("ignore") + + +def get_table(data): + data["REQUIREMENTS_ID"] = ( + data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"] + ) + + data["REQUIREMENTS_ID"] = data["REQUIREMENTS_ID"].apply( + lambda x: x[:150] + "..." if len(str(x)) > 150 else x + ) + + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_ATTRIBUTES_SECTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_kisa_ismsp( + aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID" + ) diff --git a/dashboard/compliance/csa_ccm_4_0_azure.py b/dashboard/compliance/csa_ccm_4_0_azure.py new file mode 100644 index 0000000000..346576729d --- /dev/null +++ b/dashboard/compliance/csa_ccm_4_0_azure.py @@ -0,0 +1,31 @@ +import warnings + +from dashboard.common_methods import get_section_containers_kisa_ismsp + +warnings.filterwarnings("ignore") + + +def get_table(data): + data["REQUIREMENTS_ID"] = ( + data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"] + ) + + data["REQUIREMENTS_ID"] = data["REQUIREMENTS_ID"].apply( + lambda x: x[:150] + "..." if len(str(x)) > 150 else x + ) + + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_ATTRIBUTES_SECTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_kisa_ismsp( + aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID" + ) diff --git a/dashboard/compliance/csa_ccm_4_0_gcp.py b/dashboard/compliance/csa_ccm_4_0_gcp.py new file mode 100644 index 0000000000..346576729d --- /dev/null +++ b/dashboard/compliance/csa_ccm_4_0_gcp.py @@ -0,0 +1,31 @@ +import warnings + +from dashboard.common_methods import get_section_containers_kisa_ismsp + +warnings.filterwarnings("ignore") + + +def get_table(data): + data["REQUIREMENTS_ID"] = ( + data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"] + ) + + data["REQUIREMENTS_ID"] = data["REQUIREMENTS_ID"].apply( + lambda x: x[:150] + "..." if len(str(x)) > 150 else x + ) + + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_ATTRIBUTES_SECTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_kisa_ismsp( + aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID" + ) diff --git a/dashboard/compliance/csa_ccm_4_0_oraclecloud.py b/dashboard/compliance/csa_ccm_4_0_oraclecloud.py new file mode 100644 index 0000000000..346576729d --- /dev/null +++ b/dashboard/compliance/csa_ccm_4_0_oraclecloud.py @@ -0,0 +1,31 @@ +import warnings + +from dashboard.common_methods import get_section_containers_kisa_ismsp + +warnings.filterwarnings("ignore") + + +def get_table(data): + data["REQUIREMENTS_ID"] = ( + data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"] + ) + + data["REQUIREMENTS_ID"] = data["REQUIREMENTS_ID"].apply( + lambda x: x[:150] + "..." if len(str(x)) > 150 else x + ) + + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_ATTRIBUTES_SECTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_kisa_ismsp( + aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID" + ) diff --git a/dashboard/pages/compliance.py b/dashboard/pages/compliance.py index f944f7f098..20395539e5 100644 --- a/dashboard/pages/compliance.py +++ b/dashboard/pages/compliance.py @@ -284,6 +284,11 @@ def display_data( # Rename the column LOCATION to REGION for Alibaba Cloud if "alibabacloud" in analytics_input: data = data.rename(columns={"LOCATION": "REGION"}) + + # Rename the column TENANCYID to ACCOUNTID for Oracle Cloud + if "oraclecloud" in analytics_input: + data.rename(columns={"TENANCYID": "ACCOUNTID"}, inplace=True) + # Filter the chosen level of the CIS if is_level_1: data = data[data["REQUIREMENTS_ATTRIBUTES_PROFILE"].str.contains("Level 1")] diff --git a/dashboard/pages/overview.py b/dashboard/pages/overview.py index e2502c9aad..665aa8e195 100644 --- a/dashboard/pages/overview.py +++ b/dashboard/pages/overview.py @@ -259,6 +259,8 @@ else: accounts.append(account + " - K8S") if "alibabacloud" in list(data[data["ACCOUNT_UID"] == account]["PROVIDER"]): accounts.append(account + " - ALIBABACLOUD") + if "oraclecloud" in list(data[data["ACCOUNT_UID"] == account]["PROVIDER"]): + accounts.append(account + " - OCI") account_dropdown = create_account_dropdown(accounts) @@ -306,6 +308,8 @@ else: services.append(service + " - M365") if "alibabacloud" in list(data[data["SERVICE_NAME"] == service]["PROVIDER"]): services.append(service + " - ALIBABACLOUD") + if "oraclecloud" in list(data[data["SERVICE_NAME"] == service]["PROVIDER"]): + services.append(service + " - OCI") services = ["All"] + services services = [ @@ -767,6 +771,8 @@ def filter_data( all_account_ids.append(account) if "alibabacloud" in list(data[data["ACCOUNT_UID"] == account]["PROVIDER"]): all_account_ids.append(account) + if "oraclecloud" in list(data[data["ACCOUNT_UID"] == account]["PROVIDER"]): + all_account_ids.append(account) all_account_names = [] if "ACCOUNT_NAME" in filtered_data.columns: @@ -793,6 +799,8 @@ def filter_data( data[data["ACCOUNT_UID"] == item]["PROVIDER"] ): cloud_accounts_options.append(item + " - ALIBABACLOUD") + if "oraclecloud" in list(data[data["ACCOUNT_UID"] == item]["PROVIDER"]): + cloud_accounts_options.append(item + " - OCI") if "ACCOUNT_NAME" in filtered_data.columns: if "azure" in list(data[data["ACCOUNT_NAME"] == item]["PROVIDER"]): cloud_accounts_options.append(item + " - AZURE") @@ -925,6 +933,10 @@ def filter_data( filtered_data[filtered_data["SERVICE_NAME"] == item]["PROVIDER"] ): service_filter_options.append(item + " - ALIBABACLOUD") + if "oraclecloud" in list( + filtered_data[filtered_data["SERVICE_NAME"] == item]["PROVIDER"] + ): + service_filter_options.append(item + " - OCI") # Filter Service if service_values == ["All"]: @@ -1124,6 +1136,7 @@ def filter_data( config={"displayModeBar": False}, ) table = dcc.Graph(figure=fig, config={"displayModeBar": False}) + table_row_options = [] else: # Status Pie Chart diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index b5de1af83f..9ab59f017d 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -144,6 +144,10 @@ services: condition: service_healthy neo4j: condition: service_healthy + ulimits: + nofile: + soft: 65536 + hard: 65536 entrypoint: - "/home/prowler/docker-entrypoint.sh" - "worker" @@ -166,6 +170,10 @@ services: condition: service_healthy neo4j: condition: service_healthy + ulimits: + nofile: + soft: 65536 + hard: 65536 entrypoint: - "../docker-entrypoint.sh" - "beat" diff --git a/docker-compose.yml b/docker-compose.yml index 992a753ac0..798d1ecaba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -117,6 +117,10 @@ services: condition: service_healthy postgres: condition: service_healthy + ulimits: + nofile: + soft: 65536 + hard: 65536 entrypoint: - "/home/prowler/docker-entrypoint.sh" - "worker" @@ -131,6 +135,10 @@ services: condition: service_healthy postgres: condition: service_healthy + ulimits: + nofile: + soft: 65536 + hard: 65536 entrypoint: - "../docker-entrypoint.sh" - "beat" diff --git a/docs/docs.json b/docs/docs.json index 2f190ac36d..8305388369 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -255,6 +255,12 @@ "user-guide/providers/cloudflare/authentication" ] }, + { + "group": "Image", + "pages": [ + "user-guide/providers/image/getting-started-image" + ] + }, { "group": "LLM", "pages": [ diff --git a/docs/images/prowler-app/saml/okta-app-assignments.png b/docs/images/prowler-app/saml/okta-app-assignments.png new file mode 100644 index 0000000000..3881e646fc Binary files /dev/null and b/docs/images/prowler-app/saml/okta-app-assignments.png differ diff --git a/docs/images/prowler-app/saml/okta-user-profile-attributes.png b/docs/images/prowler-app/saml/okta-user-profile-attributes.png new file mode 100644 index 0000000000..beb6781b2c Binary files /dev/null and b/docs/images/prowler-app/saml/okta-user-profile-attributes.png differ diff --git a/docs/images/prowler-app/saml/okta-user-profile-name.png b/docs/images/prowler-app/saml/okta-user-profile-name.png new file mode 100644 index 0000000000..2a08d3437b Binary files /dev/null and b/docs/images/prowler-app/saml/okta-user-profile-name.png differ diff --git a/docs/introduction.mdx b/docs/introduction.mdx index eee557b4fc..c354f2802d 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -36,7 +36,9 @@ The supported providers right now are: | [Cloudflare](/user-guide/providers/cloudflare/getting-started-cloudflare) | Official | Accounts | 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 | CLI | | [LLM](/user-guide/providers/llm/getting-started-llm) | Official | Models | CLI | +| [Image](/user-guide/providers/image/getting-started-image) | Official | Container Images | CLI | | **NHN** | Unofficial | Tenants | CLI | For more information about the checks and compliance of each provider visit [Prowler Hub](https://hub.prowler.com). diff --git a/docs/troubleshooting.mdx b/docs/troubleshooting.mdx index cd3da32946..a6a28c0ad6 100644 --- a/docs/troubleshooting.mdx +++ b/docs/troubleshooting.mdx @@ -49,15 +49,13 @@ AWS_PROFILE=prowler-profile - If you are scanning multiple AWS accounts, you may need to add multiple profiles to your AWS config. Note that this workaround is mainly for local testing; for production or multi-account setups, follow the [CloudFormation Template guide](https://github.com/prowler-cloud/prowler/issues/7745) and ensure the correct IAM roles and permissions are set up in each account. -### Scans complete but reports are missing or compliance data is empty (`Too many open files` error) +### Scans Complete but Reports Are Missing or Compliance Data Is Empty (`Too many open files` Error) -When running Prowler App via Docker Compose, you may encounter situations where scans complete successfully but reports are not available for download, compliance data shows as empty, or you see 404 errors when trying to access scan reports. Checking the `worker` container logs may reveal errors like `[Errno 24] Too many open files`. +When running Prowler App via Docker Compose, scans may complete successfully but reports are not available for download, compliance data shows as empty, or 404 errors appear when trying to access scan reports. Checking the `worker` container logs may reveal errors like `[Errno 24] Too many open files`. -This issue occurs because the default file descriptor limits in Docker containers are too low for Prowler's operations. +This issue occurs because the default file descriptor limits in Docker containers are too low for Prowler's operations. The default `docker-compose.yml` already includes `ulimits` configuration with `nofile` set to `65536` for the `worker` and `worker-beat` services to prevent this issue. -**Solution:** - -Add `ulimits` configuration to the `worker` and `worker-beat` services in your `docker-compose.yaml`: +If a custom `docker-compose.yml` is being used or the default configuration has been modified, ensure the `ulimits` configuration is present in both the `worker` and `worker-beat` services: ```yaml services: @@ -76,17 +74,13 @@ services: # ... rest of service configuration ``` -After making these changes, restart your Docker Compose stack: +After making these changes, restart the Docker Compose stack: ```bash docker compose down docker compose up -d ``` - -We are evaluating adding these values to the default `docker-compose.yml` to avoid this issue in future releases. - - ### API Container Fails to Start with JWT Key Permission Error See [GitHub Issue #8897](https://github.com/prowler-cloud/prowler/issues/8897) for more details. @@ -164,3 +158,44 @@ When these environment variables are set, the API will use them directly instead A fix addressing this permission issue is being evaluated in [PR #9953](https://github.com/prowler-cloud/prowler/pull/9953). + +### SAML/OAuth ACS URL Incorrect When Running Behind a Proxy or Load Balancer + +See [GitHub Issue #9724](https://github.com/prowler-cloud/prowler/issues/9724) for more details. + +When running Prowler behind a reverse proxy (nginx, Traefik, etc.) or load balancer, the SAML ACS (Assertion Consumer Service) URL or OAuth callback URLs may be incorrectly generated using the internal container hostname (e.g., `http://prowler-api:8080/...`) instead of your external domain URL (e.g., `https://prowler.example.com/...`). + +**Root Cause:** + +Next.js environment variables prefixed with `NEXT_PUBLIC_` are **bundled at build time**, not runtime. The pre-built Docker images from Docker Hub (`prowlercloud/prowler-ui:stable`) are built with default internal URLs. Simply setting `NEXT_PUBLIC_API_BASE_URL` in your `.env` file or environment variables and restarting the container will **NOT** work because these values are already compiled into the JavaScript bundle. + +**Solution:** + +You must **rebuild** the UI Docker image with your external URL: + +```bash +# Clone the repository (if you haven't already) +git clone https://github.com/prowler-cloud/prowler.git +cd prowler/ui + +# Build with your external URL as a build argument +docker build \ + --build-arg NEXT_PUBLIC_API_BASE_URL=https://prowler.example.com/api/v1 \ + --build-arg NEXT_PUBLIC_API_DOCS_URL=https://prowler.example.com/api/v1/docs \ + -t prowler-ui-custom:latest \ + --target prod \ + . +``` + +Then update your `docker-compose.yml` to use your custom image instead of the pre-built one: + +```yaml +services: + ui: + image: prowler-ui-custom:latest # Use your custom-built image + # ... rest of configuration +``` + + +The `NEXT_PUBLIC_` prefix is a Next.js convention that exposes environment variables to the browser. Since the browser bundle is compiled during `docker build`, these variables must be provided as build arguments, not runtime environment variables. + diff --git a/docs/user-guide/providers/github/authentication.mdx b/docs/user-guide/providers/github/authentication.mdx index d76a0a1cb5..1ea4f49610 100644 --- a/docs/user-guide/providers/github/authentication.mdx +++ b/docs/user-guide/providers/github/authentication.mdx @@ -1,230 +1,447 @@ --- -title: 'GitHub Authentication in Prowler' +title: "GitHub Authentication in Prowler" --- -Prowler supports multiple methods to [authenticate with GitHub](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api). These include: +Prowler for GitHub offers multiple authentication types across Prowler Cloud and Prowler CLI. -- [Personal Access Token (PAT)](/user-guide/providers/github/authentication#personal-access-token-pat) -- [OAuth App Token](/user-guide/providers/github/authentication#oauth-app-token) -- [GitHub App Credentials](/user-guide/providers/github/authentication#github-app-credentials) +## Common Setup -This flexibility enables scanning and analysis of GitHub accounts, including repositories, organizations, and applications, using the method that best suits the use case. +### Authentication Methods Overview -## Personal Access Token (PAT) +Prowler offers three authentication methods. Fine-Grained Personal Access Tokens are recommended for most use cases. + +| Method | Best For | Key Benefit | +|--------|----------|-------------| +| [**Fine-Grained Personal Access Token**](#fine-grained-personal-access-token-recommended) | Individual users, quick setup | Simple, user-scoped access | +| [**GitHub App**](#github-app-credentials) | Organizations, automation, CI/CD | Organization-scoped, no personal account dependency | +| [**OAuth App Token**](#oauth-app-token) | Delegated user authorization | User-consented access flows | + + +**Which should I choose?** + +- **Personal scanning or quick setup**: Use Fine-Grained PAT +- **Organization-wide scanning or CI/CD pipelines**: Use GitHub App (recommended for production) +- **Building apps with user authorization**: Use OAuth App + -Personal Access Tokens provide the simplest GitHub authentication method, but it can only access resources owned by a single user or organization. -**Classic Tokens Deprecated** +**Classic Personal Access Tokens** -GitHub has deprecated Personal Access Tokens (classic) in favor of fine-grained Personal Access Tokens. We recommend using fine-grained tokens as they provide better security through more granular permissions and resource-specific access control. +GitHub has deprecated classic Personal Access Tokens. Use Fine-Grained Tokens instead - they provide granular permission control and better security. -#### **Option 1: Create a Fine-Grained Personal Access Token (Recommended)** -1. **Navigate to GitHub Settings** - - Open [GitHub](https://github.com) and sign in - - Click the profile picture in the top right corner - - Select "Settings" from the dropdown menu +### Required Permissions -2. **Access Developer Settings** - - Scroll down the left sidebar - - Click "Developer settings" +Required permissions depend on the scan scope: user repositories, organization repositories, or both. -3. **Generate Fine-Grained Token** - - Click "Personal access tokens" - - Select "Fine-grained tokens" - - Click "Generate new token" +#### Repository Permissions -4. **Configure Token Settings** - - **Token name**: Give your token a descriptive name (e.g., "Prowler Security Scanner") - - **Expiration**: Set an appropriate expiration date (recommended: 90 days or less) - - **Repository access**: Choose "All repositories" or "Only select repositories" based on your needs +Required for scanning repository security settings: - - **Public repositories** +| Permission | Access Level | Purpose | Checks Enabled | +|------------|-------------|---------|----------------| +| **Administration** | Read | Branch protection, security settings | All branch protection checks, secret scanning status | +| **Contents** | Read | File existence checks | `repository_public_has_securitymd_file`, `repository_has_codeowners_file` | +| **Metadata** | Read | Basic repository information | All checks (automatically granted) | +| **Dependabot alerts** | Read | Dependency vulnerability scanning | `repository_dependency_scanning_enabled` | - Even if you select 'Only select repositories', the token will have access to the public repositories that you own or are a member of. + +**Pull requests permission is optional.** It's only needed if you want to audit PR-specific settings beyond what branch protection provides. + - -5. **Configure Token Permissions** - To enable Prowler functionality, configure the following permissions: +#### Organization Permissions - - **Repository permissions:** - - **Administration**: Read-only access - - **Contents**: Read-only access - - **Metadata**: Read-only access - - **Pull requests**: Read-only access +Required for scanning organization-level security settings: - - **Organization permissions:** - - **Administration**: Read-only access - - **Members**: Read-only access + +**For Fine-Grained PATs:** Organization permissions only appear when the **Resource Owner** is set to an organization (not your personal account). - - **Account permissions:** - - **Email addresses**: Read-only access +**For GitHub Apps:** Organization permissions are configured during app creation and apply to all organizations where the app is installed. + -6. **Copy and Store the Token** - - Copy the generated token immediately (GitHub displays tokens only once) - - Store tokens securely using environment variables +| Permission | Access Level | Purpose | Checks Enabled | +|------------|-------------|---------|----------------| +| **Administration** | Read | Organization security policies | `organization_members_mfa_required`, `organization_repository_creation_limited`, `organization_default_repository_permission_strict` | +| **Members** | Read | Member access reviews | Organization membership auditing | -![GitHub Personal Access Token Permissions](/images/providers/github-pat-permissions.png) +#### Account Permissions (Fine-Grained PAT only) -#### **Option 2: Create a Classic Personal Access Token (Not Recommended)** +| Permission | Access Level | Purpose | +|------------|-------------|---------| +| **Email addresses** | Read | User email verification | + + +GitHub Apps don't have account-level permissions - they operate at the organization/repository level. + + +### Permissions and Check Coverage + +With the **Read-only permissions** listed above, Prowler can run: + +| Check Category | Coverage | Notes | +|----------------|----------|-------| +| Branch protection checks (12 checks) | ✅ Full | Signed commits, status checks, PR reviews, etc. | +| Repository security checks | ✅ Full | Secret scanning, Dependabot, SECURITY.md, CODEOWNERS | +| Organization checks (3 checks) | ✅ Full | MFA, repo creation policies, default permissions | +| Compliance frameworks | ✅ Full | CIS GitHub Benchmark and others | +| Merge settings (`delete_branch_on_merge`) | ⚠️ MANUAL | Requires write permission (see below) | + +**Check that returns `MANUAL` status with Read-only permissions:** +- `repository_branch_delete_on_merge_enabled` -**Security Risk** +**About Write Permissions** -Classic tokens provide broad permissions that may exceed what Prowler actually needs. Use fine-grained tokens instead for better security. +The `delete_branch_on_merge` setting is only returned by the GitHub API when the token has **Administration: Read and write** permission. + +**Granting Write permissions is not recommended under any circumstances:** +- Token can modify repository settings +- Token can change branch protection rules +- Violates the principle of least privilege + +**Recommendation:** Accept `MANUAL` status for this single check rather than granting write access. This limitation applies equally to Fine-Grained PATs and GitHub Apps. + + +### Step-by-Step Permission Assignment + +#### Fine-Grained Personal Access Token (Recommended for Individual Use) + +**Benefits of Fine-Grained Tokens** + +Fine-Grained Personal Access Tokens are ideal for: +- **Individual users** scanning their own repositories +- **Quick setup** without app registration overhead +- **Temporary access** with mandatory expiration +- **Repository-specific access** when you only need to scan certain repos + +**Create a Fine-Grained Token:** + +1. Navigate to **GitHub Settings** > **Developer settings**. + +2. Click **Personal access tokens** > **Fine-grained tokens** > **Generate new token**. + +3. Configure basic settings: + - **Token name**: Descriptive name (e.g., "Prowler Security Scanner") + - **Expiration**: 90 days or less (recommended) + - **Resource owner**: + - Personal account (for user repositories) + - Organization name (for organization scanning - requires admin approval) + - **Repository access**: "All repositories" (recommended) + +4. Configure **Repository permissions**: + - Administration: Read + - Contents: Read + - Metadata: Read (auto-selected) + - Dependabot alerts: Read + +5. Configure **Organization permissions** (only appears when Resource owner is an organization): + - Administration: Read + - Members: Read + +6. Configure **Account permissions**: + - Email addresses: Read (optional) + +7. Click **Generate token** and copy the token immediately. + + +GitHub shows the token only once. Store it securely. -1. **Navigate to GitHub Settings** - - Open [GitHub](https://github.com) and sign in - - Click the profile picture in the top right corner - - Select "Settings" from the dropdown menu -2. **Access Developer Settings** - - Scroll down the left sidebar - - Click "Developer settings" +![GitHub Fine-Grained Token Permissions](/images/providers/github-pat-permissions.png) -3. **Generate Classic Token** - - Click "Personal access tokens" - - Select "Tokens (classic)" - - Click "Generate new token" +#### OAuth App Token -4. **Configure Token Permissions** - To enable Prowler functionality, configure the following scopes: - - `repo`: Full control of private repositories (includes `repo:status` and `repo:contents`) - - `read:org`: Read organization and team membership - - `read:user`: Read user profile data - - `security_events`: Access security events (secret scanning and Dependabot alerts) - - `read:enterprise`: Read enterprise data (if using GitHub Enterprise) +**Recommended OAuth App Use Cases:** -5. **Copy and Store the Token** - - Copy the generated token immediately (GitHub displays tokens only once) - - Store tokens securely using environment variables +Use OAuth App Tokens when building applications that need delegated user permissions and explicit user authorization. -## OAuth App Token +**OAuth Scopes:** -OAuth Apps enable applications to act on behalf of users with explicit consent. +- `repo`: Full control of repositories +- `read:org`: Read organization and team membership +- `read:user`: Read user profile data -### Create an OAuth App Token +**Create an OAuth App:** -1. **Navigate to Developer Settings** - - Open GitHub Settings → Developer settings - - Click "OAuth Apps" +1. Navigate to **GitHub Settings** > **Developer settings** > **OAuth Apps**. -2. **Register New Application** - - Click "New OAuth App" - - Complete the required fields: - - **Application name**: Descriptive application name - - **Homepage URL**: Application homepage - - **Authorization callback URL**: User redirection URL after authorization +2. Click **New OAuth App** and complete: + - Application name + - Homepage URL + - Authorization callback URL -3. **Obtain Authorization Code** - - Request authorization code (replace `{app_id}` with the application ID): +3. Obtain authorization code: ``` https://github.com/login/oauth/authorize?client_id={app_id} ``` -4. **Exchange Code for Token** - - Exchange authorization code for access token (replace `{app_id}`, `{secret}`, and `{code}`): +4. Exchange authorization code for access token: ``` https://github.com/login/oauth/access_token?code={code}&client_id={app_id}&client_secret={secret} ``` -## GitHub App Credentials -GitHub Apps provide the recommended integration method for accessing multiple repositories or organizations. +#### GitHub App Credentials -### Create a GitHub App + +**When to Use GitHub Apps** -1. **Navigate to Developer Settings** - - Open GitHub Settings → Developer settings - - Click "GitHub Apps" +GitHub Apps are ideal for: +- **Organization-wide scanning** without tying access to a personal account +- **CI/CD pipelines** where you need machine identity (not user-based) +- **Multi-organization setups** with centralized app management +- **Audit compliance** where you need to track app-level access separately from users -2. **Create New GitHub App** - - Click "New GitHub App" - - Complete the required fields: - - **GitHub App name**: Choose a unique, descriptive name (e.g., "Prowler Security Scanner") - - **Homepage URL**: Enter your organization's website or the Prowler documentation URL (e.g., `https://prowler.com` or `https://docs.prowler.com`). This is just for reference and doesn't affect functionality. - - **Webhook URL**: Leave blank or uncheck "Active" under Webhook. Prowler doesn't require webhooks since it performs on-demand scans rather than responding to GitHub events. - - **Webhook secret**: Leave blank (not needed for Prowler) - - **Permissions**: Configure in the next step (see below) +GitHub Apps use the same permission model as Fine-Grained PATs - both provide full access to all Prowler checks. + - - **About Homepage URL and Webhooks** +**GitHub App Permissions:** - The Homepage URL is purely informational and can be any valid URL - it's just displayed to users who view the app. Use your company website, your GitHub organization URL, or even `https://docs.prowler.com`. +If a GitHub App is required: - Webhooks are **not required** for Prowler. Since Prowler performs on-demand security scans when you run it (rather than automatically responding to GitHub events), you can safely disable webhooks or leave the URL blank. - +**Repository permissions:** -3. **Configure Permissions** - To enable Prowler functionality, configure these permissions: - - **Repository permissions**: - - Contents (Read) - - Metadata (Read) - - Pull requests (Read) - - **Organization permissions**: - - Members (Read) - - Administration (Read) - - **Account permissions**: - - Email addresses (Read) +| Permission | Access Level | Purpose | Checks Enabled | +|------------|-------------|---------|----------------| +| **Administration** | Read | Branch protection, security settings | All branch protection checks, `repository_secret_scanning_enabled` | +| **Contents** | Read | File existence checks | `repository_public_has_securitymd_file`, `repository_has_codeowners_file` | +| **Metadata** | Read | Basic repository information | All checks (automatically granted) | +| **Dependabot alerts** | Read | Dependency vulnerability scanning | `repository_dependency_scanning_enabled` | -4. **Where can this GitHub App be installed?** - - Select "Any account" to be able to install the GitHub App in any organization. +**Organization permissions:** -5. **Generate Private Key** - - Scroll to the "Private keys" section after app creation - - Click "Generate a private key" - - Download the `.pem` file and store securely +| Permission | Access Level | Purpose | Checks Enabled | +|------------|-------------|---------|----------------| +| **Administration** | Read | Organization security policies | `organization_members_mfa_required`, `organization_repository_creation_limited`, `organization_default_repository_permission_strict` | +| **Members** | Read | Member access reviews | Organization membership auditing | -5. **Record App ID** - - Locate the App ID at the top of the GitHub App settings page +**Create a GitHub App:** -### Install the GitHub App +1. Navigate to **GitHub Settings** > **Developer settings** > **GitHub Apps**. -1. **Install Application** - - Navigate to GitHub App settings - - Click "Install App" in the left sidebar - - Select the target account/organization - - Choose specific repositories or select "All repositories" +2. Click **New GitHub App** and complete: + - **GitHub App name**: Descriptive name (e.g., "Prowler Security Scanner") + - **Homepage URL**: Your organization's URL or Prowler documentation + - **Webhook**: Uncheck "Active" (Prowler doesn't need webhooks) -## Best Practices +3. Configure **Repository permissions** (see table above): + - Administration: Read + - Contents: Read + - Metadata: Read (auto-selected) + - Dependabot alerts: Read -### Security Considerations +4. Configure **Organization permissions** (see table above): + - Administration: Read + - Members: Read -Implement the following security measures: +5. Under **Where can this GitHub App be installed?**, select: + - "Only on this account" for single-organization use + - "Any account" if you need to install across multiple organizations -- **Secure Credential Storage**: Store credentials using environment variables instead of hardcoding tokens -- **Secrets Management**: Use dedicated secrets management systems in production environments -- **Regular Token Rotation**: Rotate tokens and keys regularly -- **Least Privilege Principle**: Grant only minimum required permissions -- **Permission Auditing**: Review and audit permissions regularly -- **Token Expiration**: Set appropriate expiration times for tokens -- **Usage Monitoring**: Monitor token usage and revoke unused tokens +6. Click **Create GitHub App**. -### Authentication Method Selection +7. On the app settings page: + - Record the **App ID** (displayed at the top) + - Click **Generate a private key** and download the `.pem` file -Choose the appropriate method based on use case: +8. Install the GitHub App: + - Click **Install App** in the left sidebar + - Select target account/organization + - Choose "All repositories" or select specific repositories + - Click **Install** -- **Personal Access Token**: Individual use, testing, or simple automation -- **OAuth App Token**: Applications requiring user consent and delegation -- **GitHub App**: Production integrations, especially for organizations + +**Private Key Security** -## Troubleshooting Common Issues +Store the `.pem` private key securely. Anyone with this key can authenticate as your GitHub App. Never commit it to version control. + -### Insufficient Permissions -- Verify token/app has necessary scopes/permissions -- Check organization restrictions on third-party applications +--- -### Token Expiration -- Confirm token has not expired -- Verify fine-grained tokens have correct resource access +## Prowler Cloud Authentication + +For step-by-step setup instructions for Prowler Cloud, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cloudapp). + +### Using Personal Access Token + +1. In Prowler Cloud, navigate to **Configuration** > **Cloud Providers** > **Add Cloud Provider** > **GitHub**. + +2. Enter your GitHub Account ID (username or organization name). + +3. Select **Personal Access Token** as the authentication method. + +4. Enter your Fine-Grained Personal Access Token. + +5. Click **Verify** to test the connection, then **Save**. + +### Using OAuth App Token + +1. Follow the same steps as Personal Access Token. + +2. Select **OAuth App Token** as the authentication method. + +3. Enter your OAuth App Token. + +### Using GitHub App + +1. Follow the same steps as Personal Access Token. + +2. Select **GitHub App** as the authentication method. + +3. Enter your GitHub App ID and upload the private key (`.pem` file). + +For complete step-by-step instructions, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cloudapp). + +--- + +## Prowler CLI Authentication + +### Authentication Methods + +Prowler CLI automatically detects credentials using environment variables in this order: + +1. `GITHUB_PERSONAL_ACCESS_TOKEN` +2. `GITHUB_OAUTH_APP_TOKEN` +3. `GITHUB_APP_ID` and `GITHUB_APP_KEY` + +### Using Environment Variables (Recommended) + +```bash +# Personal Access Token (Recommended) +export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_xxxxxxxxxxxx" +prowler github + +# OAuth App Token +export GITHUB_OAUTH_APP_TOKEN="oauth_token_here" +prowler github + +# GitHub App +export GITHUB_APP_ID="123456" +export GITHUB_APP_KEY="$(cat /path/to/private-key.pem)" +prowler github +``` + +### Using CLI Flags + +```bash +# Personal Access Token +prowler github --personal-access-token ghp_xxxxxxxxxxxx + +# OAuth App Token +prowler github --oauth-app-token oauth_token_here + +# GitHub App +prowler github --github-app-id 123456 --github-app-key-path /path/to/private-key.pem +``` + +### Scan Scope + + +**Understanding Scan Scope** + +What Prowler scans depends on the invocation method: + +| Command | What Gets Scanned | Organization Checks? | +|---------|------------------|---------------------| +| `prowler github` | All accessible repositories | No | +| `prowler github --repository owner/repo` | Single repository | No | +| `prowler github --organization org-name` | Organization repos + settings | Yes | + +**Key Point:** Scanning user repositories does NOT include organization-level checks. To audit organization MFA, security policies, etc., you must use `--organization`. + + + +**Scan user repositories:** + +```bash +prowler github +prowler github --repository username/my-repo +``` + +**Scan organizations:** + +```bash +prowler github --organization org-name +prowler github --organization org1 --organization org2 +``` + +**Filter scans:** + +```bash +prowler github --severity critical +prowler github --checks repository_default_branch_protection_enabled +prowler github --compliance cis_1.0_github +``` + +For complete step-by-step instructions, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cli). + +--- + +## Troubleshooting + +### "Insufficient Permissions" Errors + +**Symptom:** Checks fail or return `MANUAL` status. + +**Solutions:** +1. Verify token has all required permissions +2. For organization scans, ensure organization approved the Fine-Grained Token +3. For merge settings checks, accept `MANUAL` status (Write permission not recommended) + +### "No Organizations Found" + +**Symptom:** Prowler doesn't find organizations even though you're a member. + +**Cause:** Fine-Grained Token's Resource Owner is set to personal account. + +**Solution:** Create a new token with Resource Owner set to the organization and get it approved by an admin. + +### Organization Checks Return `MANUAL` + +**Symptom:** Checks like `organization_members_mfa_required` return `MANUAL`. + +**Cause:** Token lacks `Organization → Administration: Read` permission. + +**Solutions:** +1. Edit token and grant `Organization → Administration: Read` +2. Ensure token's **Resource owner** is the organization (not personal account) +3. Get organization admin approval + +### Token Not Showing Organization Permissions + +**Symptom:** Can't find Organization permissions section when creating token. + +**Cause:** **Resource owner** is set to personal account. + +**Solution:** Change **Resource owner** dropdown to the organization name. Organization permissions section will appear. ### Rate Limiting -- GitHub implements API call rate limits -- Consider GitHub Apps for higher rate limits -### Organization Settings -- Some organizations restrict third-party applications -- Contact organization administrator if access is denied +**Symptom:** "API rate limit exceeded" errors. + +**Solutions:** +- Scan during off-peak hours +- Use `--repository` to scan specific repos instead of all +- Implement delays between scans + +### Token Expired or Revoked + +**Symptom:** Authentication fails with valid-looking token. + +**Solutions:** +1. Check token expiration date in GitHub settings +2. Verify token wasn't revoked +3. For Fine-Grained Tokens, check if organization approval was revoked +4. Generate a new token + +--- + +## Additional Resources + +- [GitHub REST API Authentication](https://docs.github.com/en/rest/authentication) +- [Fine-Grained Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) +- [GitHub Apps Documentation](https://docs.github.com/en/apps) +- [GitHub API Rate Limits](https://docs.github.com/en/rest/overview/rate-limits-for-the-rest-api) +- [Getting Started Guide](/user-guide/providers/github/getting-started-github) diff --git a/docs/user-guide/providers/github/getting-started-github.mdx b/docs/user-guide/providers/github/getting-started-github.mdx index 41d472df61..b074262680 100644 --- a/docs/user-guide/providers/github/getting-started-github.mdx +++ b/docs/user-guide/providers/github/getting-started-github.mdx @@ -2,91 +2,441 @@ title: 'Getting Started with GitHub' --- -## Prowler App +This guide covers setting up GitHub security scanning with Prowler. Choose a preferred interface below: + + +**Understanding GitHub Scan Scope** + +Prowler can scan either: +- **User Repositories**: All repositories owned by or accessible to a specific GitHub user +- **Organizations**: Repositories and organization-level settings + +**Important**: Scanning user repositories does NOT include organization-level checks (MFA requirements, security policies, etc.). To scan organizations, you must explicitly configure them. + + + + + + Web-based interface with centralized management + + + Command-line interface for local or automated scans + + + +--- + +## Prowler Cloud/App > Walkthrough video onboarding a GitHub Account using GitHub App. +### Prerequisites + +Before adding GitHub to Prowler Cloud/App, ensure you have: + +1. **GitHub Account Access** + - Personal GitHub account, OR + - Admin access to a GitHub organization + +2. **Authentication Credentials** + - Choose one method (see [Authentication Guide](/user-guide/providers/github/authentication)): + - **Fine-Grained Personal Access Token** (Recommended) + - OAuth App Token + - GitHub App Credentials (Not Recommended - limited data access) + ### Step 1: Access Prowler Cloud/App 1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app) -2. Go to "Configuration" > "Cloud Providers" +2. Go to **Configuration** → **Cloud Providers** ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) -3. Click "Add Cloud Provider" +3. Click **Add Cloud Provider** ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) -4. Select "GitHub" +4. Select **GitHub** ![Select GitHub](/images/providers/select-github.png) -5. Add the GitHub Account ID (username or organization name) and an optional alias, then click "Next" +### Step 2: Configure GitHub Account + +5. Add the **GitHub Account ID** and an optional alias: + - **Account ID**: Your GitHub username (e.g., `username`) or organization name (e.g., `org-name`) + - **Alias** (optional): Friendly name for this connection (e.g., `My Personal Repos` or `Prowler Org`) ![Add GitHub Account ID](/images/providers/add-github-account-id.png) -### Step 2: Choose the preferred authentication method +6. Click **Next** -6. Choose the preferred authentication method: +### Step 3: Choose Authentication Method + + +**Recommended: Fine-Grained Personal Access Token** + +**Fine-Grained Personal Access Tokens** are strongly recommended because they provide: +- Best data access for comprehensive security scanning +- Granular permission control +- Resource-specific access + +**GitHub Apps are not recommended** — they provide the most limited access to GitHub data for security scanning purposes. + + +7. Select your preferred authentication method: ![Select auth method](/images/providers/select-auth-method.png) -7. Configure the authentication method: - - + ![Configure Personal Access Token](/images/providers/auth-pat.png) - For more details on how to create a Personal Access Token, see [Authentication > Personal Access Token](/user-guide/providers/github/authentication#personal-access-token-pat). + **Recommended method** - provides the best data access for security scanning. + + 1. Enter your Fine-Grained Personal Access Token + 2. Click **Verify** to test the connection + 3. Click **Save** + + **Don't have a token yet?** See [How to create a Personal Access Token](/user-guide/providers/github/authentication#create-a-fine-grained-personal-access-token) + ![Configure OAuth App Token](/images/providers/auth-oauth.png) - For more details on how to create an OAuth App Token, see [Authentication > OAuth App Token](/user-guide/providers/github/authentication#oauth-app-token). + For applications requiring user consent and delegated permissions. + + 1. Enter your OAuth App Token + 2. Click **Verify** to test the connection + 3. Click **Save** + + **Don't have an OAuth token?** See [How to create an OAuth App Token](/user-guide/providers/github/authentication#oauth-app-token) - + + ![Configure GitHub App](/images/providers/auth-github-app.png) - For more details on how to create a GitHub App, see [Authentication > GitHub App](/user-guide/providers/github/authentication#github-app-credentials). + + **Not recommended** - most limited data access. Use only if required by organization policy. + + + 1. Enter your GitHub App ID + 2. Upload or paste your Private Key (`.pem` file) + 3. Click **Verify** to test the connection + 4. Click **Save** + + **Don't have a GitHub App?** See [How to create a GitHub App](/user-guide/providers/github/authentication#github-app-credentials) + + +8. Click **Start Scan** to begin your first security assessment + +### Step 5: View Results + +Once the scan completes, you can: +- View security findings in the dashboard +- Export results in multiple formats (JSON, CSV, HTML) +- Set up continuous scanning schedules +- Configure alerts for critical findings + +--- + ## Prowler CLI -### Automatic Login Method Detection +### Prerequisites -If no login method is explicitly provided, Prowler will automatically attempt to authenticate using environment variables in the following order of precedence: +Before running Prowler CLI for GitHub, ensure you have: + +1. **Prowler Installed** + ```bash + # Install via pip + pip install prowler + + # Or via poetry + poetry install + ``` + +2. **Authentication Credentials** + - Choose one method (see [Authentication Guide](/user-guide/providers/github/authentication)): + - **Fine-Grained Personal Access Token** (Recommended) + - OAuth App Token + - GitHub App Credentials (Not Recommended) + +### Authentication Setup + +Prowler CLI automatically detects authentication credentials using environment variables in this order: 1. `GITHUB_PERSONAL_ACCESS_TOKEN` 2. `GITHUB_OAUTH_APP_TOKEN` -3. `GITHUB_APP_ID` and `GITHUB_APP_KEY` (where the key is the content of the private key file) +3. `GITHUB_APP_ID` and `GITHUB_APP_KEY` + + + +```bash +# Personal Access Token (Recommended) +export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_xxxxxxxxxxxx" + +# OAuth App Token +export GITHUB_OAUTH_APP_TOKEN="oauth_token_here" + +# GitHub App +export GITHUB_APP_ID="123456" +export GITHUB_APP_KEY="$(cat /path/to/private-key.pem)" +``` + +Then run Prowler without additional flags: +```bash +prowler github +``` + + + +```bash +# Personal Access Token +prowler github --personal-access-token ghp_xxxxxxxxxxxx + +# OAuth App Token +prowler github --oauth-app-token oauth_token_here + +# GitHub App +prowler github --github-app-id 123456 --github-app-key-path /path/to/private-key.pem +``` + + + +**Don't have credentials yet?** See the [Authentication Guide](/user-guide/providers/github/authentication) for step-by-step instructions. + +### Scan Scope: Understanding What Gets Scanned + + +**Distinguishing User Scans from Organization Scans** + +The scan scope depends entirely on the Prowler CLI invocation method: + +| Command | What Gets Scanned | Organization Checks Included? | +|---------|------------------|-------------------------------| +| `prowler github` | All repositories the token has access to | No | +| `prowler github --repository owner/repo` | Single specified repository | No | +| `prowler github --organization org-name` | Organization repos + org settings | Yes | +| `prowler github --organization org-name --repository owner/repo` | Organization + single repository | Yes | + +**Key Points:** +- Scanning **user repositories** does NOT run organization-level checks +- To audit organization MFA, security policies, etc., the `--organization` flag is required +- Members of multiple organizations should specify each one explicitly + + + +### Scanning User Repositories + +Scan repositories owned by your user account: + +```bash +# Scan all repositories accessible to your token +prowler github + +# Scan a specific repository +prowler github --repository username/my-repo + +# Scan multiple specific repositories +prowler github --repository username/repo1 --repository username/repo2 +``` + +**What gets scanned:** +- Repository security settings +- Branch protection rules +- Secret scanning configuration +- Dependabot settings +- Organization-level policies (not included) + +### Scanning Organizations + +Scan organization repositories and organization-level security settings: + +```bash +# Scan a single organization +prowler github --organization prowler-cloud + +# Scan multiple organizations +prowler github --organization org1 --organization org2 + +# Scan organization and specific repositories within it +prowler github --organization my-org --repository my-org/critical-repo +``` + +**What gets scanned:** +- All organization repositories +- Repository security settings +- Organization MFA requirements +- Organization security policies +- Member access and permissions + +### Scan Scoping + +Scan scoping controls which repositories and organizations Prowler includes in a security assessment. By default, Prowler scans all repositories accessible to the authenticated user or organization. To limit the scan to specific repositories or organizations, use the following flags. + +#### Scanning Specific Repositories + +To restrict the scan to one or more repositories, use the `--repository` flag followed by the repository name(s) in `owner/repo-name` format: + +```console +prowler github --repository owner/repo-name +``` + +To scan multiple repositories, specify them as space-separated arguments: + +```console +prowler github --repository owner/repo-name-1 owner/repo-name-2 +``` + +#### Scanning Specific Organizations + +To restrict the scan to one or more organizations or user accounts, use the `--organization` flag: + +```console +prowler github --organization my-organization +``` + +To scan multiple organizations, specify them as space-separated arguments: + +```console +prowler github --organization org-1 org-2 +``` + +#### Scanning Specific Repositories Within an Organization + +To scan specific repositories within an organization, combine the `--organization` and `--repository` flags. The `--organization` flag qualifies unqualified repository names automatically: + +```console +prowler github --organization my-organization --repository my-repo +``` + +This scans only `my-organization/my-repo`. Fully qualified repository names (`owner/repo-name`) are also supported alongside `--organization`: + +```console +prowler github --organization my-org --repository my-repo other-owner/other-repo +``` + +In this case, `my-repo` is qualified as `my-org/my-repo`, while `other-owner/other-repo` is used as-is. -Ensure the corresponding environment variables are set up before running Prowler for automatic detection when not specifying the login method. - +The `--repository` and `--organization` flags can be combined with any authentication method. -For more details on how to set up authentication with GitHub, see [Authentication > GitHub](/user-guide/providers/github/authentication). -### Personal Access Token (PAT) +### Filtering Scans -Use this method by providing your personal access token directly. +Customize your scan scope with these options: -```console -prowler github --personal-access-token pat +```bash +# Run only critical severity checks +prowler github --severity critical + +# Run specific checks +prowler github --checks repository_default_branch_protection_enabled,organization_members_mfa_required + +# Exclude specific checks +prowler github --excluded-checks repository_archived + +# Scan with specific compliance framework +prowler github --compliance cis_1.0_github + +# Output results in specific format +prowler github --output-formats json,csv,html ``` -### OAuth App Token +### Example Workflows -Authenticate using an OAuth app token. + + +```bash +# Scan your personal repositories for critical issues +export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_xxxx" +prowler github --severity critical high +``` + -```console -prowler github --oauth-app-token oauth_token + +```bash +# Full organization scan with CIS compliance +export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_xxxx" +prowler github \ + --organization prowler-cloud \ + --compliance cis_1.0_github \ + --output-formats json,html +``` + + + +```bash +# Scan specific repository in CI pipeline +prowler github \ + --personal-access-token "$GITHUB_TOKEN" \ + --repository "$GITHUB_REPOSITORY" \ + --severity critical \ + --output-formats json + +# Exit with non-zero if critical findings +if grep -q '"Status": "FAIL".*"Severity": "critical"' prowler-output*.json; then + echo "Critical security issues found!" + exit 1 +fi +``` + + + +```bash +# Scan multiple organizations you're part of +export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_xxxx" +prowler github \ + --organization org1 \ + --organization org2 \ + --organization org3 \ + --output-formats csv +``` + + + +### Viewing Prowler CLI Scan Results + +Prowler CLI generates results in multiple formats: + +```bash +# Results are saved in ./output/ directory by default +ls output/ + +# View HTML report in browser +open output/prowler-output-*.html + +# Parse JSON results with jq +cat output/prowler-output-*.json | jq '.findings[] | select(.Status=="FAIL")' + +# Import CSV into spreadsheet +open output/prowler-output-*.csv ``` -### GitHub App Credentials -Use GitHub App credentials by specifying the App ID and the private key path. +--- -```console -prowler github --github-app-id app_id --github-app-key-path app_key_path -``` +## Next Steps + + + + Detailed permissions and token creation + + + Browse all GitHub security checks + + + CIS, NIST, and other frameworks + + + Common issues and solutions + + + +## Additional Resources + +- [GitHub REST API Documentation](https://docs.github.com/en/rest) +- [Fine-Grained Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) +- [GitHub Security Best Practices](https://docs.github.com/en/code-security) +- [Prowler CLI Reference](/getting-started/basic-usage/prowler-cli) diff --git a/docs/user-guide/providers/image/getting-started-image.mdx b/docs/user-guide/providers/image/getting-started-image.mdx new file mode 100644 index 0000000000..4fe509c2af --- /dev/null +++ b/docs/user-guide/providers/image/getting-started-image.mdx @@ -0,0 +1,197 @@ +--- +title: "Getting Started with the Image Provider" +--- + +import { VersionBadge } from "/snippets/version-badge.mdx" + +Prowler's Image provider enables comprehensive container image security scanning by integrating with [Trivy](https://trivy.dev/). This provider detects vulnerabilities, exposed secrets, and misconfigurations in container images, converting Trivy findings into Prowler's standard reporting format for unified security assessment. + +## How It Works + +* **Trivy integration:** Prowler leverages [Trivy](https://trivy.dev/) to scan container images for vulnerabilities, secrets, misconfigurations, and license issues. +* **Trivy required:** Trivy must be installed and available in the system PATH before running any scan. +* **Authentication:** No registry authentication is required for public images. For private registries, configure Docker credentials via `docker login` before scanning. +* **Output formats:** Results are output in the same formats as other Prowler providers (CSV, JSON, HTML, etc.). + +## Prowler CLI + + + + +The Image provider is currently available in Prowler CLI only. + + +### Install Trivy + +Install Trivy using one of the following methods: + + + + ```bash + brew install trivy + ``` + + + ```bash + sudo apt-get install trivy + ``` + + + ```bash + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + ``` + + + +For additional installation methods, see the [Trivy installation guide](https://trivy.dev/latest/getting-started/installation/). + + +### Supported Scanners + +Prowler CLI supports the following scanners: + +* [Vulnerability](https://trivy.dev/docs/latest/guide/scanner/vulnerability/) +* [Secret](https://trivy.dev/docs/latest/guide/scanner/secret/) +* [Misconfiguration](https://trivy.dev/docs/latest/guide/scanner/misconfiguration/) +* [License](https://trivy.dev/docs/latest/guide/scanner/license/) + +By default, only vulnerability and secret scanners run during a scan. To specify which scanners to use, refer to the [Specify Scanners](#specify-scanners) section below. + +### Scan Container Images + +Use the `image` argument to run Prowler with the Image provider. Specify the images to scan using the `-I` flag or an image list file. + +#### Scan a Single Image + +To scan a single container image: + +```bash +prowler image -I alpine:3.18 +``` + +#### Scan Multiple Images + +To scan multiple images, repeat the `-I` flag: + +```bash +prowler image -I nginx:latest -I redis:7 -I python:3.12-slim +``` + +#### Scan From an Image List File + +For large-scale scanning, provide a file containing one image per line: + +```bash +prowler image --image-list images.txt +``` + +The file supports comments (lines starting with `#`) and blank lines: + +```text +# Production images +nginx:1.25 +redis:7-alpine + +# Development images +python:3.12-slim +node:20-bookworm +``` + + +Image list files are limited to a maximum of 10,000 lines. Individual image names exceeding 500 characters are automatically skipped with a warning. + + + +Image names must follow the Open Container Initiative (OCI) reference format. Valid names start with an alphanumeric character and contain only letters, digits, periods, hyphens, underscores, slashes, colons, and `@` symbols. Names containing shell metacharacters (`;`, `|`, `&`, `$`, `` ` ``) are rejected to prevent command injection. + + +Valid examples: +* **Standard tag:** `alpine:3.18` +* **Custom registry:** `myregistry.io/myapp:v1.0` +* **SHA digest:** `ghcr.io/org/image@sha256:abc123...` + +#### Specify Scanners + +To select which scanners Trivy runs, use the `--scanners` option. By default, Prowler enables `vuln` and `secret` scanners: + +```bash +# Vulnerability scanning only +prowler image -I alpine:3.18 --scanners vuln + +# All available scanners +prowler image -I alpine:3.18 --scanners vuln secret misconfig license +``` + + +#### Image Config Scanners + +To scan Dockerfile-level metadata for misconfigurations or embedded secrets, use the `--image-config-scanners` option: + +```bash +# Scan Dockerfile for misconfigurations +prowler image -I alpine:3.18 --image-config-scanners misconfig + +# Scan Dockerfile for both misconfigurations and secrets +prowler image -I alpine:3.18 --image-config-scanners misconfig secret +``` + +Available image config scanners: + +* **misconfig**: Detects Dockerfile misconfigurations (e.g., running as root, missing health checks) +* **secret**: Identifies secrets embedded in Dockerfile instructions + + +Image config scanners are disabled by default. This option is independent from `--scanners` and specifically targets the image configuration (Dockerfile) rather than the image filesystem. + + +#### Filter by Severity + +To filter findings by severity level, use the `--trivy-severity` option: + +```bash +# Only critical and high severity findings +prowler image -I alpine:3.18 --trivy-severity CRITICAL HIGH +``` + +Available severity levels: `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`, `UNKNOWN`. + +#### Ignore Unfixed Vulnerabilities + +To exclude vulnerabilities without available fixes: + +```bash +prowler image -I alpine:3.18 --ignore-unfixed +``` + +#### Configure Scan Timeout + +To adjust the scan timeout for large images or slow network conditions, use the `--timeout` option: + +```bash +prowler image -I large-image:latest --timeout 10m +``` + +The timeout accepts values in seconds (`s`), minutes (`m`), or hours (`h`). Default: `5m`. + +### Authentication for Private Registries + +The Image provider relies on Trivy for registry authentication. To scan images from private registries, configure Docker credentials before running the scan: + +```bash +# Log in to a private registry +docker login myregistry.io + +# Then scan the image +prowler image -I myregistry.io/myapp:v1.0 +``` + +Trivy automatically uses credentials from Docker's credential store (`~/.docker/config.json`). + +### Troubleshooting Common Scan Errors + +The Image provider categorizes common Trivy errors with actionable guidance: + +* **Authentication failure (401/403):** Registry credentials are missing or invalid. Run `docker login` for the target registry and retry the scan. +* **Image not found (404):** The specified image name, tag, or registry is incorrect. Verify the image reference exists and is accessible. +* **Rate limited (429):** The container registry is throttling requests. Wait before retrying, or authenticate to increase rate limits. +* **Network issue:** Trivy cannot reach the registry due to connectivity problems. Check network access, DNS resolution, and firewall rules. diff --git a/docs/user-guide/tutorials/prowler-app-sso.mdx b/docs/user-guide/tutorials/prowler-app-sso.mdx index fa062100d1..f823fae3df 100644 --- a/docs/user-guide/tutorials/prowler-app-sso.mdx +++ b/docs/user-guide/tutorials/prowler-app-sso.mdx @@ -53,7 +53,7 @@ On the profile page, find the "SAML SSO Integration" card and click "Enable" to ![Enable SAML Integration](/images/prowler-app/saml/saml-step-2.png) -Next section will explain how to fill the IdP configuration based on your Identity Provider. +The next section explains how to configure the IdP settings based on the selected Identity Provider. #### Step 3: Configure the Identity Provider (IdP) Choose a Method: @@ -79,6 +79,31 @@ Choose a Method: + **Configure Attribute Mapping in the IdP** + + For Prowler App to correctly identify and provision users, configure the IdP to send the following attributes in the SAML assertion: + + | Attribute Name | Description | Required | + |----------------|---------------------------------------------------------------------------------------------------------|----------| + | `firstName` | The user's first name. | Yes | + | `lastName` | The user's last name. | Yes | + | `userType` | Determines which Prowler role the user receives (e.g., `admin`, `auditor`). If a role with that name already exists, the user receives it automatically; if it does not exist, Prowler App creates a new role with that name without permissions. If `userType` is not defined, the user is assigned the `no_permissions` role. Role permissions can be edited in the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac). | No | + | `companyName` | The user's company name. This is automatically populated if the IdP sends an `organization` attribute. | No | + + + **IdP Attribute Mapping** + + Note that the attribute name is just an example and may be different depending on the IdP. For instance, if the IdP provides a `division` attribute, it can be mapped to `userType`. + ![IdP configuration](/images/prowler-app/saml/saml_attribute_statements.png) + + + + **Dynamic Updates** + + Prowler App updates these attributes each time a user logs in. Any changes made in the Identity Provider (IdP) will be reflected when the user logs in again. + + + Instead of creating a custom SAML integration, Okta administrators can configure Prowler Cloud directly from Okta's application catalog. @@ -105,39 +130,48 @@ Choose a Method: 6. **Assign Users**: Navigate to the "Assignments" tab and assign the appropriate users or groups to the Prowler application by clicking "Assign" and selecting "Assign to People" or "Assign to Groups". + ![Okta App Assignments](/images/prowler-app/saml/okta-app-assignments.png) + + 7. **Configure User Attributes in Okta**: Okta acts as the central source for user profile information. Prowler App maps the following Okta user profile attributes during each SAML login: + + * **First name** (`firstName`): Maps to the user's first name in Prowler App. + * **Last name** (`lastName`): Maps to the user's last name in Prowler App. + + ![Okta User Profile — First Name and Last Name](/images/prowler-app/saml/okta-user-profile-name.png) + + * **Organization** (`organization`): Maps to the company name displayed in Prowler App. This attribute is optional. + * **User type** (`userType`): Determines the Prowler role assigned to the user. This attribute is **case-sensitive** and must match the exact name of an existing role in Prowler App. + + ![Okta User Profile — User Type and Organization](/images/prowler-app/saml/okta-user-profile-attributes.png) + + To modify these values, edit the user's profile directly in the Okta admin console under the "Profile" tab. Changes are reflected in Prowler App the next time the user logs in via SAML. + + + **User Type and Role Assignment** + + The `userType` attribute controls which Prowler role is assigned to the user: + + * If a role with the specified name already exists in Prowler App, the user automatically receives that role. + * If the role does not exist, Prowler App creates a new role with that exact name but without any permissions, preventing the user from performing any actions. + * If `userType` is not defined in the user's Okta profile, the user is assigned the `no_permissions` role. + + In all cases where the resulting role has no permissions, a Prowler administrator (a user whose role includes the "Manage Account" permission) must configure the appropriate permissions through the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac). + + This behavior is intentional: by defaulting to no permissions, Prowler App ensures that a misconfiguration in Okta cannot inadvertently grant elevated access. + + **Example:** To assign the `IT` role to a user, set the `userType` value to `IT` in Okta. If a role named `IT` already exists in Prowler App, the user receives it automatically upon login. If it does not exist, Prowler App creates a new role called `IT` without permissions, and a Prowler administrator must configure the desired permissions for it. + + + With this step, the Okta app catalog configuration is complete. Users can now access Prowler Cloud using either [IdP-initiated](#idp-initiated-sso) or [SP-initiated SSO](#sp-initiated-sso) flows. - 7. **Download Metadata XML**: Inside the "Sign On" section, go to the "Metadata URL" and download the metadata XML file. + 8. **Download Metadata XML**: Inside the "Sign On" section, go to the "Metadata URL" and download the metadata XML file. - Jump to [Step 5: Upload IdP Metadata to Prowler](#step-5:-upload-idp-metadata-to-prowler). + Jump to [Step 4: Upload IdP Metadata to Prowler](#step-4:-upload-idp-metadata-to-prowler). -#### Step 4: Configure Attribute Mapping in the IdP - -For Prowler App to correctly identify and provision users, configure the IdP to send the following attributes in the SAML assertion: - -| Attribute Name | Description | Required | -|----------------|---------------------------------------------------------------------------------------------------------|----------| -| `firstName` | The user's first name. | Yes | -| `lastName` | The user's last name. | Yes | -| `userType` | The Prowler role to be assigned to the user (e.g., `admin`, `auditor`). If a role with that name already exists, it will be used; otherwise, a new role called `no_permissions` will be created with minimal permissions. Role permissions can be edited in the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac). | No | -| `companyName` | The user's company name. This is automatically populated if the IdP sends an `organization` attribute. | No | - - -**IdP Attribute Mapping** - -Note that the attribute name is just an example and may be different depending on the IdP. For instance, if the IdP provides a `division` attribute, it can be mapped to `userType`. -![IdP configuration](/images/prowler-app/saml/saml_attribute_statements.png) - - - -**Dynamic Updates** - -Prowler App updates these attributes each time a user logs in. Any changes made in the Identity Provider (IdP) will be reflected when the user logs in again. - - -#### Step 5: Upload IdP Metadata to Prowler +#### Step 4: Upload IdP Metadata to Prowler Once the IdP is configured, it provides a **metadata XML file**. This file contains the IdP's configuration information, such as its public key and login URL. @@ -151,7 +185,7 @@ To complete the Prowler App configuration: ![Configure Prowler with IdP Metadata](/images/prowler-app/saml/saml-step-3.png) -#### Step 6: Save and Verify Configuration +#### Step 5: Save and Verify Configuration Click the "Save" button to complete the setup. The "SAML Integration" card will now display an "Active" status, indicating the configuration is complete and enabled. diff --git a/poetry.lock b/poetry.lock index f4d82258e7..2b403394ab 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4869,21 +4869,21 @@ files = [ [[package]] name = "pydantic" -version = "2.11.7" +version = "2.12.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, - {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.33.2" -typing-extensions = ">=4.12.2" -typing-inspection = ">=0.4.0" +pydantic-core = "2.41.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -4891,115 +4891,137 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.41.5" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, - {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, ] [package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +typing-extensions = ">=4.14.1" [[package]] name = "pyflakes" @@ -6303,14 +6325,14 @@ files = [ [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.2" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, - {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, ] [package.dependencies] diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 328cf92304..4bfea57e81 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -6,7 +6,69 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🚀 Added +- `organization_verified_badge` check for GitHub provider [(#10033)](https://github.com/prowler-cloud/prowler/pull/10033) +- OpenStack provider `clouds_yaml_content` parameter for API integration [(#10003)](https://github.com/prowler-cloud/prowler/pull/10003) +- `defender_safe_attachments_policy_enabled` check for M365 provider [(#9833)](https://github.com/prowler-cloud/prowler/pull/9833) +- `defender_safelinks_policy_enabled` check for M365 provider [(#9832)](https://github.com/prowler-cloud/prowler/pull/9832) - AI Skills: Added a skill for creating new Attack Paths queries in openCypher, compatible with Neo4j and Neptune [(#9975)](https://github.com/prowler-cloud/prowler/pull/9975) +- CSA CCM 4.0 for the AWS provider [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018) +- CSA CCM 4.0 for the GCP provider [(#10042)](https://github.com/prowler-cloud/prowler/pull/10042) +- CSA CCM 4.0 for the Azure provider [(#10039)](https://github.com/prowler-cloud/prowler/pull/10039) +- CSA CCM 4.0 for the Oracle Cloud provider [(#10057)](https://github.com/prowler-cloud/prowler/pull/10057) +- OCI regions updater script and CI workflow [(#10020)](https://github.com/prowler-cloud/prowler/pull/10020) +- `image` provider for container image scanning with Trivy integration [(#9984)](https://github.com/prowler-cloud/prowler/pull/9984) +- OpenStack compute 7 new checks [(#9944)](https://github.com/prowler-cloud/prowler/pull/9944) +- CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061) +- ECS Exec (ECS-006) privilege escalation detection via `ecs:ExecuteCommand` + `ecs:DescribeTasks` [(#10066)](https://github.com/prowler-cloud/prowler/pull/10066) +- Add file descriptor limits (`ulimits`) to Docker Compose worker services to prevent `Too many open files` errors [(#10107)](https://github.com/prowler-cloud/prowler/pull/10107) + +### 🔄 Changed + +- Update Azure Monitor service metadata to new format [(#9622)](https://github.com/prowler-cloud/prowler/pull/9622) +- GitHub provider enhanced documentation and `repository_branch_delete_on_merge_enabled` logic [(#9830)](https://github.com/prowler-cloud/prowler/pull/9830) +- Parallelize Cloudflare zone API calls with threading to improve scan performance [(#9982)](https://github.com/prowler-cloud/prowler/pull/9982) +- Update GCP API Keys service metadata to new format [(#9637)](https://github.com/prowler-cloud/prowler/pull/9637) +- Update GCP BigQuery service metadata to new format [(#9638)](https://github.com/prowler-cloud/prowler/pull/9638) +- Update GCP Cloud SQL service metadata to new format [(#9639)](https://github.com/prowler-cloud/prowler/pull/9639) +- Update GCP Cloud Storage service metadata to new format [(#9640)](https://github.com/prowler-cloud/prowler/pull/9640) +- Update GCP Compute Engine service metadata to new format [(#9641)](https://github.com/prowler-cloud/prowler/pull/9641) +- Update GCP Dataproc service metadata to new format [(#9642)](https://github.com/prowler-cloud/prowler/pull/9642) +- Update GCP DNS service metadata to new format [(#9643)](https://github.com/prowler-cloud/prowler/pull/9643) +- Update GCP GCR service metadata to new format [(#9644)](https://github.com/prowler-cloud/prowler/pull/9644) +- Update GCP GKE service metadata to new format [(#9645)](https://github.com/prowler-cloud/prowler/pull/9645) +- Update GCP IAM service metadata to new format [(#9646)](https://github.com/prowler-cloud/prowler/pull/9646) +- Update GCP KMS service metadata to new format [(#9647)](https://github.com/prowler-cloud/prowler/pull/9647) +- Update GCP Logging service metadata to new format [(#9648)](https://github.com/prowler-cloud/prowler/pull/9648) +- Update Azure Key Vault service metadata to new format [(#9621)](https://github.com/prowler-cloud/prowler/pull/9621) +- Update Azure Entra ID service metadata to new format [(#9619)](https://github.com/prowler-cloud/prowler/pull/9619) +- Update Azure Virtual Machines service metadata to new format [(#9629)](https://github.com/prowler-cloud/prowler/pull/9629) + +### 🔐 Security + +- Bumped `py-ocsf-models` to 0.8.1 and `cryptography` to 44.0.3 [(#10059)](https://github.com/prowler-cloud/prowler/pull/10059) + +--- + +## [5.18.3] (Prowler UNRELEASED) + +### 🐞 Fixed + +- `pip install prowler` failing on systems without C compiler due to `netifaces` transitive dependency from `openstacksdk` [(#10055)](https://github.com/prowler-cloud/prowler/pull/10055) +- `kms_key_not_publicly_accessible` false negative for specific KMS actions (e.g., `kms:DescribeKey`, `kms:Decrypt`) with unrestricted principals [(#10071)](https://github.com/prowler-cloud/prowler/pull/10071) +- Remove account_id and location for manual requirements in M365CIS [(#10105)](https://github.com/prowler-cloud/prowler/pull/10105) + +--- + +## [5.18.2] (Prowler v5.18.2) + +### 🐞 Fixed + +- `--repository` and `--organization` flags combined interaction in GitHub provider, qualifying unqualified repository names with organization [(#10001)](https://github.com/prowler-cloud/prowler/pull/10001) +- HPACK library logging tokens in debug mode for Azure, M365, and Cloudflare providers [(#10010)](https://github.com/prowler-cloud/prowler/pull/10010) + +### 🐞 Fixed + +- Use `defusedxml` in the Alibaba Cloud OSS service to prevent XXE vulnerabilities when parsing XML responses [(#9999)](https://github.com/prowler-cloud/prowler/pull/9999) --- @@ -17,6 +79,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `defender_zap_for_teams_enabled` check for M365 provider [(#9838)](https://github.com/prowler-cloud/prowler/pull/9838) - `compute_instance_suspended_without_persistent_disks` check for GCP provider [(#9747)](https://github.com/prowler-cloud/prowler/pull/9747) - `codebuild_project_webhook_filters_use_anchored_patterns` check for AWS provider to detect CodeBreach vulnerability [(#9840)](https://github.com/prowler-cloud/prowler/pull/9840) +- `defender_atp_safe_attachments_policy_enabled` check for M365 provider [(#9837)](https://github.com/prowler-cloud/prowler/pull/9837) - `exchange_shared_mailbox_sign_in_disabled` check for M365 provider [(#9828)](https://github.com/prowler-cloud/prowler/pull/9828) - CloudTrail Timeline abstraction for querying resource modification history [(#9101)](https://github.com/prowler-cloud/prowler/pull/9101) - Cloudflare `--account-id` filter argument [(#9894)](https://github.com/prowler-cloud/prowler/pull/9894) @@ -25,6 +88,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `OpenStack` documentation for the support in the CLI [(#9848)](https://github.com/prowler-cloud/prowler/pull/9848) - Add HIPAA compliance framework for the Azure provider [(#9957)](https://github.com/prowler-cloud/prowler/pull/9957) - Cloudflare provider credentials as constructor parameters (`api_token`, `api_key`, `api_email`) [(#9907)](https://github.com/prowler-cloud/prowler/pull/9907) +- CIS 3.1 for the Oracle Cloud provider [(#9971)](https://github.com/prowler-cloud/prowler/pull/9971) ### 🔄 Changed @@ -44,7 +108,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Update Azure Network service metadata to new format [(#9624)](https://github.com/prowler-cloud/prowler/pull/9624) - Update Azure Storage service metadata to new format [(#9628)](https://github.com/prowler-cloud/prowler/pull/9628) -### 🐛 Fixed +### 🐞 Fixed - Duplicated findings in `entra_user_with_vm_access_has_mfa` check when user has multiple VM access roles [(#9914)](https://github.com/prowler-cloud/prowler/pull/9914) - Jira integration failing with `INVALID_INPUT` error when sending findings with long resource UIDs exceeding 255-character summary limit [(#9926)](https://github.com/prowler-cloud/prowler/pull/9926) @@ -112,6 +176,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Update Azure AI Search service metadata to new format [(#9087)](https://github.com/prowler-cloud/prowler/pull/9087) - Update Azure AKS service metadata to new format [(#9611)](https://github.com/prowler-cloud/prowler/pull/9611) - Update Azure API Management service metadata to new format [(#9612)](https://github.com/prowler-cloud/prowler/pull/9612) +- Enhance AWS IAM privilege escalation detection with patterns from pathfinding.cloud library [(#9922)](https://github.com/prowler-cloud/prowler/pull/9922) ### Fixed diff --git a/prowler/__main__.py b/prowler/__main__.py index fd7869193b..9fa9e91ad9 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -8,6 +8,7 @@ from colorama import Fore, Style from colorama import init as colorama_init from prowler.config.config import ( + EXTERNAL_TOOL_PROVIDERS, csv_file_suffix, get_available_compliance_frameworks, html_file_suffix, @@ -65,6 +66,11 @@ 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.compliance import display_compliance_table +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 +from prowler.lib.outputs.compliance.csa.csa_gcp import GCPCSA +from prowler.lib.outputs.compliance.csa.csa_oraclecloud import OracleCloudCSA from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS @@ -119,6 +125,8 @@ from prowler.providers.common.quick_inventory import run_provider_quick_inventor from prowler.providers.gcp.models import GCPOutputOptions from prowler.providers.github.models import GithubOutputOptions from prowler.providers.iac.models import IACOutputOptions +from prowler.providers.image.exceptions.exceptions import ImageBaseException +from prowler.providers.image.models import ImageOutputOptions from prowler.providers.kubernetes.models import KubernetesOutputOptions from prowler.providers.llm.models import LLMOutputOptions from prowler.providers.m365.models import M365OutputOptions @@ -206,8 +214,8 @@ def prowler(): # Load compliance frameworks logger.debug("Loading compliance frameworks from .json files") - # Skip compliance frameworks for IAC and LLM providers - if provider != "iac" and provider != "llm": + # Skip compliance frameworks for external-tool providers + if provider not in EXTERNAL_TOOL_PROVIDERS: bulk_compliance_frameworks = Compliance.get_bulk(provider) # Complete checks metadata with the compliance framework specification bulk_checks_metadata = update_checks_metadata_with_compliance( @@ -264,8 +272,8 @@ def prowler(): if not args.only_logs: global_provider.print_credentials() - # Skip service and check loading for IAC and LLM providers - if provider != "iac" and provider != "llm": + # Skip service and check loading for external-tool providers + if provider not in EXTERNAL_TOOL_PROVIDERS: # Import custom checks from folder if checks_folder: custom_checks = parse_checks_from_folder(global_provider, checks_folder) @@ -352,6 +360,8 @@ def prowler(): ) elif provider == "iac": output_options = IACOutputOptions(args, bulk_checks_metadata) + elif provider == "image": + output_options = ImageOutputOptions(args, bulk_checks_metadata) elif provider == "llm": output_options = LLMOutputOptions(args, bulk_checks_metadata) elif provider == "oraclecloud": @@ -375,8 +385,8 @@ def prowler(): # Execute checks findings = [] - if provider == "iac" or provider == "llm": - # For IAC and LLM providers, run the scan directly + if provider in EXTERNAL_TOOL_PROVIDERS: + # For external-tool providers, run the scan directly if provider == "llm": def streaming_callback(findings_batch): @@ -386,7 +396,11 @@ def prowler(): findings = global_provider.run_scan(streaming_callback=streaming_callback) else: # Original behavior for IAC or non-verbose LLM - findings = global_provider.run() + try: + findings = global_provider.run() + except ImageBaseException as error: + logger.critical(f"{error}") + sys.exit(1) # Note: IaC doesn't support granular progress tracking since Trivy runs as a black box # and returns all findings at once. Progress tracking would just be 0% → 100%. @@ -626,6 +640,18 @@ def prowler(): ) generated_outputs["compliance"].append(c5) c5.batch_write_data_to_file() + elif compliance_name == "csa_ccm_4.0_aws": + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + csa_ccm_4_0_aws = AWSCSA( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + file_path=filename, + ) + generated_outputs["compliance"].append(csa_ccm_4_0_aws) + csa_ccm_4_0_aws.batch_write_data_to_file() else: filename = ( f"{output_options.output_directory}/compliance/" @@ -729,6 +755,18 @@ def prowler(): ) generated_outputs["compliance"].append(c5_azure) c5_azure.batch_write_data_to_file() + elif compliance_name == "csa_ccm_4.0_azure": + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + csa_ccm_4_0_azure = AzureCSA( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + file_path=filename, + ) + generated_outputs["compliance"].append(csa_ccm_4_0_azure) + csa_ccm_4_0_azure.batch_write_data_to_file() else: filename = ( f"{output_options.output_directory}/compliance/" @@ -832,6 +870,18 @@ def prowler(): ) generated_outputs["compliance"].append(c5_gcp) c5_gcp.batch_write_data_to_file() + elif compliance_name == "csa_ccm_4.0_gcp": + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + csa_ccm_4_0_gcp = GCPCSA( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + file_path=filename, + ) + generated_outputs["compliance"].append(csa_ccm_4_0_gcp) + csa_ccm_4_0_gcp.batch_write_data_to_file() else: filename = ( f"{output_options.output_directory}/compliance/" @@ -1024,6 +1074,18 @@ def prowler(): ) generated_outputs["compliance"].append(cis) cis.batch_write_data_to_file() + elif compliance_name == "csa_ccm_4.0_oraclecloud": + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + csa_ccm_4_0_oraclecloud = OracleCloudCSA( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + file_path=filename, + ) + generated_outputs["compliance"].append(csa_ccm_4_0_oraclecloud) + csa_ccm_4_0_oraclecloud.batch_write_data_to_file() else: filename = ( f"{output_options.output_directory}/compliance/" @@ -1052,6 +1114,18 @@ def prowler(): ) generated_outputs["compliance"].append(cis) cis.batch_write_data_to_file() + elif compliance_name == "csa_ccm_4.0_alibabacloud": + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + csa_ccm_4_0_alibabacloud = AlibabaCloudCSA( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + file_path=filename, + ) + generated_outputs["compliance"].append(csa_ccm_4_0_alibabacloud) + csa_ccm_4_0_alibabacloud.batch_write_data_to_file() elif compliance_name == "prowler_threatscore_alibabacloud": filename = ( f"{output_options.output_directory}/compliance/" diff --git a/prowler/compliance/alibabacloud/csa_ccm_4.0_alibabacloud.json b/prowler/compliance/alibabacloud/csa_ccm_4.0_alibabacloud.json new file mode 100644 index 0000000000..060b6e819e --- /dev/null +++ b/prowler/compliance/alibabacloud/csa_ccm_4.0_alibabacloud.json @@ -0,0 +1,7305 @@ +{ + "Framework": "CSA-CCM", + "Name": "CSA Cloud Controls Matrix (CCM) v4.0.13", + "Version": "4.0", + "Provider": "alibabacloud", + "Description": "The Cloud Security Alliance (CSA) Cloud Controls Matrix (CCM) is a cybersecurity control framework for cloud computing, composed of 197 control objectives structured in 17 domains covering all key aspects of cloud technology. The CCM can be used as a tool for the systematic assessment of a cloud implementation, and provides guidance on which security controls should be implemented by which actor within the cloud supply chain.", + "Requirements": [ + { + "Id": "A&A-02", + "Description": "Conduct independent audit and assurance assessments according to relevant standards at least annually.", + "Name": "Independent Assessments", + "Attributes": [ + { + "Section": "Audit & Assurance", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC4.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AAC-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.5.2", + "5.2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "AS1.1", + "AS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.2.1", + "27002: 18.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.35", + "27001: A.5.36" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-2", + "CA-2(1)", + "CA-2(2)", + "CA-7", + "CA-7(1)" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-01" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_advanced_or_enterprise_edition" + ] + }, + { + "Id": "A&A-04", + "Description": "Verify compliance with all relevant standards, regulations, legal/contractual, and statutory requirements applicable to the audit.", + "Name": "Requirements Compliance", + "Attributes": [ + { + "Section": "Audit & Assurance", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC3.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-01", + "GRM-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "7.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "AS1.1", + "AS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.3.2", + "27001: A.18.2.2", + "27002: 18.2.2", + "27001: A.18.2.3", + "27002: 18.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.3.2", + "27001: A.5.31", + "27001: A.5.32", + "27001: A.5.33", + "27001: A.5.34", + "27001: A.5.36" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-1" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-3", + "DE.DP-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-01" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_advanced_or_enterprise_edition" + ] + }, + { + "Id": "AIS-04", + "Description": "Define and implement a SDLC process for application design, development, deployment, and operation in accordance with security requirements defined by the organization.", + "Name": "Secure Application Design and Development", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AIS-01", + "AIS-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.4", + "5.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.1.1", + "27002: 14.1.1", + "27017: 14.1.1", + "27001: A.14.1.2", + "27002: 14.1.2", + "27017: 14.1.2", + "27001: A.14.2.1", + "27002: 14.2.1", + "27017: 14.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.8", + "27001: A.8.25", + "27001: A.8.26", + "27001: A.8.28" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-2", + "PL-8", + "PL-8(1)", + "SA-3", + "SA-3(1)", + "SA-4", + "SA-4(2)", + "SA-4(3)", + "SA-4(8)", + "SA-4(9)", + "SA-5", + "SA-8", + "SA-8(1)-(7)", + "SA-8(9)-(13)", + "SA-8(15)-(20)", + "SA-8(22)", + "SA-8(24)-(28)", + "SA-8(30)-(33)", + "SA-17", + "SA-17(1)-(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-6", + "PR.DS-7", + "PR.IP-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "PR.IR-01", + "PR.PS-01", + "PR.PS-02", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.1", + "6.2.3", + "6.5.2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "AIS-05", + "Description": "Implement a testing strategy, including criteria for acceptance of new information systems, upgrades and new versions, which provides application security assurance and maintains compliance while enabling organizational speed of delivery goals. Automate when applicable and possible.", + "Name": "Automated Application Security Testing", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AIS-01", + "AIS-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.12", + "16.13" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD2.3", + "SD2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.2.8", + "27001: A.14.2.9", + "27001: A.12.1.2", + "27002: 12.1.2", + "27001: A.14.1.1", + "27002: 14.1.1", + "27001: A.14.2.2", + "27002: 14.2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.25", + "27001: A.8.29", + "27001: A.8.32", + "27002: 8.25 (e)", + "27002: 8.32 (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SA-11", + "SA-11(1)-(9)", + "SI-6", + "SI-6(2)", + "SI-6(3)", + "SI-10", + "SI-10(1)-(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.PT-3", + "PR.IP-12", + "DE.CM-8" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "ID.RA-01", + "PR.PS-01", + "PR.PS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A.3.2.2", + "A.3.2.2.1", + "6.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.4", + "6.4.1", + "6.4.2", + "6.5.1" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_vulnerability_scan_enabled" + ] + }, + { + "Id": "AIS-07", + "Description": "Define and implement a process to remediate application security vulnerabilities, automating remediation when possible.", + "Name": "Application Vulnerability Remediation", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.1", + "CC7.4", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.2", + "16.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27001: A.12.6.1", + "27002: 12.6.1", + "27017: 12.6.1", + "27018: 12.6.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.26", + "27001: A.8.8", + "27002: 5.26 (j)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SI-2", + "SI-2(2)-(6)", + "SA-11", + "SA-11(2)", + "SA-15", + "SA-15(1)-(3)", + "SA-15(5)-(8)", + "SA-15(10)-(12)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.IP-12", + "DE.CM-8", + "RS.AN-5", + "RS.MI-3", + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "ID.RA-01", + "ID.RA-06", + "ID.RA-08", + "PR.PS-02", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.2", + "6.5", + "6.5.1-10" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "11.3.1", + "11.3.1.1" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_vulnerability_scan_enabled" + ] + }, + { + "Id": "BCR-08", + "Description": "Periodically backup data stored in the cloud. Ensure the confidentiality, integrity and availability of the backup, and verify data restoration from backup for resiliency.", + "Name": "Backup", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "A1.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "11.1", + "11.2", + "11.3", + "11.4", + "11.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8", + "5.2.9" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.3", + "27017: 12.3", + "27018: 12.3.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.13", + "27001: A.5.23", + "27001: A.5.30", + "27002: 8.13", + "27002: 5.23 2nd (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-4", + "CP-4(4)", + "CP-6", + "CP-6(1)-(3)", + "CP-9", + "CP-9(1)", + "CP-9(2)", + "CP-10", + "CP-10(2)", + "CP-10(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-4", + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-11", + "RC.RP-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "9.5.1", + "12.10.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1", + "10.3.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "BCR-09", + "Description": "Establish, document, approve, communicate, apply, evaluate and maintain a disaster response plan to recover from natural and man-made disasters. Update the plan at least annually or upon significant changes.", + "Name": "Disaster Response Plan", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "CC3.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8", + "5.2.9", + "1.6.1", + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "BC1.4", + "BC2.1", + "BC2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.29", + "27001: A.5.30", + "27002: 5.29", + "27002: 5.30" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2(1)", + "CP-2(2)", + "CP-2(3)", + "CP-2(5)", + "CP-2(6)", + "CP-2(7)", + "CP-2(8)", + "PE-13", + "PE-13(1)", + "PE-13(2)", + "PE-13(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-9", + "PR.IP-10", + "RC.IM-1", + "RC.IM-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-04" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "BCR-11", + "Description": "Supplement business-critical equipment with redundant equipment independently located at a reasonable minimum distance in accordance with applicable industry standards.", + "Name": "Equipment Redundancy", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "No", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "CC3.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-06" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "BC1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.20", + "27001: A.7.11", + "27001: A.8.14", + "27002: 5.20 (t)", + "27002: 8.14 (c)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2", + "CP-2(2)", + "CP-4(3)", + "CP-6", + "CP-6(1)", + "CP-7", + "CP-8", + "CP-8(1)-(3)", + "CP-9", + "CP-9(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.BE-4", + "ID.BE-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.OC-04", + "GV.OC-05", + "PR.IR-03" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC-04", + "Description": "Restrict the unauthorized addition, removal, update, and management of organization assets.", + "Name": "Unauthorized Change Protection", + "Attributes": [ + { + "Section": "Change Control and Configuration Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "CCC-04" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.1", + "1.3.4", + "5.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.4", + "SM2.6" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.1.4", + "27002: 12.1.4", + "27001: A.12.4.2", + "27002: 12.4.2", + "27001: A.14.2.2", + "27017: 14.2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.3", + "27001: A.8.4", + "27001: A.8.15", + "27001: A.8.31", + "27001: A.8.32" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-7", + "CA-7(4)", + "CM-3", + "CM-3(1)", + "CM-3(5)", + "CM-3(7)", + "CM-3(8)", + "CM-5", + "CM-5(1)", + "CM-5(4)", + "CM-5(5)", + "CM-6", + "CM-6(1)", + "CM-6(2)", + "CM-7", + "CM-7(1)", + "CM-7(4)", + "CM-7(5)", + "CM-7(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-1", + "ID.AM-2", + "ID.AM-4", + "PR.MA-1", + "PR.MA-2", + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-01", + "ID.AM-02", + "ID.AM-04", + "ID.AM-08", + "PR.PS-02", + "PR.PS-03", + "PR.PS-05", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.5.1", + "6.5.2" + ] + } + ] + } + ], + "Checks": [ + "actiontrail_multi_region_enabled" + ] + }, + { + "Id": "CCC-07", + "Description": "Implement detection measures with proactive notification in case of changes deviating from the established baseline.", + "Name": "Detection of Baseline Deviation", + "Attributes": [ + { + "Section": "Change Control and Configuration Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-01" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.5.1", + "1.5.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.2.2", + "27001: A.14.2.4", + "27001: A.12.4.1", + "27002: 12.4.1 (g)", + "27001: A.5.1.1", + "27017: 5.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.9", + "27001: A.8.15", + "27002: 8.9" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-6", + "CM-6(2)", + "SI-2", + "SI-2(2)-(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.MA-1", + "PR.IP-1", + "DE.DP-4", + "PR.IP-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-01", + "DE.CM-09", + "DE.AE-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4.5.3", + "6.4.5.4", + "11.5", + "11.5.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "11.5.2", + "11.6.1" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_advanced_or_enterprise_edition", + "sls_security_group_changes_alert_enabled", + "sls_vpc_changes_alert_enabled", + "sls_vpc_network_route_changes_alert_enabled", + "sls_customer_created_cmk_changes_alert_enabled", + "sls_cloud_firewall_changes_alert_enabled", + "sls_management_console_authentication_failures_alert_enabled", + "sls_rds_instance_configuration_changes_alert_enabled" + ] + }, + { + "Id": "CEK-03", + "Description": "Provide cryptographic protection to data at-rest and in-transit, using cryptographic libraries certified to approved standards.", + "Name": "Data Encryption", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-03", + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.6", + "3.1", + "3.11", + "11.3", + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.1", + "27001: A.18.1.2", + "27001: A.18.1.3", + "27001: A.18.1.4", + "27001: A.18.1.5", + "27001: A.10.1", + "27002: 10.1", + "27001: A.13.2.1", + "27002: 13.2.1", + "27001: A.18", + "27002: 18", + "27001: A.14.1.2", + "27002: 14.1.2", + "27001: A.14.1.3", + "27002 14.1.3 c)", + "27001 - A.10.1.1", + "27017 - 10.1.1", + "27001 - A.10.1.2", + "27017 - 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.8.24", + "27002: 8.24 Other Information (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-19", + "AC-19(5)", + "SC-8", + "SC-8(1)", + "SC-8(3)", + "SC-8(4)", + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-28", + "SC-28(1)-(3)", + "SI-4", + "SI-4(10)", + "SI-7", + "SI-7(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "Requirement 3", + "2.2.3", + "2.3", + "3.4", + "3.5.3", + "4.1", + "8.2.1", + "PCI Glossary - Strong Cryptography" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.7", + "3.5.1", + "4.2.1", + "4.2.1.2", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "ecs_attached_disk_encrypted", + "ecs_unattached_disk_encrypted", + "rds_instance_tde_enabled", + "rds_instance_ssl_enabled", + "oss_bucket_secure_transport_enabled" + ] + }, + { + "Id": "CEK-04", + "Description": "Use encryption algorithms that are appropriate for data protection, considering the classification of data, associated risks, and usability of the encryption technology.", + "Name": "Encryption Algorithm", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.2", + "27001: 6.1.3", + "27001: A.8.2", + "27002: 8.2", + "27001: A.8.3", + "27001: A.10.1.1", + "27002: 10.1.1 (b)", + "27001: A.10.1.2", + "27002: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.2", + "27001: 6.1.3", + "27001: A.8.24", + "27001: A.5.12", + "27001: A.5.13", + "27002: 8.24 General (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2", + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02", + "ID.AM-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A2", + "Requirement 3", + "2.3", + "2.2.3", + "3.4", + "3.5.3", + "4.1", + "8.2.1", + "PCI Glossary - Strong Cryptography" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.7", + "3.5.1", + "4.2.1", + "4.2.1.2", + "4.2.2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CEK-08", + "Description": "CSPs must provide the capability for CSCs to manage their own data encryption keys.", + "Name": "CSC Key Management Capability", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2", + "SC2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1", + "27017: 10.1", + "27001: A.10.1.1", + "27017: 10.1.1", + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.23", + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-9", + "CP-9(8)", + "SA-9", + "SA-9(6)", + "SC-12", + "SC-12(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.SC-3", + "ID.AM-6", + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.SC-05" + ] + } + ] + } + ], + "Checks": [ + "rds_instance_tde_key_custom" + ] + }, + { + "Id": "CEK-10", + "Description": "Generate Cryptographic keys using industry accepted cryptographic libraries specifying the algorithm strength and the random number generator used.", + "Name": "Key Generation", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2", + "TS2.3", + "SY1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27002: 10.1.1 (e)", + "27017: 10.1.1", + "27001: A.10.1.2", + "27002: 10.1.2", + "27002: 10.1.2 (a)", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24", + "27002: 8.24 (d), Key management (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.2.3", + "3.6.1", + "PCI Glossary - Cryptographic Key Generation" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1", + "3.6.1.1", + "3.7.1" + ] + } + ] + } + ], + "Checks": [ + "rds_instance_tde_key_custom" + ] + }, + { + "Id": "CEK-12", + "Description": "Rotate cryptographic keys in accordance with the calculated cryptoperiod, which includes provisions for considering the risk of information disclosure and legal and regulatory requirements.", + "Name": "Key Rotation", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27017: 10.1.1", + "27001: A.10.1.2", + "27002: 10.1.2 e)", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.31", + "27001: A.8.24", + "27002: 5.31 Cryptography", + "27002: 8.24 Key management (e,m)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "ID.GV-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05", + "GV.OC-03" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.7.4", + "3.7.5" + ] + } + ] + } + ], + "Checks": [ + "ram_rotate_access_key_90_days" + ] + }, + { + "Id": "CEK-14", + "Description": "Define, implement and evaluate processes, procedures and technical measures to destroy keys stored outside a secure environment and revoke keys stored in Hardware Security Modules (HSMs) when they are no longer needed, which include provisions for legal and regulatory requirements.", + "Name": "Key Destruction", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27017: 10.1.1", + "27017: 10.1.2", + "27001: A.10.1.2", + "27002: 10.1.2 (j)", + "27001: A.18.1.3", + "27002: 18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.31", + "27001: A.8.24", + "27002: 5.31 Cryptography", + "27002: 8.24 Key management (j,m)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.IP-6", + "ID.GV-3", + "PR.DS-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05", + "ID.AM-08", + "GV.OC-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.6.4", + "3.6.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.7.4", + "3.7.5" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "DCS-06", + "Description": "Catalogue and track all relevant physical and logical assets located at all of the CSP's sites within a secured system.", + "Name": "Assets Cataloguing and Tracking", + "Attributes": [ + { + "Section": "Datacenter Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DCS - 01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "1.1", + "2.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SM2.6" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.1.1", + "27002: 8.1.1", + "27017: 8.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.9" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-8", + "CM-8(1)", + "CM-8(2)", + "CM-8(4)", + "CM-8(7)", + "CM-8(8)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-1", + "ID.AM-2", + "ID.AM-4", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-01", + "ID.AM-02", + "ID.AM-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.4", + "9.7.1", + "9.9.1", + "9.9.1.a", + "9.9.1.b", + "9.9.1.c", + "12.3.3", + "12.3.4" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1.1", + "6.3.2", + "9.4.2", + "9.4.3", + "12.5.1" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_all_assets_agent_installed" + ] + }, + { + "Id": "DSP-02", + "Description": "Apply industry accepted methods for the secure disposal of data from storage media such that data is not recoverable by any forensic means.", + "Name": "Secure Disposal", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3", + "CC6.4", + "CC6.5", + "CC6.7", + "P4.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DSI-07" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.3.3", + "7.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.3.2", + "27002: 8.3.2", + "27001: A.11.2.7", + "27002: 11.2.7" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.7.10", + "27001: A.7.14", + "27001: A.8.10", + "27002: 7.10 (Secure reuse or disposal)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-22", + "SI-12", + "SI-12(3)", + "SI-18", + "SI-18(1)", + "SI-18(4)", + "SI-18(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.SC-10", + "PR.PS-02", + "PR.PS-03", + "ID.AM-08" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.1", + "9.8", + "9.8.1", + "9.8.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1", + "3.7.5", + "9.4.7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "DSP-03", + "Description": "Create and maintain a data inventory, at least for any sensitive data and personal data.", + "Name": "Data Inventory", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1", + "1.3.2", + "1.3.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.1.1", + "27002: 8.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.9", + "27001: A.8.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-12", + "CM-12(1)", + "PM-5", + "PM-5(1)", + "SI-12", + "SI-12(1)", + "SI-19", + "SI-19(1)", + "SI-19(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1", + "9.4.5" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_all_assets_agent_installed" + ] + }, + { + "Id": "DSP-04", + "Description": "Classify data according to its type and sensitivity level.", + "Name": "Data Classification", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "C1.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DSI-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.7" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1", + "1.3.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.2.1", + "27002: 8.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-16", + "AC-16(9)", + "PM-22", + "PM-23", + "PT-2", + "PT-2(1)", + "SI-18", + "SI-18(2)", + "SI-19", + "SI-19(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-05", + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "9.6.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "9.4.2", + "9.4.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "DSP-07", + "Description": "Develop systems, products, and business practices based upon a principle of security by design and industry best practices.", + "Name": "Data Protection by Design and Default", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "PI1.2", + "PI1.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.3.1", + "5.3.2", + "5.3.3", + "5.3.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD2.2", + "IM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.1.1", + "27002:14.1.1", + "27001: A.14.2.5", + "27002:14.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.27", + "27001: A.8.28", + "27001: A.8.29", + "27002: 5.8 (Information security requirements a-i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-17", + "PM-24", + "PM-25", + "PT-2", + "PT-2(2)", + "SA-3", + "SA-4", + "SA-5", + "SA-8", + "SA-8(9)", + "SA-8(13)", + "SA-8(18)", + "SA-8(20)", + "SA-8(22)", + "SA-8(23)", + "SA-8(33)", + "SA-15", + "SA-15(12)", + "SC-3", + "SC-3(3)", + "SC-7", + "SC-7(24)", + "SC-8", + "SC-8(1)-(4)", + "SC-28", + "SC-28(1)", + "SI-12", + "SI-12(1)-(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.PT-3", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.1" + ] + } + ] + } + ], + "Checks": [ + "oss_bucket_not_publicly_accessible", + "rds_instance_no_public_access_whitelist" + ] + }, + { + "Id": "DSP-10", + "Description": "Define, implement and evaluate processes, procedures and technical measures that ensure any transfer of personal or sensitive data is protected from unauthorized access and only processed within scope as permitted by the respective laws and regulations.", + "Name": "Sensitive Data Transfer", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-02", + "EKM-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.1", + "3.12", + "3.13" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "9.5.1", + "9.5.2", + "9.5.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.4", + "IM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.13.2.1", + "27002: 13.2.1", + "27001: A.8.3.3", + "27002: 8.3.3", + "27001: A.13.2.3", + "27002: 13.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.7.10" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-4", + "AC-4(23)-(25)", + "CA-3", + "CA-3(6)", + "CA-6", + "CA-6(1)", + "CA-6(2)", + "SC-4", + "SC-4(2)", + "SC-7", + "SC-7(10)", + "SC-7(24)", + "SC-8", + "SC-8(1)-(5)", + "SC-16", + "SC-16(1)-(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2", + "PR.DS-5", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02", + "PR.IR-01", + "ID.AM-03", + "GV.OC-03", + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "4.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "4.1.1", + "4.2.1", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "oss_bucket_secure_transport_enabled", + "rds_instance_ssl_enabled" + ] + }, + { + "Id": "DSP-16", + "Description": "Data retention, archiving and deletion is managed in accordance with business requirements, applicable laws and regulations.", + "Name": "Data Retention and Deletion", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "C1.1", + "C1.2", + "CC3.1", + "P4.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-02", + "BCR-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.4", + "3.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.3.1", + "7.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.33", + "27001: A.8.10", + "27002: 5.33 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SI-12", + "SI-12(1)-(3)", + "SI-18", + "SI-18(1)", + "SI-18(4)", + "SI-18(5)", + "SI-19", + "SI-19(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-3", + "PR.IP-6", + "ID.GV-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "GV.OC-03", + "GV.SC-10", + "PR.DS-11" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1" + ] + } + ] + } + ], + "Checks": [ + "sls_logstore_retention_period", + "rds_instance_sql_audit_retention" + ] + }, + { + "Id": "DSP-17", + "Description": "Define and implement, processes, procedures and technical measures to protect sensitive data throughout it's lifecycle.", + "Name": "Sensitive Data Protection", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSC-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.1", + "CC6.1", + "CC6.3", + "CC6.7", + "CC8.1", + "C1.1", + "P2.0", + "P3.0", + "P4.0", + "P5.0", + "P6.0" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.1", + "3.1", + "3.14" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.3.3", + "9.1.1", + "9.2.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3", + "27002: 18.1.3", + "27001:A.18.1.4", + "27002:18.1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.11", + "27001: A.8.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-2", + "PM-22", + "PM-24", + "PT-7", + "PT-7(1)", + "PT-7(2)", + "PT-8", + "SC-8", + "SC-8(1)-(5)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2", + "PR.DS-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02", + "PR.DS-10" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.0 (including all subsections)", + "4.0 (including all subsections)" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.1.1", + "4.1.1" + ] + } + ] + } + ], + "Checks": [ + "oss_bucket_not_publicly_accessible", + "rds_instance_no_public_access_whitelist", + "ecs_attached_disk_encrypted", + "ecs_unattached_disk_encrypted", + "rds_instance_tde_enabled" + ] + }, + { + "Id": "GRC-05", + "Description": "Develop and implement an Information Security Program, which includes programs for all the relevant domains of the CCM.", + "Name": "Information Security Program", + "Attributes": [ + { + "Section": "Governance, Risk and Compliance", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "14.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SG2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 4.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-1", + "PM-3", + "PM-14", + "PL-2", + "PM-18", + "PM-31" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.4.1", + "A.3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.4.1", + "A3.1.1" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_advanced_or_enterprise_edition" + ] + }, + { + "Id": "IAM-02", + "Description": "Establish, document, approve, communicate, implement, apply, evaluate and maintain strong password policies and procedures. Review and update the policies and procedures at least annually.", + "Name": "Strong Password Policy and Procedures", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-12", + "GRM-06", + "GRM-09" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.1.1", + "1.5.1", + "4.1.2", + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1", + "SA1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5", + "27002: 5", + "27001: A.9.4.3", + "27002: 9.4.3", + "27017: 9.4.3", + "27018: 9.4.3", + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27001: A.7.2.2", + "27002: 7.2.2", + "27001: A.9.2.6", + "27002: 9.2.6", + "27001: A.9.2.3", + "27002: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5.1", + "27001: A.5.4", + "27001: A.5.17", + "27001: A.6.3", + "27001: A.8.5", + "27001: A.5.37" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(3)", + "AC-2(11)", + "AC-3", + "AC-3(3)", + "AC-12", + "AC-12(1)", + "IA-2", + "IA-2(10)", + "IA-5", + "IA-5(1)", + "IA-5(18)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "PR.AC-1", + "PR.AC-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.PO-01", + "GV.PO-02", + "ID.IM-03", + "PR.AA-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.4", + "12.1", + "12.1.1", + "12.11" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.1.1", + "8.3.8" + ] + } + ] + } + ], + "Checks": [ + "ram_password_policy_minimum_length", + "ram_password_policy_lowercase", + "ram_password_policy_uppercase", + "ram_password_policy_number", + "ram_password_policy_symbol", + "ram_password_policy_password_reuse_prevention", + "ram_password_policy_max_password_age", + "ram_password_policy_max_login_attempts" + ] + }, + { + "Id": "IAM-03", + "Description": "Manage, store, and review the information of system identities, and level of access.", + "Name": "Identity Inventory", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-04", + "IAM-08", + "IAM-10" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1", + "5.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.2 (c)", + "27001: A.8.1.1", + "27002: 8.1.1", + "27001: A.9.4.1", + "27002: 9.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.2 (c)", + "27001: A.5.15", + "27001: A.5.16", + "27001: A.5.18", + "27001: A.7.4", + "27001: A.8.15", + "27001: A.8.2", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-10", + "AU-10(1)", + "AU-10(2)", + "AU-16", + "AU-16(1)", + "IA-4", + "IA-4(8)", + "IA-4(9)", + "IA-5", + "IA-5(5)", + "IA-8", + "IA-8(4)", + "PM-5(1)", + "SA-8", + "SA-8(22)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-04", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.4.a" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.5", + "7.2.5.1" + ] + } + ] + } + ], + "Checks": [ + "ram_user_console_access_unused" + ] + }, + { + "Id": "IAM-04", + "Description": "Employ the separation of duties principle when implementing information system access.", + "Name": "Separation of Duties", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC1.3", + "CC5.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.2.2", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.6.1.2", + "27002: 6.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.18", + "27001: A.5.3", + "27001: A.8.2" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(3)", + "AC-2(11)", + "AC-6", + "AC-6(1)-(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4", + "6.4.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.5.3", + "6.5.4", + "7.2.1", + "7.2.2" + ] + } + ] + } + ], + "Checks": [ + "ram_policy_attached_only_to_group_or_roles" + ] + }, + { + "Id": "IAM-05", + "Description": "Employ the least privilege principle when implementing information system access.", + "Name": "Least Privilege", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-06", + "IVS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.1.1", + "27002: 9.1.1", + "27001: A.9.1.2", + "27002: 9.1.2", + "27001: A.9.2.3", + "27002: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.8.2", + "27002: 5.15 (Other information 2nd (a))" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(4)", + "IA-12", + "IA-12(2)", + "IA-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "7.1", + "7.1.1", + "7.1.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "7.2.2", + "7.2.5", + "7.2.6" + ] + } + ] + } + ], + "Checks": [ + "ram_policy_no_administrative_privileges" + ] + }, + { + "Id": "IAM-07", + "Description": "De-provision or respectively modify access of movers / leavers or system identity changes in a timely manner in order to effectively adopt and communicate identity and access management policies.", + "Name": "User Access Changes and Revocation", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.3", + "6.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.18" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(1)", + "AC-2(2)", + "AC-2(6)", + "AC-2(8)", + "AC-3", + "AC-3(8)", + "AC-6", + "AC-6(7)", + "AU-10", + "AU-10(4)", + "AU-16", + "AU-16(1)", + "CM-7", + "CM-7(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4", + "PR.IP-11" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.RR-04", + "GV.SC-10", + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1.2", + "8.1.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.2.5", + "8.2.6" + ] + } + ] + } + ], + "Checks": [ + "ram_user_console_access_unused" + ] + }, + { + "Id": "IAM-08", + "Description": "Review and revalidate user access for least privilege and separation of duties with a frequency that is commensurate with organizational risk tolerance.", + "Name": "User Access Review", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-10" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.5", + "27001: A.9.2.6", + "27001: A.9.4.1", + "27017: 9.4.1", + "27001: A.6.1.2", + "27001: A 9.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.3", + "27001: A.5.18", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(4)", + "AC-6(8)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.5.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.5.1", + "7.2.5", + "7.2.4" + ] + } + ] + } + ], + "Checks": [ + "ram_user_console_access_unused", + "ram_rotate_access_key_90_days" + ] + }, + { + "Id": "IAM-09", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the segregation of privileged access roles such that administrative access to data, encryption and key management capabilities and logging capabilities are distinct and separated.", + "Name": "Segregation of Privileged Access Roles", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.1", + "CC6.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.3", + "27002: 9.2.3", + "27017: 9.2.3", + "27018: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.2", + "27001: A.8.18", + "27002: 8.2 (j)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-3(7)", + "AC-6(4)", + "AC-6(8)", + "IA-5", + "IA-5(6)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.3", + "3.5.2", + "7.1.2", + "7.1.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1", + "3.7.6", + "6.5.3", + "6.5.4", + "7.2.1", + "7.2.2", + "10.3.1" + ] + } + ] + } + ], + "Checks": [ + "ram_policy_attached_only_to_group_or_roles", + "ram_no_root_access_key" + ] + }, + { + "Id": "IAM-10", + "Description": "Define and implement an access process to ensure privileged access roles and rights are granted for a time limited period, and implement procedures to prevent the culmination of segregated privileged access.", + "Name": "Management of Privileged Access Roles", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1", + "6.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.3", + "27002: 9.2.3", + "27017: 9.2.3", + "27018: 9.2.3", + "27001: A.9.4.4", + "27002: 9.4.4", + "27017: 9.4.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.2", + "27001: A.8.18", + "27002: 8.2 (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(7)", + "AC-3", + "AC-3(4)", + "AC-3(11)", + "AC-3(13)", + "AC-3(14)", + "AC-6", + "AC-6(4)", + "AC-6(5)", + "AC-6(8)", + "AC-12", + "AC-12(3)", + "AC-17", + "AC-17(4)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "7.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "7.2.2" + ] + } + ] + } + ], + "Checks": [ + "ram_no_root_access_key", + "ram_policy_no_administrative_privileges" + ] + }, + { + "Id": "IAM-12", + "Description": "Define, implement and evaluate processes, procedures and technical measures to ensure the logging infrastructure is read-only for all with write access, including privileged access roles, and that the ability to disable it is controlled through a procedure that ensures the segregation of duties and break glass procedures.", + "Name": "Safeguard Logs Integrity", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.3" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1", + "27018: 12.4.1", + "27001: A.12.4.2", + "27002: 12.4.2", + "27017: 12.4.2", + "27018: 12.4.2", + "27001: A.12.4.3", + "27002: 12.4.3", + "27017: 12.4.3", + "27018: 12.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15", + "27001: A.8.18", + "27002: 8.15 Protection of Logs" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(11)", + "AC-2(12)", + "IA-8", + "IA-8(4)", + "SA-8", + "SA-8(22)", + "SC-34", + "SC-34(1)", + "SC-34(2)", + "SC-36", + "SI-4", + "SI-4(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4" + ] + } + ] + } + ], + "Checks": [ + "actiontrail_oss_bucket_not_publicly_accessible" + ] + }, + { + "Id": "IAM-13", + "Description": "Define, implement and evaluate processes, procedures and technical measures that ensure users are identifiable through unique IDs or which can associate individuals to the usage of user IDs.", + "Name": "Uniquely Identifiable Users", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.1", + "27002: 9.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.16" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-3", + "AC-3(14)", + "AC-24", + "AC-24(2)", + "AU-10", + "AU-10(1)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(12)", + "IA-4", + "IA-4(1)", + "SA-8", + "SA-8(22)", + "SC-23", + "SC-23(3)", + "SC-40(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1", + "8.2", + "8.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.2.1", + "8.2.2", + "8.2.4" + ] + } + ] + } + ], + "Checks": [ + "ram_user_mfa_enabled_console_access" + ] + }, + { + "Id": "IAM-14", + "Description": "Define, implement and evaluate processes, procedures and technical measures for authenticating access to systems, application and data assets, including multifactor authentication for at least privileged user and sensitive data access. Adopt digital certificates or alternatives which achieve an equivalent level of security for system identities.", + "Name": "Strong Authentication", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.3", + "6.5", + "12.5", + "12.7" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3", + "SA1.4", + "SA1.8" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.1.2", + "27002: 9.1.2", + "27017: 9.1.2", + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27001: A.9.4.2", + "27002: 9.4.2", + "27017: 9.4.2", + "27018: 9.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.17", + "27001: A.8.5", + "27001: A.8.24", + "27002: 8.5", + "27002: 8.24 other information (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(5)", + "AC-7", + "AC-7(4)", + "AU-10", + "AU-10(2)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(8)", + "IA-2(12)", + "IA-3", + "IA-3(1)", + "IA-5", + "IA-5(2)", + "IA-5(7)", + "IA-5(9)", + "IA-5(10)", + "IA-5(12)", + "IA-5(14)-(16)", + "IA-8", + "IA-8(1)", + "IA-8(6)", + "SC-23", + "SC-23(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6", + "PR.AC-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1.2", + "8.1.3", + "8.1.6", + "8.2", + "8.3", + "8.3.2", + "12.3.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "8.3.1", + "8.3.2", + "8.4.1", + "8.4.2", + "8.4.3" + ] + } + ] + } + ], + "Checks": [ + "ram_user_mfa_enabled_console_access" + ] + }, + { + "Id": "IAM-15", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the secure management of passwords.", + "Name": "Passwords Management", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27018: 9.2.4", + "27001: A.9.3.1", + "27002: 9.3.1", + "27017: 9.3.1", + "27018: 9.3.1", + "27001: A.9.4.3", + "27002: 9.4.3", + "27017: 9.4.3", + "27018: 9.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.17" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IA-4", + "IA-4(8)", + "IA-5", + "IA-5(1)", + "IA-5(8)", + "IA-5(18)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.2", + "8.2.1-6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.2", + "2.3.1", + "8.3.5", + "8.3.6", + "8.3.7", + "8.3.8", + "8.3.9", + "8.3.10", + "8.3.10.1", + "8.6.2" + ] + } + ] + } + ], + "Checks": [ + "ram_password_policy_minimum_length", + "ram_password_policy_password_reuse_prevention", + "ram_password_policy_max_password_age" + ] + }, + { + "Id": "IAM-16", + "Description": "Define, implement and evaluate processes, procedures and technical measures to verify access to data and system functions is authorized.", + "Name": "Authorization Mechanisms", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3", + "SA1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.5", + "27002: 9.2.5", + "27017: 9.2.5", + "27018: 9.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.18" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-3", + "AC-3(5)", + "AC-4", + "AC-4(17)", + "AC-4(21)", + "AC-4(22)", + "AC-6", + "AC-6(8)", + "AC-6(9)", + "AC-12", + "AC-12(1)", + "AC-20", + "AC-20(1)", + "AU-10", + "AU-10(1)", + "AU-10(2)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(12)", + "IA-3", + "IA-3(1)", + "IA-5(1)", + "IA-5(2)", + "IA-5(5)", + "IA-5(8)", + "IA-5(10)", + "IA-5(12)", + "IA-8", + "IA-8(1)", + "IA-8(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4", + "PR.AC-6", + "PR.AC-7", + "PR.PT-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-03", + "PR.AA-04", + "PR.AA-05", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.3", + "7.1.4" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.4", + "7.2.3", + "7.2.5.1" + ] + } + ] + } + ], + "Checks": [ + "ram_policy_no_administrative_privileges", + "cs_kubernetes_rbac_enabled" + ] + }, + { + "Id": "IPY-03", + "Description": "Implement cryptographically secure and standardized network protocols for the management, import and export of data.", + "Name": "Secure Interoperability and Portability Management", + "Attributes": [ + { + "Section": "Interoperability & Portability", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IPY-04" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY1.1", + "SY1.2", + "NC1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1", + "27001: A.15.1.1", + "27002: 15.1.1", + "27017: 15.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.19", + "27001: A.5.23", + "27001: A.5.31", + "27001: A.5.32", + "27001: A.5.33", + "27001: A.5.34" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PT-2", + "PT-2(2)", + "SA-4", + "SC-16", + "SC-16(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.2.1", + "1.2.5", + "1.2.6", + "2.2.4", + "2.2.5", + "2.2.7", + "4.2.1" + ] + } + ] + } + ], + "Checks": [ + "oss_bucket_secure_transport_enabled" + ] + }, + { + "Id": "IVS-02", + "Description": "Plan and monitor the availability, quality, and adequate capacity of resources in order to deliver the required system performance as determined by the business.", + "Name": "Capacity and Resource Planning", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "No", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-04" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.3", + "27001: 6.1", + "27001: 9.1", + "27001: A.12.1.3", + "27002: 12.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.3 (b)", + "27001: 6.1", + "27001: 9.1", + "27001: A.8.6", + "27001: A.8.14" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2", + "CP-2(2)", + "SC-5", + "SC-5(2)", + "SC-4", + "SI-4" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-4", + "ID.BE-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.IR-04", + "GV.OC-04" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "IVS-03", + "Description": "Monitor, encrypt and restrict communications between environments to only authenticated and authorized connections, as justified by the business. Review these configurations at least annually, and support them by a documented justification of all allowed services, protocols, ports, and compensating controls.", + "Name": "Network Security", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-06" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.8", + "3.1", + "12.2", + "13.6", + "13.9" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "NC1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.13.1.1", + "27002: 13.1.1", + "27001: A.13.1.2", + "27002: 13.1.2", + "27001: A.13.1.3", + "27002: 13.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.5.15", + "27001: A.5.37", + "27001: A.8.5", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.21", + "27001: A.8.22", + "27001: A.8.24", + "27002: A.5.15 2nd c)", + "27002: 8.20", + "27002: 8.21", + "27002: 8.22", + "27002: 8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-1", + "SC-4", + "SC-7", + "SC-7(4)", + "SC-7(5)", + "SC-7(8)", + "SC-7(9)", + "SC-7(11)", + "SC-8", + "SC-8(1)", + "SC-11", + "SC-12", + "SC-16", + "SC-23", + "SC-29", + "SC-29(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-5", + "PR.AC-7", + "PR.PT-4", + "DE.CM-1", + "DE.CM-7", + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.IR-01", + "PR.AA-03", + "PR.AA-05", + "DE.CM-01", + "PR.DS-02", + "ID.AM-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "1.1.6", + "1.2", + "1.2.3", + "2.2", + "4.1.1", + "10.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.2.5", + "1.2.6", + "1.2.7", + "1.4.2", + "2.2.4", + "2.2.5", + "2.2.7", + "4.2.1", + "10.1.1" + ] + } + ] + } + ], + "Checks": [ + "vpc_flow_logs_enabled", + "ecs_securitygroup_restrict_ssh_internet", + "ecs_securitygroup_restrict_rdp_internet" + ] + }, + { + "Id": "IVS-04", + "Description": "Harden host and guest OS, hypervisor or infrastructure control plane according to their respective best practices, and supported by technical controls, as part of a security baseline.", + "Name": "OS Hardening and Base Controls", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.8", + "CC7.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-07", + "IVS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "4.1", + "4.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3", + "5.2.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY1.1", + "SY1.3", + "SY1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.14.2.2", + "27002: 14.2.2", + "27001: A.14.2.3", + "27001 A.14.2.4", + "27018: 12.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.5.37", + "27001: A.8.5", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.22", + "27001: A.8.24", + "27002: 8.20", + "27002: 8.22", + "27002: 8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-6", + "CM-6(1)", + "SC-29", + "SC-29(1)", + "SC-2", + "SC-7", + "SC-7(12)", + "SC-30", + "SC-34", + "SC-35", + "SC-39", + "SC-44" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-1", + "PR.PT-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.1" + ] + } + ] + } + ], + "Checks": [ + "ecs_instance_latest_os_patches_applied", + "ecs_instance_endpoint_protection_installed" + ] + }, + { + "Id": "IVS-06", + "Description": "Design, develop, deploy and configure applications and infrastructures such that CSP and CSC (tenant) user access and intra-tenant access is appropriately segmented and segregated, monitored and restricted from other tenants.", + "Name": "Segmentation and Segregation", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-09" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.3.4", + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SC2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.1", + "27001: A.13.1.3", + "27002: 13.1.3", + "27017: 13.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.1", + "27001: A.5.15", + "27001: A.5.20", + "27001: A.8.3", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.22", + "27002: 5.15 (b)", + "27002: 8.3 (b)", + "27002: 8.16 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-3", + "SC-7", + "SC-7(20)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.AC-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.IR-01", + "PR.PS-01", + "PR.PS-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.6", + "8.3.1", + "10.8", + "11.3", + "A3.2.1", + "A3.3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "A1.1.1", + "A1.1.2", + "A1.1.3" + ] + } + ] + } + ], + "Checks": [ + "cs_kubernetes_network_policy_enabled", + "cs_kubernetes_private_cluster_enabled", + "ecs_instance_no_legacy_network" + ] + }, + { + "Id": "IVS-07", + "Description": "Use secure and encrypted communication channels when migrating servers, services, applications, or data to cloud environments. Such channels must include only up-to-date and approved protocols.", + "Name": "Migration to Cloud Environments", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-10" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.4", + "IM1.4", + "NC1.4", + "SC2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.13.1.1", + "27002: 13.1.1", + "27017: 13.1.1", + "27018: 13.1.1", + "27001: A.13.1.2", + "27002: 13.1.2", + "27017: 13.1.2", + "27018: 13.1.2", + "27001: A.13.1.3", + "27002: 13.1.3", + "27017: 13.1.3", + "27018: 13.1.3", + "27001: A.13.2.1", + "27002: 13.2.1", + "27017: 13.2.1", + "27018: 13.2.1", + "27001: A.13.2.2", + "27002: 13.2.2", + "27017: 13.2.2", + "27018: 13.2.2", + "27001: A.13.2.3", + "27002: 13.2.3", + "27017: 13.2.3", + "27018: 13.2.3", + "27001: A.13.2.4", + "27002: 13.2.4", + "27017: 13.2.4", + "27018: 13.2.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.8.20", + "27001: A.8.24", + "27002: 8.20 (e)", + "27002: 8.24 Guidance (b,f), other information (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-17", + "AC-20", + "SC-7", + "SC-7(28)", + "SC-8", + "SC-8(1)", + "SC-12", + "SC-23", + "SC-29", + "SI-7", + "SI-7(1)-(3)", + "SI-7(5)-(10)", + "SI-7(12)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "4.2.1" + ] + } + ] + } + ], + "Checks": [ + "rds_instance_ssl_enabled" + ] + }, + { + "Id": "IVS-09", + "Description": "Define, implement and evaluate processes, procedures and defense-in-depth techniques for protection, detection, and timely response to network-based attacks.", + "Name": "Network Defense", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.6", + "CC6.8", + "CC7.1", + "CC7.2", + "CC7.5" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-13" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "13.3", + "13.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.3", + "5.2.4", + "5.2.5", + "5.2.7", + "5.3.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "NC1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1", + "27001: 6.2", + "27001: A.14.1.2", + "27002: 14.1.2", + "27017: 14.1.2", + "27001: A.11.1.4", + "27002: 11.1.4", + "27017: 11.1.4", + "27018: 16.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1", + "27001: 6.2", + "27001: A.5.24", + "27001: A.5.26", + "27001: A.8.8", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.21", + "27001: A.8.22", + "27001: A.8.26", + "27002: 8.8 (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-8", + "PL-8(1)", + "SC-5", + "SC-5(1)", + "SC-5(3)", + "SC-7", + "SC-7(13)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.DP-1", + "DE.CM-1", + "DE.CM-7", + "PR.AC-5", + "RS.MI-2", + "PR.DS-2", + "RS.RP-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-03", + "DE.CM-01", + "PR.IR-01", + "RS.MA-01", + "RS.MI-01", + "RS.MI-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.6", + "1.1", + "1.2", + "1.3", + "1.5", + "12.10.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.1.1", + "1.3.1", + "1.3.2", + "1.3.3", + "1.4.1", + "1.4.2", + "1.4.3", + "1.4.4", + "1.4.5", + "1.5.1", + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_advanced_or_enterprise_edition", + "sls_cloud_firewall_changes_alert_enabled" + ] + }, + { + "Id": "LOG-02", + "Description": "Define, implement and evaluate processes, procedures and technical measures to ensure the security and retention of audit logs.", + "Name": "Audit Logs Protection", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.1", + "8.9", + "8.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "3.1.3", + "5.1.2", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3", + "27002: 18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.28", + "27001: A.5.33", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-4", + "AU-11" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.IP-4", + "PR.IP-6", + "PR.PT-1", + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.DS-01", + "PR.DS-02", + "ID.AM-08", + "PR.DS-11", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5", + "10.7" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4", + "10.5.1" + ] + } + ] + } + ], + "Checks": [ + "actiontrail_oss_bucket_not_publicly_accessible", + "sls_logstore_retention_period" + ] + }, + { + "Id": "LOG-03", + "Description": "Identify and monitor security-related events within applications and the underlying infrastructure. Define and implement a system to generate alerts to responsible stakeholders based on such events and corresponding metrics.", + "Name": "Security Monitoring and Alerting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-03", + "SEF-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4", + "5.2.7", + "1.6.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2", + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.28", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-5", + "AU-5(2)", + "AU-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.AE-2", + "DE.AE-3", + "DE.AE-5", + "DE.CM-1", + "DE.CM-2", + "DE.CM-3", + "DE.CM-4", + "DE.CM-5", + "DE.CM-6", + "DE.CM-7", + "DE.DP-1", + "DE.DP-4", + "DE.AE-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.AE-02", + "DE.AE-03", + "DE.AE-04", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1", + "10.2.2", + "10.4.1.1", + "10.4.2.1", + "10.4.3" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_advanced_or_enterprise_edition", + "securitycenter_notification_enabled_high_risk", + "sls_unauthorized_api_calls_alert_enabled", + "sls_root_account_usage_alert_enabled", + "sls_management_console_signin_without_mfa_alert_enabled" + ] + }, + { + "Id": "LOG-04", + "Description": "Restrict audit logs access to authorized personnel and maintain records that provide unique access accountability.", + "Name": "Audit Logs Access and Accountability", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.14" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "3.1.1", + "4.1.2", + "4.1.3", + "4.2.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.2", + "27001: A.12.4.1", + "27002: 12.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.33", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(4)", + "AU-9(6)", + "AU-10" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.1", + "10.2.1", + "10.2.3", + "10.5.1", + "10.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1.3", + "10.3.1" + ] + } + ] + } + ], + "Checks": [ + "actiontrail_oss_bucket_not_publicly_accessible" + ] + }, + { + "Id": "LOG-05", + "Description": "Monitor security audit logs to detect activity outside of typical or expected patterns. Establish and follow a defined process to review and take appropriate and timely actions on detected anomalies.", + "Name": "Audit Logs Monitoring and Response", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.8", + "8.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.1", + "1.6.2", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.3", + "27002: 12.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15", + "27001: A.8.16" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-6", + "AU-6(1)", + "AU-6(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-3", + "PR.PT-1", + "RS.AN-1", + "RS.CO-1.", + "DE.AE-1", + "DE.AE-5", + "DE.DP-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-03", + "PR.PS-04", + "DE.AE-02", + "DE.AE-03", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.6", + "10.6.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.4.1.1", + "10.4.2.1" + ] + } + ] + } + ], + "Checks": [ + "sls_unauthorized_api_calls_alert_enabled", + "sls_root_account_usage_alert_enabled", + "sls_management_console_signin_without_mfa_alert_enabled", + "sls_ram_role_changes_alert_enabled", + "sls_security_group_changes_alert_enabled", + "sls_vpc_changes_alert_enabled", + "sls_vpc_network_route_changes_alert_enabled", + "sls_management_console_authentication_failures_alert_enabled", + "sls_customer_created_cmk_changes_alert_enabled", + "sls_oss_bucket_policy_changes_alert_enabled", + "sls_oss_permission_changes_alert_enabled", + "sls_cloud_firewall_changes_alert_enabled", + "sls_rds_instance_configuration_changes_alert_enabled" + ] + }, + { + "Id": "LOG-07", + "Description": "Establish, document and implement which information meta/data system events should be logged. Review and update the scope at least annually or whenever there is a change in the threat environment.", + "Name": "Logging Scope", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5.3", + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5.3", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-1", + "AU-14", + "AU-16" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.SC-3", + "ID.SC-4", + "PR.PT-1", + "ID.GV-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1", + "10.2.2" + ] + } + ] + } + ], + "Checks": [ + "actiontrail_multi_region_enabled", + "vpc_flow_logs_enabled" + ] + }, + { + "Id": "LOG-08", + "Description": "Generate audit records containing relevant security information.", + "Name": "Log Records", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-3", + "AU-3(1)", + "AU-3(3)", + "AU-6", + "AU-6(8)", + "AU-12", + "AU-12(1)", + "AU-12(2)", + "AU-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.PT-1", + "DE.AE-3", + "DE.CM-1", + "DE.CM-2", + "DE.CM-3", + "DE.CM-6", + "DE.CM-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.2" + ] + } + ] + } + ], + "Checks": [ + "actiontrail_multi_region_enabled", + "vpc_flow_logs_enabled", + "oss_bucket_logging_enabled", + "rds_instance_sql_audit_enabled", + "cs_kubernetes_log_service_enabled", + "rds_instance_postgresql_log_connections_enabled", + "rds_instance_postgresql_log_disconnections_enabled", + "rds_instance_postgresql_log_duration_enabled" + ] + }, + { + "Id": "LOG-09", + "Description": "The information system protects audit records from unauthorized access, modification, and deletion.", + "Name": "Log Protection", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-04", + "IVS-01" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.2", + "27002: 12.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(2)", + "AU-9(3)", + "AU-9(4)", + "AU-12(3)", + "AU-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.IP-4", + "PR.IP-6", + "PR.PT-1", + "PR.DS-1", + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.DS-01", + "PR.DS-02", + "PR.DS-11" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5", + "10.5.1", + "10.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4" + ] + } + ] + } + ], + "Checks": [ + "actiontrail_oss_bucket_not_publicly_accessible" + ] + }, + { + "Id": "LOG-10", + "Description": "Establish and maintain a monitoring and internal reporting capability over the operations of cryptographic, encryption and key management policies, processes, procedures, and controls.", + "Name": "Encryption Monitoring and Reporting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-02", + "EKM-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1", + "27002: 10.1", + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-1", + "AU-9", + "AU-9(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "PR.PT-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.1.1", + "10.2.1", + "10.4.1" + ] + } + ] + } + ], + "Checks": [ + "sls_customer_created_cmk_changes_alert_enabled" + ] + }, + { + "Id": "LOG-11", + "Description": "Log and monitor key lifecycle management events to enable auditing and reporting on usage of cryptographic keys.", + "Name": "Transaction/Activity Logging", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.PT-1", + "DE.AE-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-09" + ] + } + ] + } + ], + "Checks": [ + "actiontrail_multi_region_enabled" + ] + }, + { + "Id": "LOG-13", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the reporting of anomalies and failures of the monitoring system and provide immediate notification to the accountable party.", + "Name": "Failures and Anomalies Reporting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.3", + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.1", + "27002: 16.1.1", + "27001: A.16.1.2", + "27017: 16.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.24", + "27001: A.6.8", + "27002: 6.8 (g)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-5", + "AU-5(2)", + "AU-6", + "AU-6(3)", + "AU-6(4)", + "AU-6(5)", + "AU-16" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-3", + "DE.DP-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.AE-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.4.3", + "10.7.1", + "10.7.2", + "10.7.3" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_advanced_or_enterprise_edition", + "securitycenter_notification_enabled_high_risk" + ] + }, + { + "Id": "SEF-03", + "Description": "'Establish, document, approve, communicate, apply, evaluate and maintain a security incident response plan, which includes but is not limited to: relevant internal departments, impacted CSCs, and other business critical relationships (such as supply-chain) that may be impacted.'", + "Name": "Incident Response Plans", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2", + "CC7.3", + "CC7.4" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "17.2", + "17.4" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27017: CLD.12.1.5", + "27018: 16.1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: A.5.26", + "27002: 5.26 (e,f)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IR-1", + "IR-2", + "IR-2(1)-(3)", + "IR-3", + "IR-3(1)-(3)", + "IR-4", + "IR-4(1)-(15)", + "IR-5", + "IR-5(1)", + "IR-6", + "IR-6(1)-(3)", + "IR-7", + "IR-7(1)", + "IR-7(2)", + "IR-8", + "IR-8(1)", + "IR-9", + "IR-9(1)-(4)", + "PM-12" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "RS.CO-1", + "RS.CO-4", + "ID.AM-6", + "ID.GV-2", + "ID.SC-5", + "PR.IP-9", + "PR.IP10" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AT-01", + "PR.AT-02", + "RS.MA-01", + "GV.SC-08", + "ID.IM-02", + "ID.IM-04", + "RC.RP-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.1", + "12.10.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1", + "12.10.5" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "SEF-06", + "Description": "Define, implement and evaluate processes, procedures and technical measures supporting business processes to triage security-related events.", + "Name": "Event Triage Processes", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.4", + "27002: 16.1.4", + "27017: 16.1.4", + "27018: 16.1.4", + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27018: 16.1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.25" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-7", + "CA-7(3)", + "CA-7(4)", + "CA-7(5)", + "CA-7(6)", + "IR-4", + "IR-4(1)", + "IR-4(3)", + "IR-4(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.AE-2", + "DE.AE-4", + "RS.RP-1", + "RS.AN-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "RS.MA-02", + "RS.MA-03", + "RS.AN-03", + "DE.AE-02", + "DE.AE-04", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "RS.MI-02", + "RC.RP-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_advanced_or_enterprise_edition" + ] + }, + { + "Id": "SEF-08", + "Description": "Maintain points of contact for applicable regulation authorities, national and local law enforcement, and other legal jurisdictional authorities.", + "Name": "Points of Contact Maintenance", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "17.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 4.2", + "27001: A.6.1.3", + "27002: 6.1.3", + "27017: 6.1.3", + "27018: 6.1.3", + "27001: A.16.1.1", + "27002: 16.1.1", + "27001: A.18.1.1", + "27002: 18.1.1", + "27017: 18.1.1", + "27018: 18.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.5", + "27001: A.5.24", + "27002: 5.24 Incident management procedure (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IR-4", + "IR-4(8)", + "IR-6", + "IR-6(3)", + "IR-7", + "IR-7(2)", + "PM-21", + "PM-23", + "PM-26" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-2", + "RS.CO-3", + "RS.CO-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.RR-02", + "RS.CO-02", + "RS.CO-03" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "TVM-02", + "Description": "Establish, document, approve, communicate, apply, evaluate and maintain policies and procedures to protect against malware on managed assets. Review and update the policies and procedures at least annually.", + "Name": "Malware Protection Policy and Procedures", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC6.8" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-01", + "GRM-06", + "GRM-09" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "9.7", + "10.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.1.1", + "1.5.1", + "5.2.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS1.2", + "TS1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5", + "27002: 5", + "27001: A.12.2.1", + "27001: A.6.2.1", + "27002: 6.2.1 (h)", + "27001: A.6.2.2", + "27002: 6.2.2 (j)", + "27001: A.7.2.2", + "27002: 7.2.2 (d)", + "27001: A.10.1.1", + "27002: 10.1.1 (g)", + "27001: A.13.2.1", + "27002: 13.2.1 (b)", + "27001: A.15.1.2", + "27017: 15.1.2", + "27001: A.12.2.1", + "27002: 12.2.1 (a),(d)", + "27017: CLD.9.5.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5.1", + "27001: A.5.4", + "27001: A.5.7", + "27001: A.5.37", + "27001: A.8.7", + "27002: 5.7 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-3", + "RA-3(3)", + "RA-5", + "RA-5(3)", + "RA-5(5)", + "SI-3", + "SI-3(4)", + "SI-3(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "DE.CM-4", + "DE.CM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.PO-01", + "GV.PO-02", + "ID.IM-03", + "DE.CM-01", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.4", + "12.1", + "12.1.1", + "12.3.1", + "12.5.1", + "12.11" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.1.1", + "12.1.2", + "5.1.1", + "5.3.2.1" + ] + } + ] + } + ], + "Checks": [ + "ecs_instance_endpoint_protection_installed" + ] + }, + { + "Id": "TVM-03", + "Description": "Define, implement and evaluate processes, procedures and technical measures to enable both scheduled and emergency responses to vulnerability identifications, based on the identified risk.", + "Name": "Vulnerability Remediation Schedule", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC7.1", + "CC7.4" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "7.2", + "7.7", + "17.9" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1", + "TM2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.12.2.1", + "27001: A.12.6.1", + "27002: 12.6.1(c)(d)(j)", + "27018: 12.6.1(k)(i)" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.8.7", + "27001: A.8.8", + "27001: A.8.32", + "27002: 8.7", + "27002: 8.8", + "27002: 8.32" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-31", + "RA-3", + "RA-3(1)", + "RA-5", + "RA-5(2)-(4)", + "RA-5(6)", + "SI-3", + "SI-3(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "RS.AN-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01", + "ID.RA-06", + "ID.RA-08", + "PR.PS-02", + "PR.PS-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "6.1.a", + "6.1.b" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.1.1", + "6.3.1", + "6.3.2", + "6.3.3", + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "ecs_instance_latest_os_patches_applied" + ] + }, + { + "Id": "TVM-04", + "Description": "Define, implement and evaluate processes, procedures and technical measures to update detection tools, threat signatures, and indicators of compromise on a weekly, or more frequent basis.", + "Name": "Detection Updates", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "No mapping" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "10.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS1.3", + "TS1.4", + "TM1.3", + "TM1.4", + "IM1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.5.1.1", + "27002: 5.1.1 (h)", + "27001: A.12.6.1", + "27002: 12.6.1 (b),(c)" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.5.1", + "27001: A.8.8", + "27001: A.8.15", + "27001: A.8.16", + "27002: 5.1", + "27002: 5.37", + "27002: 8.8", + "27002: 8.15 (d)", + "27002: 8.16 (d,e)", + "27002: 8.31 2nd (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-7", + "CM-7(4)", + "RA-3", + "RA-3(3)", + "RA-5(2)", + "SA-10", + "SA-10(5)", + "SA-11", + "SA-11(2)", + "SI-2", + "SI-2(4)", + "SI-3", + "SI-3(4)", + "SI-4", + "SI-4(9)", + "SI-4(24)", + "SI-8", + "SI-8(2)", + "SI-8(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-02", + "ID.RA-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.2", + "5.2a", + "5.2b", + "5.2c" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "5.3.1" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_advanced_or_enterprise_edition", + "securitycenter_vulnerability_scan_enabled" + ] + }, + { + "Id": "TVM-05", + "Description": "Define, implement and evaluate processes, procedures and technical measures to identify updates for applications which use third party or open source libraries according to the organization's vulnerability management policy.", + "Name": "External Library Vulnerabilities", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC3.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "No mapping" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1", + "SD2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.12.6.2", + "27002: 12.6.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A 5.6", + "27001: A.8.19", + "27001: A.8.8", + "27001: A.8.28", + "27001: A.8.31", + "27002: 5.6 (c)", + "27001: 8.19", + "27001: 8.8", + "27001: 8.28", + "27001: 8.31" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-5", + "RA-5(3)", + "SA-11", + "SA-11(2)", + "SA-11(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01", + "ID.RA-03", + "PR.PS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "6.2", + "6.3.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "6.3.2", + "6.3.3" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_vulnerability_scan_enabled" + ] + }, + { + "Id": "TVM-07", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the detection of vulnerabilities on organizationally managed assets at least monthly.", + "Name": "Vulnerability Identification", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "7.1", + "7.5", + "7.6" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.5", + "5.2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.6", + "27001: A.12.6.1", + "27002: 12.6.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.8", + "27002: 8.8" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-5", + "RA-5(4)", + "RA-5(5)", + "SA-11", + "SA-11(5)", + "SA-15(5)", + "SC-7", + "SC-7(10)", + "SI-3(8)", + "SI-3(10)", + "SI-7", + "SI-7(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.RA-1", + "DE.CM-8", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "11.2", + "11.2.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "6.3.2", + "6.3.3", + "11.3.2", + "11.3.2.1" + ] + } + ] + } + ], + "Checks": [ + "securitycenter_vulnerability_scan_enabled", + "securitycenter_advanced_or_enterprise_edition" + ] + }, + { + "Id": "UEM-08", + "Description": "Protect information from unauthorized disclosure on managed endpoint devices with storage encryption.", + "Name": "Storage Encryption", + "Attributes": [ + { + "Section": "Universal Endpoint Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "MOS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.6" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "3.1.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "PA1.2", + "PA1.3", + "PA1.5", + "PA2.2", + "PM1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.11.2.7", + "27002: 11.2.7", + "27001: A.18.1.1", + "27017: 18.1.1", + "27001: A.12.3.1", + "27017: 12.3.1", + "27018: A.11.4", + "27018: A.11.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.1", + "27002: 8.1 (h)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-19(5)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.4", + "3.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.5.1", + "3.6" + ] + } + ] + } + ], + "Checks": [ + "ecs_attached_disk_encrypted", + "ecs_unattached_disk_encrypted" + ] + }, + { + "Id": "UEM-11", + "Description": "Configure managed endpoints with Data Loss Prevention (DLP) technologies and rules in accordance with a risk assessment.", + "Name": "Data Loss Prevention", + "Attributes": [ + { + "Section": "Universal Endpoint Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.13" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.5", + "PA2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.3", + "27002: 12.3", + "27001: A.8.3.1", + "27002: 8.3.1", + "27001: A.12.2", + "27002: 12.2", + "27001: A.18.1.3", + "27002: 18.1.3", + "27001: A.6.1.1", + "27017: 6.1.1", + "27018: 12.3.1", + "27018: 10.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.12", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-7", + "SC-7(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02", + "PR.DS-10", + "PR.PS-01", + "ID.AM-08", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A3.2.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "A3.2.6" + ] + } + ] + } + ], + "Checks": [] + } + ] +} diff --git a/prowler/compliance/aws/csa_ccm_4.0_aws.json b/prowler/compliance/aws/csa_ccm_4.0_aws.json new file mode 100644 index 0000000000..6d0a019c2b --- /dev/null +++ b/prowler/compliance/aws/csa_ccm_4.0_aws.json @@ -0,0 +1,7604 @@ +{ + "Framework": "CSA-CCM", + "Name": "CSA Cloud Controls Matrix (CCM) v4.0.13", + "Version": "4.0", + "Provider": "AWS", + "Description": "The Cloud Security Alliance (CSA) Cloud Controls Matrix (CCM) is a cybersecurity control framework for cloud computing, composed of 197 control objectives structured in 17 domains covering all key aspects of cloud technology. The CCM can be used as a tool for the systematic assessment of a cloud implementation, and provides guidance on which security controls should be implemented by which actor within the cloud supply chain.", + "Requirements": [ + { + "Id": "A&A-02", + "Description": "Conduct independent audit and assurance assessments according to relevant standards at least annually.", + "Name": "Independent Assessments", + "Attributes": [ + { + "Section": "Audit & Assurance", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC4.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AAC-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.5.2", + "5.2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "AS1.1", + "AS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.2.1", + "27002: 18.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.35", + "27001: A.5.36" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-2", + "CA-2(1)", + "CA-2(2)", + "CA-7", + "CA-7(1)" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-01" + ] + } + ] + } + ], + "Checks": [ + "securityhub_enabled" + ] + }, + { + "Id": "A&A-04", + "Description": "Verify compliance with all relevant standards, regulations, legal/contractual, and statutory requirements applicable to the audit.", + "Name": "Requirements Compliance", + "Attributes": [ + { + "Section": "Audit & Assurance", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC3.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-01", + "GRM-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "7.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "AS1.1", + "AS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.3.2", + "27001: A.18.2.2", + "27002: 18.2.2", + "27001: A.18.2.3", + "27002: 18.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.3.2", + "27001: A.5.31", + "27001: A.5.32", + "27001: A.5.33", + "27001: A.5.34", + "27001: A.5.36" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-1" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-3", + "DE.DP-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-01" + ] + } + ] + } + ], + "Checks": [ + "securityhub_enabled", + "config_recorder_all_regions_enabled" + ] + }, + { + "Id": "AIS-04", + "Description": "Define and implement a SDLC process for application design, development, deployment, and operation in accordance with security requirements defined by the organization.", + "Name": "Secure Application Design and Development", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AIS-01", + "AIS-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.4", + "5.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.1.1", + "27002: 14.1.1", + "27017: 14.1.1", + "27001: A.14.1.2", + "27002: 14.1.2", + "27017: 14.1.2", + "27001: A.14.2.1", + "27002: 14.2.1", + "27017: 14.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.8", + "27001: A.8.25", + "27001: A.8.26", + "27001: A.8.28" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-2", + "PL-8", + "PL-8(1)", + "SA-3", + "SA-3(1)", + "SA-4", + "SA-4(2)", + "SA-4(3)", + "SA-4(8)", + "SA-4(9)", + "SA-5", + "SA-8", + "SA-8(1)-(7)", + "SA-8(9)-(13)", + "SA-8(15)-(20)", + "SA-8(22)", + "SA-8(24)-(28)", + "SA-8(30)-(33)", + "SA-17", + "SA-17(1)-(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-6", + "PR.DS-7", + "PR.IP-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "PR.IR-01", + "PR.PS-01", + "PR.PS-02", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.1", + "6.2.3", + "6.5.2" + ] + } + ] + } + ], + "Checks": [ + "codebuild_project_source_repo_url_no_sensitive_credentials", + "codebuild_project_no_secrets_in_variables" + ] + }, + { + "Id": "AIS-05", + "Description": "Implement a testing strategy, including criteria for acceptance of new information systems, upgrades and new versions, which provides application security assurance and maintains compliance while enabling organizational speed of delivery goals. Automate when applicable and possible.", + "Name": "Automated Application Security Testing", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AIS-01", + "AIS-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.12", + "16.13" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD2.3", + "SD2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.2.8", + "27001: A.14.2.9", + "27001: A.12.1.2", + "27002: 12.1.2", + "27001: A.14.1.1", + "27002: 14.1.1", + "27001: A.14.2.2", + "27002: 14.2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.25", + "27001: A.8.29", + "27001: A.8.32", + "27002: 8.25 (e)", + "27002: 8.32 (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SA-11", + "SA-11(1)-(9)", + "SI-6", + "SI-6(2)", + "SI-6(3)", + "SI-10", + "SI-10(1)-(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.PT-3", + "PR.IP-12", + "DE.CM-8" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "ID.RA-01", + "PR.PS-01", + "PR.PS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A.3.2.2", + "A.3.2.2.1", + "6.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.4", + "6.4.1", + "6.4.2", + "6.5.1" + ] + } + ] + } + ], + "Checks": [ + "inspector2_is_enabled", + "ecr_repositories_scan_vulnerabilities_in_latest_image", + "ecr_registry_scan_images_on_push_enabled" + ] + }, + { + "Id": "AIS-07", + "Description": "Define and implement a process to remediate application security vulnerabilities, automating remediation when possible.", + "Name": "Application Vulnerability Remediation", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.1", + "CC7.4", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.2", + "16.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27001: A.12.6.1", + "27002: 12.6.1", + "27017: 12.6.1", + "27018: 12.6.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.26", + "27001: A.8.8", + "27002: 5.26 (j)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SI-2", + "SI-2(2)-(6)", + "SA-11", + "SA-11(2)", + "SA-15", + "SA-15(1)-(3)", + "SA-15(5)-(8)", + "SA-15(10)-(12)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.IP-12", + "DE.CM-8", + "RS.AN-5", + "RS.MI-3", + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "ID.RA-01", + "ID.RA-06", + "ID.RA-08", + "PR.PS-02", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.2", + "6.5", + "6.5.1-10" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "11.3.1", + "11.3.1.1" + ] + } + ] + } + ], + "Checks": [ + "inspector2_is_enabled", + "inspector2_active_findings_exist" + ] + }, + { + "Id": "BCR-08", + "Description": "Periodically backup data stored in the cloud. Ensure the confidentiality, integrity and availability of the backup, and verify data restoration from backup for resiliency.", + "Name": "Backup", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "A1.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "11.1", + "11.2", + "11.3", + "11.4", + "11.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8", + "5.2.9" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.3", + "27017: 12.3", + "27018: 12.3.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.13", + "27001: A.5.23", + "27001: A.5.30", + "27002: 8.13", + "27002: 5.23 2nd (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-4", + "CP-4(4)", + "CP-6", + "CP-6(1)-(3)", + "CP-9", + "CP-9(1)", + "CP-9(2)", + "CP-10", + "CP-10(2)", + "CP-10(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-4", + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-11", + "RC.RP-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "9.5.1", + "12.10.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1", + "10.3.3" + ] + } + ] + } + ], + "Checks": [ + "backup_plans_exist", + "backup_vaults_exist", + "backup_vaults_encrypted", + "backup_recovery_point_encrypted", + "dynamodb_tables_pitr_enabled", + "dynamodb_table_protected_by_backup_plan", + "ec2_ebs_volume_snapshots_exists", + "ec2_ebs_volume_protected_by_backup_plan", + "efs_have_backup_enabled", + "rds_instance_backup_enabled", + "rds_instance_protected_by_backup_plan", + "rds_cluster_protected_by_backup_plan", + "redshift_cluster_automated_snapshot", + "s3_bucket_object_versioning", + "documentdb_cluster_backup_enabled", + "neptune_cluster_backup_enabled", + "elasticache_redis_cluster_backup_enabled", + "fsx_file_system_copy_tags_to_backups_enabled", + "lightsail_instance_automated_snapshots", + "dlm_ebs_snapshot_lifecycle_policy_exists" + ] + }, + { + "Id": "BCR-09", + "Description": "Establish, document, approve, communicate, apply, evaluate and maintain a disaster response plan to recover from natural and man-made disasters. Update the plan at least annually or upon significant changes.", + "Name": "Disaster Response Plan", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "CC3.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8", + "5.2.9", + "1.6.1", + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "BC1.4", + "BC2.1", + "BC2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.29", + "27001: A.5.30", + "27002: 5.29", + "27002: 5.30" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2(1)", + "CP-2(2)", + "CP-2(3)", + "CP-2(5)", + "CP-2(6)", + "CP-2(7)", + "CP-2(8)", + "PE-13", + "PE-13(1)", + "PE-13(2)", + "PE-13(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-9", + "PR.IP-10", + "RC.IM-1", + "RC.IM-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-04" + ] + } + ] + } + ], + "Checks": [ + "drs_job_exist", + "ssmincidents_enabled_with_plans" + ] + }, + { + "Id": "BCR-11", + "Description": "Supplement business-critical equipment with redundant equipment independently located at a reasonable minimum distance in accordance with applicable industry standards.", + "Name": "Equipment Redundancy", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "No", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "CC3.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-06" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "BC1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.20", + "27001: A.7.11", + "27001: A.8.14", + "27002: 5.20 (t)", + "27002: 8.14 (c)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2", + "CP-2(2)", + "CP-4(3)", + "CP-6", + "CP-6(1)", + "CP-7", + "CP-8", + "CP-8(1)-(3)", + "CP-9", + "CP-9(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.BE-4", + "ID.BE-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.OC-04", + "GV.OC-05", + "PR.IR-03" + ] + } + ] + } + ], + "Checks": [ + "rds_instance_multi_az", + "rds_cluster_multi_az", + "elbv2_is_in_multiple_az", + "elb_is_in_multiple_az", + "autoscaling_group_multiple_az", + "autoscaling_group_multiple_instance_types", + "ec2_ebs_volume_protected_by_backup_plan", + "elasticache_redis_cluster_multi_az_enabled", + "elasticache_redis_cluster_automatic_failover_enabled", + "opensearch_service_domains_fault_tolerant_data_nodes", + "opensearch_service_domains_fault_tolerant_master_nodes", + "dynamodb_accelerator_cluster_multi_az", + "documentdb_cluster_multi_az_enabled", + "neptune_cluster_multi_az", + "efs_multi_az_enabled", + "vpc_vpn_connection_tunnels_up", + "directconnect_connection_redundancy", + "directconnect_virtual_interface_redundancy", + "networkfirewall_multi_az", + "vpc_endpoint_multi_az_enabled", + "awslambda_function_vpc_multi_az", + "redshift_cluster_multi_az_enabled" + ] + }, + { + "Id": "CCC-04", + "Description": "Restrict the unauthorized addition, removal, update, and management of organization assets.", + "Name": "Unauthorized Change Protection", + "Attributes": [ + { + "Section": "Change Control and Configuration Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "CCC-04" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.1", + "1.3.4", + "5.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.4", + "SM2.6" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.1.4", + "27002: 12.1.4", + "27001: A.12.4.2", + "27002: 12.4.2", + "27001: A.14.2.2", + "27017: 14.2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.3", + "27001: A.8.4", + "27001: A.8.15", + "27001: A.8.31", + "27001: A.8.32" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-7", + "CA-7(4)", + "CM-3", + "CM-3(1)", + "CM-3(5)", + "CM-3(7)", + "CM-3(8)", + "CM-5", + "CM-5(1)", + "CM-5(4)", + "CM-5(5)", + "CM-6", + "CM-6(1)", + "CM-6(2)", + "CM-7", + "CM-7(1)", + "CM-7(4)", + "CM-7(5)", + "CM-7(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-1", + "ID.AM-2", + "ID.AM-4", + "PR.MA-1", + "PR.MA-2", + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-01", + "ID.AM-02", + "ID.AM-04", + "ID.AM-08", + "PR.PS-02", + "PR.PS-03", + "PR.PS-05", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.5.1", + "6.5.2" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_multi_region_enabled", + "cloudtrail_log_file_validation_enabled", + "s3_bucket_object_lock", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "servicecatalog_portfolio_shared_within_organization_only" + ] + }, + { + "Id": "CCC-07", + "Description": "Implement detection measures with proactive notification in case of changes deviating from the established baseline.", + "Name": "Detection of Baseline Deviation", + "Attributes": [ + { + "Section": "Change Control and Configuration Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-01" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.5.1", + "1.5.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.2.2", + "27001: A.14.2.4", + "27001: A.12.4.1", + "27002: 12.4.1 (g)", + "27001: A.5.1.1", + "27017: 5.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.9", + "27001: A.8.15", + "27002: 8.9" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-6", + "CM-6(2)", + "SI-2", + "SI-2(2)-(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.MA-1", + "PR.IP-1", + "DE.DP-4", + "PR.IP-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-01", + "DE.CM-09", + "DE.AE-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4.5.3", + "6.4.5.4", + "11.5", + "11.5.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "11.5.2", + "11.6.1" + ] + } + ] + } + ], + "Checks": [ + "config_recorder_all_regions_enabled", + "guardduty_is_enabled", + "securityhub_enabled", + "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_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_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk" + ] + }, + { + "Id": "CEK-03", + "Description": "Provide cryptographic protection to data at-rest and in-transit, using cryptographic libraries certified to approved standards.", + "Name": "Data Encryption", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-03", + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.6", + "3.1", + "3.11", + "11.3", + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.1", + "27001: A.18.1.2", + "27001: A.18.1.3", + "27001: A.18.1.4", + "27001: A.18.1.5", + "27001: A.10.1", + "27002: 10.1", + "27001: A.13.2.1", + "27002: 13.2.1", + "27001: A.18", + "27002: 18", + "27001: A.14.1.2", + "27002: 14.1.2", + "27001: A.14.1.3", + "27002 14.1.3 c)", + "27001 - A.10.1.1", + "27017 - 10.1.1", + "27001 - A.10.1.2", + "27017 - 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.8.24", + "27002: 8.24 Other Information (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-19", + "AC-19(5)", + "SC-8", + "SC-8(1)", + "SC-8(3)", + "SC-8(4)", + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-28", + "SC-28(1)-(3)", + "SI-4", + "SI-4(10)", + "SI-7", + "SI-7(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "Requirement 3", + "2.2.3", + "2.3", + "3.4", + "3.5.3", + "4.1", + "8.2.1", + "PCI Glossary - Strong Cryptography" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.7", + "3.5.1", + "4.2.1", + "4.2.1.2", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "ec2_ebs_volume_encryption", + "ec2_ebs_default_encryption", + "ec2_ebs_snapshots_encrypted", + "s3_bucket_default_encryption", + "s3_bucket_kms_encryption", + "s3_bucket_secure_transport_policy", + "rds_instance_storage_encrypted", + "rds_cluster_storage_encrypted", + "rds_instance_transport_encrypted", + "rds_snapshots_encrypted", + "efs_encryption_at_rest_enabled", + "dynamodb_tables_kms_cmk_encryption_enabled", + "dynamodb_accelerator_cluster_encryption_enabled", + "dynamodb_accelerator_cluster_in_transit_encryption_enabled", + "kinesis_stream_encrypted_at_rest", + "firehose_stream_encrypted_at_rest", + "sns_topics_kms_encryption_at_rest_enabled", + "sqs_queues_server_side_encryption_enabled", + "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", + "opensearch_service_domains_https_communications_enforced", + "redshift_cluster_encrypted_at_rest", + "redshift_cluster_in_transit_encryption_enabled", + "documentdb_cluster_storage_encrypted", + "neptune_cluster_storage_encrypted", + "neptune_cluster_snapshot_encrypted", + "elasticache_redis_cluster_rest_encryption_enabled", + "elasticache_redis_cluster_in_transit_encryption_enabled", + "kafka_cluster_in_transit_encryption_enabled", + "kafka_cluster_encryption_at_rest_uses_cmk", + "kafka_connector_in_transit_encryption_enabled", + "dms_endpoint_ssl_enabled", + "dms_endpoint_redis_in_transit_encryption_enabled", + "elb_ssl_listeners", + "elbv2_ssl_listeners", + "elbv2_insecure_ssl_ciphers", + "elbv2_nlb_tls_termination_enabled", + "cloudfront_distributions_https_enabled", + "cloudfront_distributions_origin_traffic_encrypted", + "cloudfront_distributions_custom_ssl_certificate", + "transfer_server_in_transit_encryption_enabled", + "sagemaker_notebook_instance_encryption_enabled", + "sagemaker_training_jobs_volume_and_output_encryption_enabled", + "workspaces_volume_encryption_enabled", + "storagegateway_fileshare_encryption_enabled", + "backup_vaults_encrypted", + "backup_recovery_point_encrypted", + "athena_workgroup_encryption", + "glue_data_catalogs_connection_passwords_encryption_enabled", + "glue_data_catalogs_metadata_encryption_enabled", + "glue_etl_jobs_amazon_s3_encryption_enabled", + "glue_etl_jobs_cloudwatch_logs_encryption_enabled", + "glue_etl_jobs_job_bookmark_encryption_enabled", + "glue_development_endpoints_s3_encryption_enabled", + "glue_development_endpoints_cloudwatch_logs_encryption_enabled", + "glue_development_endpoints_job_bookmark_encryption_enabled", + "glue_ml_transform_encrypted_at_rest", + "bedrock_model_invocation_logs_encryption_enabled", + "codebuild_project_s3_logs_encrypted", + "codebuild_report_group_export_encrypted" + ] + }, + { + "Id": "CEK-04", + "Description": "Use encryption algorithms that are appropriate for data protection, considering the classification of data, associated risks, and usability of the encryption technology.", + "Name": "Encryption Algorithm", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.2", + "27001: 6.1.3", + "27001: A.8.2", + "27002: 8.2", + "27001: A.8.3", + "27001: A.10.1.1", + "27002: 10.1.1 (b)", + "27001: A.10.1.2", + "27002: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.2", + "27001: 6.1.3", + "27001: A.8.24", + "27001: A.5.12", + "27001: A.5.13", + "27002: 8.24 General (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2", + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02", + "ID.AM-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A2", + "Requirement 3", + "2.3", + "2.2.3", + "3.4", + "3.5.3", + "4.1", + "8.2.1", + "PCI Glossary - Strong Cryptography" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.7", + "3.5.1", + "4.2.1", + "4.2.1.2", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "acm_certificates_with_secure_key_algorithms", + "elb_insecure_ssl_ciphers", + "elbv2_insecure_ssl_ciphers", + "cloudfront_distributions_using_deprecated_ssl_protocols" + ] + }, + { + "Id": "CEK-08", + "Description": "CSPs must provide the capability for CSCs to manage their own data encryption keys.", + "Name": "CSC Key Management Capability", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2", + "SC2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1", + "27017: 10.1", + "27001: A.10.1.1", + "27017: 10.1.1", + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.23", + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-9", + "CP-9(8)", + "SA-9", + "SA-9(6)", + "SC-12", + "SC-12(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.SC-3", + "ID.AM-6", + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.SC-05" + ] + } + ] + } + ], + "Checks": [ + "kms_cmk_are_used", + "s3_bucket_kms_encryption", + "dynamodb_tables_kms_cmk_encryption_enabled", + "kms_key_not_publicly_accessible", + "kms_cmk_not_multi_region" + ] + }, + { + "Id": "CEK-10", + "Description": "Generate Cryptographic keys using industry accepted cryptographic libraries specifying the algorithm strength and the random number generator used.", + "Name": "Key Generation", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2", + "TS2.3", + "SY1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27002: 10.1.1 (e)", + "27017: 10.1.1", + "27001: A.10.1.2", + "27002: 10.1.2", + "27002: 10.1.2 (a)", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24", + "27002: 8.24 (d), Key management (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.2.3", + "3.6.1", + "PCI Glossary - Cryptographic Key Generation" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1", + "3.6.1.1", + "3.7.1" + ] + } + ] + } + ], + "Checks": [ + "kms_cmk_are_used" + ] + }, + { + "Id": "CEK-12", + "Description": "Rotate cryptographic keys in accordance with the calculated cryptoperiod, which includes provisions for considering the risk of information disclosure and legal and regulatory requirements.", + "Name": "Key Rotation", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27017: 10.1.1", + "27001: A.10.1.2", + "27002: 10.1.2 e)", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.31", + "27001: A.8.24", + "27002: 5.31 Cryptography", + "27002: 8.24 Key management (e,m)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "ID.GV-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05", + "GV.OC-03" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.7.4", + "3.7.5" + ] + } + ] + } + ], + "Checks": [ + "kms_cmk_rotation_enabled", + "iam_rotate_access_key_90_days", + "secretsmanager_automatic_rotation_enabled", + "secretsmanager_secret_rotated_periodically" + ] + }, + { + "Id": "CEK-14", + "Description": "Define, implement and evaluate processes, procedures and technical measures to destroy keys stored outside a secure environment and revoke keys stored in Hardware Security Modules (HSMs) when they are no longer needed, which include provisions for legal and regulatory requirements.", + "Name": "Key Destruction", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27017: 10.1.1", + "27017: 10.1.2", + "27001: A.10.1.2", + "27002: 10.1.2 (j)", + "27001: A.18.1.3", + "27002: 18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.31", + "27001: A.8.24", + "27002: 5.31 Cryptography", + "27002: 8.24 Key management (j,m)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.IP-6", + "ID.GV-3", + "PR.DS-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05", + "ID.AM-08", + "GV.OC-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.6.4", + "3.6.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.7.4", + "3.7.5" + ] + } + ] + } + ], + "Checks": [ + "kms_cmk_not_deleted_unintentionally" + ] + }, + { + "Id": "DCS-06", + "Description": "Catalogue and track all relevant physical and logical assets located at all of the CSP's sites within a secured system.", + "Name": "Assets Cataloguing and Tracking", + "Attributes": [ + { + "Section": "Datacenter Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DCS - 01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "1.1", + "2.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SM2.6" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.1.1", + "27002: 8.1.1", + "27017: 8.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.9" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-8", + "CM-8(1)", + "CM-8(2)", + "CM-8(4)", + "CM-8(7)", + "CM-8(8)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-1", + "ID.AM-2", + "ID.AM-4", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-01", + "ID.AM-02", + "ID.AM-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.4", + "9.7.1", + "9.9.1", + "9.9.1.a", + "9.9.1.b", + "9.9.1.c", + "12.3.3", + "12.3.4" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1.1", + "6.3.2", + "9.4.2", + "9.4.3", + "12.5.1" + ] + } + ] + } + ], + "Checks": [ + "config_recorder_all_regions_enabled", + "resourceexplorer2_indexes_found" + ] + }, + { + "Id": "DSP-02", + "Description": "Apply industry accepted methods for the secure disposal of data from storage media such that data is not recoverable by any forensic means.", + "Name": "Secure Disposal", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3", + "CC6.4", + "CC6.5", + "CC6.7", + "P4.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DSI-07" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.3.3", + "7.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.3.2", + "27002: 8.3.2", + "27001: A.11.2.7", + "27002: 11.2.7" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.7.10", + "27001: A.7.14", + "27001: A.8.10", + "27002: 7.10 (Secure reuse or disposal)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-22", + "SI-12", + "SI-12(3)", + "SI-18", + "SI-18(1)", + "SI-18(4)", + "SI-18(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.SC-10", + "PR.PS-02", + "PR.PS-03", + "ID.AM-08" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.1", + "9.8", + "9.8.1", + "9.8.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1", + "3.7.5", + "9.4.7" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_lifecycle_enabled", + "dlm_ebs_snapshot_lifecycle_policy_exists", + "ecr_repositories_lifecycle_policy_enabled" + ] + }, + { + "Id": "DSP-03", + "Description": "Create and maintain a data inventory, at least for any sensitive data and personal data.", + "Name": "Data Inventory", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1", + "1.3.2", + "1.3.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.1.1", + "27002: 8.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.9", + "27001: A.8.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-12", + "CM-12(1)", + "PM-5", + "PM-5(1)", + "SI-12", + "SI-12(1)", + "SI-19", + "SI-19(1)", + "SI-19(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1", + "9.4.5" + ] + } + ] + } + ], + "Checks": [ + "macie_is_enabled", + "macie_automated_sensitive_data_discovery_enabled", + "config_recorder_all_regions_enabled" + ] + }, + { + "Id": "DSP-04", + "Description": "Classify data according to its type and sensitivity level.", + "Name": "Data Classification", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "C1.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DSI-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.7" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1", + "1.3.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.2.1", + "27002: 8.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-16", + "AC-16(9)", + "PM-22", + "PM-23", + "PT-2", + "PT-2(1)", + "SI-18", + "SI-18(2)", + "SI-19", + "SI-19(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-05", + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "9.6.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "9.4.2", + "9.4.3" + ] + } + ] + } + ], + "Checks": [ + "macie_is_enabled", + "macie_automated_sensitive_data_discovery_enabled" + ] + }, + { + "Id": "DSP-07", + "Description": "Develop systems, products, and business practices based upon a principle of security by design and industry best practices.", + "Name": "Data Protection by Design and Default", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "PI1.2", + "PI1.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.3.1", + "5.3.2", + "5.3.3", + "5.3.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD2.2", + "IM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.1.1", + "27002:14.1.1", + "27001: A.14.2.5", + "27002:14.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.27", + "27001: A.8.28", + "27001: A.8.29", + "27002: 5.8 (Information security requirements a-i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-17", + "PM-24", + "PM-25", + "PT-2", + "PT-2(2)", + "SA-3", + "SA-4", + "SA-5", + "SA-8", + "SA-8(9)", + "SA-8(13)", + "SA-8(18)", + "SA-8(20)", + "SA-8(22)", + "SA-8(23)", + "SA-8(33)", + "SA-15", + "SA-15(12)", + "SC-3", + "SC-3(3)", + "SC-7", + "SC-7(24)", + "SC-8", + "SC-8(1)-(4)", + "SC-28", + "SC-28(1)", + "SI-12", + "SI-12(1)-(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.PT-3", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.1" + ] + } + ] + } + ], + "Checks": [ + "ec2_ebs_default_encryption", + "s3_account_level_public_access_blocks", + "s3_bucket_level_public_access_block", + "ec2_ebs_snapshot_account_block_public_access", + "rds_instance_no_public_access", + "rds_snapshots_public_access" + ] + }, + { + "Id": "DSP-10", + "Description": "Define, implement and evaluate processes, procedures and technical measures that ensure any transfer of personal or sensitive data is protected from unauthorized access and only processed within scope as permitted by the respective laws and regulations.", + "Name": "Sensitive Data Transfer", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-02", + "EKM-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.1", + "3.12", + "3.13" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "9.5.1", + "9.5.2", + "9.5.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.4", + "IM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.13.2.1", + "27002: 13.2.1", + "27001: A.8.3.3", + "27002: 8.3.3", + "27001: A.13.2.3", + "27002: 13.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.7.10" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-4", + "AC-4(23)-(25)", + "CA-3", + "CA-3(6)", + "CA-6", + "CA-6(1)", + "CA-6(2)", + "SC-4", + "SC-4(2)", + "SC-7", + "SC-7(10)", + "SC-7(24)", + "SC-8", + "SC-8(1)-(5)", + "SC-16", + "SC-16(1)-(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2", + "PR.DS-5", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02", + "PR.IR-01", + "ID.AM-03", + "GV.OC-03", + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "4.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "4.1.1", + "4.2.1", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_secure_transport_policy", + "opensearch_service_domains_https_communications_enforced", + "redshift_cluster_in_transit_encryption_enabled", + "rds_instance_transport_encrypted", + "transfer_server_in_transit_encryption_enabled", + "kafka_cluster_mutual_tls_authentication_enabled" + ] + }, + { + "Id": "DSP-16", + "Description": "Data retention, archiving and deletion is managed in accordance with business requirements, applicable laws and regulations.", + "Name": "Data Retention and Deletion", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "C1.1", + "C1.2", + "CC3.1", + "P4.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-02", + "BCR-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.4", + "3.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.3.1", + "7.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.33", + "27001: A.8.10", + "27002: 5.33 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SI-12", + "SI-12(1)-(3)", + "SI-18", + "SI-18(1)", + "SI-18(4)", + "SI-18(5)", + "SI-19", + "SI-19(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-3", + "PR.IP-6", + "ID.GV-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "GV.OC-03", + "GV.SC-10", + "PR.DS-11" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_lifecycle_enabled", + "cloudwatch_log_group_retention_policy_specific_days_enabled", + "kinesis_stream_data_retention_period", + "ecr_repositories_lifecycle_policy_enabled" + ] + }, + { + "Id": "DSP-17", + "Description": "Define and implement, processes, procedures and technical measures to protect sensitive data throughout it's lifecycle.", + "Name": "Sensitive Data Protection", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSC-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.1", + "CC6.1", + "CC6.3", + "CC6.7", + "CC8.1", + "C1.1", + "P2.0", + "P3.0", + "P4.0", + "P5.0", + "P6.0" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.1", + "3.1", + "3.14" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.3.3", + "9.1.1", + "9.2.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3", + "27002: 18.1.3", + "27001:A.18.1.4", + "27002:18.1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.11", + "27001: A.8.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-2", + "PM-22", + "PM-24", + "PT-7", + "PT-7(1)", + "PT-7(2)", + "PT-8", + "SC-8", + "SC-8(1)-(5)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2", + "PR.DS-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02", + "PR.DS-10" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.0 (including all subsections)", + "4.0 (including all subsections)" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.1.1", + "4.1.1" + ] + } + ] + } + ], + "Checks": [ + "s3_account_level_public_access_blocks", + "s3_bucket_public_access", + "s3_bucket_policy_public_write_access", + "ec2_ebs_public_snapshot", + "rds_snapshots_public_access", + "rds_instance_no_public_access", + "ec2_ebs_volume_encryption", + "s3_bucket_default_encryption", + "rds_instance_storage_encrypted", + "secretsmanager_not_publicly_accessible", + "macie_is_enabled" + ] + }, + { + "Id": "GRC-05", + "Description": "Develop and implement an Information Security Program, which includes programs for all the relevant domains of the CCM.", + "Name": "Information Security Program", + "Attributes": [ + { + "Section": "Governance, Risk and Compliance", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "14.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SG2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 4.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-1", + "PM-3", + "PM-14", + "PL-2", + "PM-18", + "PM-31" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.4.1", + "A.3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.4.1", + "A3.1.1" + ] + } + ] + } + ], + "Checks": [ + "securityhub_enabled", + "guardduty_is_enabled" + ] + }, + { + "Id": "IAM-02", + "Description": "Establish, document, approve, communicate, implement, apply, evaluate and maintain strong password policies and procedures. Review and update the policies and procedures at least annually.", + "Name": "Strong Password Policy and Procedures", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-12", + "GRM-06", + "GRM-09" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.1.1", + "1.5.1", + "4.1.2", + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1", + "SA1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5", + "27002: 5", + "27001: A.9.4.3", + "27002: 9.4.3", + "27017: 9.4.3", + "27018: 9.4.3", + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27001: A.7.2.2", + "27002: 7.2.2", + "27001: A.9.2.6", + "27002: 9.2.6", + "27001: A.9.2.3", + "27002: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5.1", + "27001: A.5.4", + "27001: A.5.17", + "27001: A.6.3", + "27001: A.8.5", + "27001: A.5.37" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(3)", + "AC-2(11)", + "AC-3", + "AC-3(3)", + "AC-12", + "AC-12(1)", + "IA-2", + "IA-2(10)", + "IA-5", + "IA-5(1)", + "IA-5(18)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "PR.AC-1", + "PR.AC-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.PO-01", + "GV.PO-02", + "ID.IM-03", + "PR.AA-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.4", + "12.1", + "12.1.1", + "12.11" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.1.1", + "8.3.8" + ] + } + ] + } + ], + "Checks": [ + "iam_password_policy_minimum_length_14", + "iam_password_policy_lowercase", + "iam_password_policy_uppercase", + "iam_password_policy_number", + "iam_password_policy_symbol", + "iam_password_policy_reuse_24", + "iam_password_policy_expires_passwords_within_90_days_or_less", + "cognito_user_pool_password_policy_minimum_length_14", + "cognito_user_pool_password_policy_lowercase", + "cognito_user_pool_password_policy_uppercase", + "cognito_user_pool_password_policy_number", + "cognito_user_pool_password_policy_symbol" + ] + }, + { + "Id": "IAM-03", + "Description": "Manage, store, and review the information of system identities, and level of access.", + "Name": "Identity Inventory", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-04", + "IAM-08", + "IAM-10" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1", + "5.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.2 (c)", + "27001: A.8.1.1", + "27002: 8.1.1", + "27001: A.9.4.1", + "27002: 9.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.2 (c)", + "27001: A.5.15", + "27001: A.5.16", + "27001: A.5.18", + "27001: A.7.4", + "27001: A.8.15", + "27001: A.8.2", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-10", + "AU-10(1)", + "AU-10(2)", + "AU-16", + "AU-16(1)", + "IA-4", + "IA-4(8)", + "IA-4(9)", + "IA-5", + "IA-5(5)", + "IA-8", + "IA-8(4)", + "PM-5(1)", + "SA-8", + "SA-8(22)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-04", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.4.a" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.5", + "7.2.5.1" + ] + } + ] + } + ], + "Checks": [ + "iam_user_accesskey_unused", + "iam_user_console_access_unused", + "iam_user_two_active_access_key" + ] + }, + { + "Id": "IAM-04", + "Description": "Employ the separation of duties principle when implementing information system access.", + "Name": "Separation of Duties", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC1.3", + "CC5.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.2.2", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.6.1.2", + "27002: 6.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.18", + "27001: A.5.3", + "27001: A.8.2" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(3)", + "AC-2(11)", + "AC-6", + "AC-6(1)-(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4", + "6.4.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.5.3", + "6.5.4", + "7.2.1", + "7.2.2" + ] + } + ] + } + ], + "Checks": [ + "iam_policy_attached_only_to_group_or_roles", + "iam_securityaudit_role_created", + "iam_support_role_created" + ] + }, + { + "Id": "IAM-05", + "Description": "Employ the least privilege principle when implementing information system access.", + "Name": "Least Privilege", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-06", + "IVS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.1.1", + "27002: 9.1.1", + "27001: A.9.1.2", + "27002: 9.1.2", + "27001: A.9.2.3", + "27002: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.8.2", + "27002: 5.15 (Other information 2nd (a))" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(4)", + "IA-12", + "IA-12(2)", + "IA-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "7.1", + "7.1.1", + "7.1.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "7.2.2", + "7.2.5", + "7.2.6" + ] + } + ] + } + ], + "Checks": [ + "iam_aws_attached_policy_no_administrative_privileges", + "iam_customer_attached_policy_no_administrative_privileges", + "iam_inline_policy_no_administrative_privileges", + "iam_customer_unattached_policy_no_administrative_privileges", + "iam_policy_allows_privilege_escalation", + "iam_inline_policy_allows_privilege_escalation", + "iam_no_custom_policy_permissive_role_assumption", + "iam_role_administratoraccess_policy", + "iam_user_administrator_access_policy", + "iam_group_administrator_access_policy", + "iam_administrator_access_with_mfa" + ] + }, + { + "Id": "IAM-07", + "Description": "De-provision or respectively modify access of movers / leavers or system identity changes in a timely manner in order to effectively adopt and communicate identity and access management policies.", + "Name": "User Access Changes and Revocation", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.3", + "6.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.18" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(1)", + "AC-2(2)", + "AC-2(6)", + "AC-2(8)", + "AC-3", + "AC-3(8)", + "AC-6", + "AC-6(7)", + "AU-10", + "AU-10(4)", + "AU-16", + "AU-16(1)", + "CM-7", + "CM-7(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4", + "PR.IP-11" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.RR-04", + "GV.SC-10", + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1.2", + "8.1.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.2.5", + "8.2.6" + ] + } + ] + } + ], + "Checks": [ + "iam_user_accesskey_unused", + "iam_user_console_access_unused", + "iam_user_no_setup_initial_access_key" + ] + }, + { + "Id": "IAM-08", + "Description": "Review and revalidate user access for least privilege and separation of duties with a frequency that is commensurate with organizational risk tolerance.", + "Name": "User Access Review", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-10" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.5", + "27001: A.9.2.6", + "27001: A.9.4.1", + "27017: 9.4.1", + "27001: A.6.1.2", + "27001: A 9.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.3", + "27001: A.5.18", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(4)", + "AC-6(8)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.5.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.5.1", + "7.2.5", + "7.2.4" + ] + } + ] + } + ], + "Checks": [ + "iam_user_accesskey_unused", + "iam_user_console_access_unused", + "iam_rotate_access_key_90_days", + "secretsmanager_secret_unused" + ] + }, + { + "Id": "IAM-09", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the segregation of privileged access roles such that administrative access to data, encryption and key management capabilities and logging capabilities are distinct and separated.", + "Name": "Segregation of Privileged Access Roles", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.1", + "CC6.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.3", + "27002: 9.2.3", + "27017: 9.2.3", + "27018: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.2", + "27001: A.8.18", + "27002: 8.2 (j)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-3(7)", + "AC-6(4)", + "AC-6(8)", + "IA-5", + "IA-5(6)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.3", + "3.5.2", + "7.1.2", + "7.1.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1", + "3.7.6", + "6.5.3", + "6.5.4", + "7.2.1", + "7.2.2", + "10.3.1" + ] + } + ] + } + ], + "Checks": [ + "iam_policy_attached_only_to_group_or_roles", + "iam_role_administratoraccess_policy", + "iam_avoid_root_usage", + "iam_no_root_access_key" + ] + }, + { + "Id": "IAM-10", + "Description": "Define and implement an access process to ensure privileged access roles and rights are granted for a time limited period, and implement procedures to prevent the culmination of segregated privileged access.", + "Name": "Management of Privileged Access Roles", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1", + "6.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.3", + "27002: 9.2.3", + "27017: 9.2.3", + "27018: 9.2.3", + "27001: A.9.4.4", + "27002: 9.4.4", + "27017: 9.4.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.2", + "27001: A.8.18", + "27002: 8.2 (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(7)", + "AC-3", + "AC-3(4)", + "AC-3(11)", + "AC-3(13)", + "AC-3(14)", + "AC-6", + "AC-6(4)", + "AC-6(5)", + "AC-6(8)", + "AC-12", + "AC-12(3)", + "AC-17", + "AC-17(4)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "7.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "7.2.2" + ] + } + ] + } + ], + "Checks": [ + "iam_avoid_root_usage", + "iam_no_root_access_key", + "iam_role_cross_account_readonlyaccess_policy", + "iam_role_cross_service_confused_deputy_prevention", + "iam_inline_policy_allows_privilege_escalation", + "iam_policy_allows_privilege_escalation" + ] + }, + { + "Id": "IAM-12", + "Description": "Define, implement and evaluate processes, procedures and technical measures to ensure the logging infrastructure is read-only for all with write access, including privileged access roles, and that the ability to disable it is controlled through a procedure that ensures the segregation of duties and break glass procedures.", + "Name": "Safeguard Logs Integrity", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.3" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1", + "27018: 12.4.1", + "27001: A.12.4.2", + "27002: 12.4.2", + "27017: 12.4.2", + "27018: 12.4.2", + "27001: A.12.4.3", + "27002: 12.4.3", + "27017: 12.4.3", + "27018: 12.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15", + "27001: A.8.18", + "27002: 8.15 Protection of Logs" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(11)", + "AC-2(12)", + "IA-8", + "IA-8(4)", + "SA-8", + "SA-8(22)", + "SC-34", + "SC-34(1)", + "SC-34(2)", + "SC-36", + "SI-4", + "SI-4(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_log_file_validation_enabled", + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", + "cloudtrail_logs_s3_bucket_access_logging_enabled", + "cloudtrail_kms_encryption_enabled", + "cloudtrail_bucket_requires_mfa_delete" + ] + }, + { + "Id": "IAM-13", + "Description": "Define, implement and evaluate processes, procedures and technical measures that ensure users are identifiable through unique IDs or which can associate individuals to the usage of user IDs.", + "Name": "Uniquely Identifiable Users", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.1", + "27002: 9.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.16" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-3", + "AC-3(14)", + "AC-24", + "AC-24(2)", + "AU-10", + "AU-10(1)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(12)", + "IA-4", + "IA-4(1)", + "SA-8", + "SA-8(22)", + "SC-23", + "SC-23(3)", + "SC-40(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1", + "8.2", + "8.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.2.1", + "8.2.2", + "8.2.4" + ] + } + ] + } + ], + "Checks": [ + "iam_user_mfa_enabled_console_access", + "iam_check_saml_providers_sts" + ] + }, + { + "Id": "IAM-14", + "Description": "Define, implement and evaluate processes, procedures and technical measures for authenticating access to systems, application and data assets, including multifactor authentication for at least privileged user and sensitive data access. Adopt digital certificates or alternatives which achieve an equivalent level of security for system identities.", + "Name": "Strong Authentication", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.3", + "6.5", + "12.5", + "12.7" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3", + "SA1.4", + "SA1.8" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.1.2", + "27002: 9.1.2", + "27017: 9.1.2", + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27001: A.9.4.2", + "27002: 9.4.2", + "27017: 9.4.2", + "27018: 9.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.17", + "27001: A.8.5", + "27001: A.8.24", + "27002: 8.5", + "27002: 8.24 other information (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(5)", + "AC-7", + "AC-7(4)", + "AU-10", + "AU-10(2)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(8)", + "IA-2(12)", + "IA-3", + "IA-3(1)", + "IA-5", + "IA-5(2)", + "IA-5(7)", + "IA-5(9)", + "IA-5(10)", + "IA-5(12)", + "IA-5(14)-(16)", + "IA-8", + "IA-8(1)", + "IA-8(6)", + "SC-23", + "SC-23(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6", + "PR.AC-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1.2", + "8.1.3", + "8.1.6", + "8.2", + "8.3", + "8.3.2", + "12.3.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "8.3.1", + "8.3.2", + "8.4.1", + "8.4.2", + "8.4.3" + ] + } + ] + } + ], + "Checks": [ + "iam_root_mfa_enabled", + "iam_root_hardware_mfa_enabled", + "iam_user_mfa_enabled_console_access", + "iam_user_hardware_mfa_enabled", + "cognito_user_pool_mfa_enabled" + ] + }, + { + "Id": "IAM-15", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the secure management of passwords.", + "Name": "Passwords Management", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27018: 9.2.4", + "27001: A.9.3.1", + "27002: 9.3.1", + "27017: 9.3.1", + "27018: 9.3.1", + "27001: A.9.4.3", + "27002: 9.4.3", + "27017: 9.4.3", + "27018: 9.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.17" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IA-4", + "IA-4(8)", + "IA-5", + "IA-5(1)", + "IA-5(8)", + "IA-5(18)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.2", + "8.2.1-6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.2", + "2.3.1", + "8.3.5", + "8.3.6", + "8.3.7", + "8.3.8", + "8.3.9", + "8.3.10", + "8.3.10.1", + "8.6.2" + ] + } + ] + } + ], + "Checks": [ + "iam_password_policy_minimum_length_14", + "iam_password_policy_reuse_24", + "iam_password_policy_expires_passwords_within_90_days_or_less", + "cognito_user_pool_password_policy_minimum_length_14", + "cognito_user_pool_temporary_password_expiration" + ] + }, + { + "Id": "IAM-16", + "Description": "Define, implement and evaluate processes, procedures and technical measures to verify access to data and system functions is authorized.", + "Name": "Authorization Mechanisms", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3", + "SA1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.5", + "27002: 9.2.5", + "27017: 9.2.5", + "27018: 9.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.18" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-3", + "AC-3(5)", + "AC-4", + "AC-4(17)", + "AC-4(21)", + "AC-4(22)", + "AC-6", + "AC-6(8)", + "AC-6(9)", + "AC-12", + "AC-12(1)", + "AC-20", + "AC-20(1)", + "AU-10", + "AU-10(1)", + "AU-10(2)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(12)", + "IA-3", + "IA-3(1)", + "IA-5(1)", + "IA-5(2)", + "IA-5(5)", + "IA-5(8)", + "IA-5(10)", + "IA-5(12)", + "IA-8", + "IA-8(1)", + "IA-8(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4", + "PR.AC-6", + "PR.AC-7", + "PR.PT-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-03", + "PR.AA-04", + "PR.AA-05", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.3", + "7.1.4" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.4", + "7.2.3", + "7.2.5.1" + ] + } + ] + } + ], + "Checks": [ + "iam_aws_attached_policy_no_administrative_privileges", + "iam_customer_attached_policy_no_administrative_privileges", + "iam_inline_policy_no_administrative_privileges", + "apigateway_restapi_authorizers_enabled", + "apigatewayv2_api_authorizers_enabled", + "awslambda_function_not_publicly_accessible", + "awslambda_function_url_public", + "cognito_user_pool_waf_acl_attached" + ] + }, + { + "Id": "IPY-03", + "Description": "Implement cryptographically secure and standardized network protocols for the management, import and export of data.", + "Name": "Secure Interoperability and Portability Management", + "Attributes": [ + { + "Section": "Interoperability & Portability", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IPY-04" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY1.1", + "SY1.2", + "NC1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1", + "27001: A.15.1.1", + "27002: 15.1.1", + "27017: 15.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.19", + "27001: A.5.23", + "27001: A.5.31", + "27001: A.5.32", + "27001: A.5.33", + "27001: A.5.34" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PT-2", + "PT-2(2)", + "SA-4", + "SC-16", + "SC-16(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.2.1", + "1.2.5", + "1.2.6", + "2.2.4", + "2.2.5", + "2.2.7", + "4.2.1" + ] + } + ] + } + ], + "Checks": [ + "s3_bucket_secure_transport_policy" + ] + }, + { + "Id": "IVS-02", + "Description": "Plan and monitor the availability, quality, and adequate capacity of resources in order to deliver the required system performance as determined by the business.", + "Name": "Capacity and Resource Planning", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "No", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-04" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.3", + "27001: 6.1", + "27001: 9.1", + "27001: A.12.1.3", + "27002: 12.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.3 (b)", + "27001: 6.1", + "27001: 9.1", + "27001: A.8.6", + "27001: A.8.14" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2", + "CP-2(2)", + "SC-5", + "SC-5(2)", + "SC-4", + "SI-4" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-4", + "ID.BE-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.IR-04", + "GV.OC-04" + ] + } + ] + } + ], + "Checks": [ + "autoscaling_group_multiple_az", + "autoscaling_group_multiple_instance_types" + ] + }, + { + "Id": "IVS-03", + "Description": "Monitor, encrypt and restrict communications between environments to only authenticated and authorized connections, as justified by the business. Review these configurations at least annually, and support them by a documented justification of all allowed services, protocols, ports, and compensating controls.", + "Name": "Network Security", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-06" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.8", + "3.1", + "12.2", + "13.6", + "13.9" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "NC1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.13.1.1", + "27002: 13.1.1", + "27001: A.13.1.2", + "27002: 13.1.2", + "27001: A.13.1.3", + "27002: 13.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.5.15", + "27001: A.5.37", + "27001: A.8.5", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.21", + "27001: A.8.22", + "27001: A.8.24", + "27002: A.5.15 2nd c)", + "27002: 8.20", + "27002: 8.21", + "27002: 8.22", + "27002: 8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-1", + "SC-4", + "SC-7", + "SC-7(4)", + "SC-7(5)", + "SC-7(8)", + "SC-7(9)", + "SC-7(11)", + "SC-8", + "SC-8(1)", + "SC-11", + "SC-12", + "SC-16", + "SC-23", + "SC-29", + "SC-29(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-5", + "PR.AC-7", + "PR.PT-4", + "DE.CM-1", + "DE.CM-7", + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.IR-01", + "PR.AA-03", + "PR.AA-05", + "DE.CM-01", + "PR.DS-02", + "ID.AM-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "1.1.6", + "1.2", + "1.2.3", + "2.2", + "4.1.1", + "10.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.2.5", + "1.2.6", + "1.2.7", + "1.4.2", + "2.2.4", + "2.2.5", + "2.2.7", + "4.2.1", + "10.1.1" + ] + } + ] + } + ], + "Checks": [ + "vpc_flow_logs_enabled", + "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_allow_ingress_from_internet_to_tcp_port_22", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", + "ec2_networkacl_allow_ingress_any_port", + "ec2_networkacl_allow_ingress_tcp_port_22", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "vpc_peering_routing_tables_with_least_privilege", + "vpc_subnet_no_public_ip_by_default" + ] + }, + { + "Id": "IVS-04", + "Description": "Harden host and guest OS, hypervisor or infrastructure control plane according to their respective best practices, and supported by technical controls, as part of a security baseline.", + "Name": "OS Hardening and Base Controls", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.8", + "CC7.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-07", + "IVS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "4.1", + "4.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3", + "5.2.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY1.1", + "SY1.3", + "SY1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.14.2.2", + "27002: 14.2.2", + "27001: A.14.2.3", + "27001 A.14.2.4", + "27018: 12.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.5.37", + "27001: A.8.5", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.22", + "27001: A.8.24", + "27002: 8.20", + "27002: 8.22", + "27002: 8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-6", + "CM-6(1)", + "SC-29", + "SC-29(1)", + "SC-2", + "SC-7", + "SC-7(12)", + "SC-30", + "SC-34", + "SC-35", + "SC-39", + "SC-44" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-1", + "PR.PT-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.1" + ] + } + ] + } + ], + "Checks": [ + "ec2_instance_imdsv2_enabled", + "ec2_instance_account_imdsv2_enabled", + "ec2_launch_template_imdsv2_required", + "ec2_instance_managed_by_ssm", + "ssm_managed_compliant_patching" + ] + }, + { + "Id": "IVS-06", + "Description": "Design, develop, deploy and configure applications and infrastructures such that CSP and CSC (tenant) user access and intra-tenant access is appropriately segmented and segregated, monitored and restricted from other tenants.", + "Name": "Segmentation and Segregation", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-09" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.3.4", + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SC2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.1", + "27001: A.13.1.3", + "27002: 13.1.3", + "27017: 13.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.1", + "27001: A.5.15", + "27001: A.5.20", + "27001: A.8.3", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.22", + "27002: 5.15 (b)", + "27002: 8.3 (b)", + "27002: 8.16 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-3", + "SC-7", + "SC-7(20)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.AC-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.IR-01", + "PR.PS-01", + "PR.PS-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.6", + "8.3.1", + "10.8", + "11.3", + "A3.2.1", + "A3.3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "A1.1.1", + "A1.1.2", + "A1.1.3" + ] + } + ] + } + ], + "Checks": [ + "ec2_securitygroup_default_restrict_traffic", + "vpc_subnet_separate_private_public", + "vpc_peering_routing_tables_with_least_privilege", + "ec2_instance_public_ip", + "awslambda_function_inside_vpc", + "sagemaker_notebook_instance_vpc_settings_configured", + "sagemaker_models_vpc_settings_configured", + "sagemaker_training_jobs_vpc_settings_configured" + ] + }, + { + "Id": "IVS-07", + "Description": "Use secure and encrypted communication channels when migrating servers, services, applications, or data to cloud environments. Such channels must include only up-to-date and approved protocols.", + "Name": "Migration to Cloud Environments", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-10" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.4", + "IM1.4", + "NC1.4", + "SC2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.13.1.1", + "27002: 13.1.1", + "27017: 13.1.1", + "27018: 13.1.1", + "27001: A.13.1.2", + "27002: 13.1.2", + "27017: 13.1.2", + "27018: 13.1.2", + "27001: A.13.1.3", + "27002: 13.1.3", + "27017: 13.1.3", + "27018: 13.1.3", + "27001: A.13.2.1", + "27002: 13.2.1", + "27017: 13.2.1", + "27018: 13.2.1", + "27001: A.13.2.2", + "27002: 13.2.2", + "27017: 13.2.2", + "27018: 13.2.2", + "27001: A.13.2.3", + "27002: 13.2.3", + "27017: 13.2.3", + "27018: 13.2.3", + "27001: A.13.2.4", + "27002: 13.2.4", + "27017: 13.2.4", + "27018: 13.2.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.8.20", + "27001: A.8.24", + "27002: 8.20 (e)", + "27002: 8.24 Guidance (b,f), other information (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-17", + "AC-20", + "SC-7", + "SC-7(28)", + "SC-8", + "SC-8(1)", + "SC-12", + "SC-23", + "SC-29", + "SI-7", + "SI-7(1)-(3)", + "SI-7(5)-(10)", + "SI-7(12)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "4.2.1" + ] + } + ] + } + ], + "Checks": [ + "dms_endpoint_ssl_enabled" + ] + }, + { + "Id": "IVS-09", + "Description": "Define, implement and evaluate processes, procedures and defense-in-depth techniques for protection, detection, and timely response to network-based attacks.", + "Name": "Network Defense", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.6", + "CC6.8", + "CC7.1", + "CC7.2", + "CC7.5" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-13" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "13.3", + "13.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.3", + "5.2.4", + "5.2.5", + "5.2.7", + "5.3.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "NC1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1", + "27001: 6.2", + "27001: A.14.1.2", + "27002: 14.1.2", + "27017: 14.1.2", + "27001: A.11.1.4", + "27002: 11.1.4", + "27017: 11.1.4", + "27018: 16.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1", + "27001: 6.2", + "27001: A.5.24", + "27001: A.5.26", + "27001: A.8.8", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.21", + "27001: A.8.22", + "27001: A.8.26", + "27002: 8.8 (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-8", + "PL-8(1)", + "SC-5", + "SC-5(1)", + "SC-5(3)", + "SC-7", + "SC-7(13)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.DP-1", + "DE.CM-1", + "DE.CM-7", + "PR.AC-5", + "RS.MI-2", + "PR.DS-2", + "RS.RP-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-03", + "DE.CM-01", + "PR.IR-01", + "RS.MA-01", + "RS.MI-01", + "RS.MI-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.6", + "1.1", + "1.2", + "1.3", + "1.5", + "12.10.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.1.1", + "1.3.1", + "1.3.2", + "1.3.3", + "1.4.1", + "1.4.2", + "1.4.3", + "1.4.4", + "1.4.5", + "1.5.1", + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "networkfirewall_in_all_vpc", + "networkfirewall_logging_enabled", + "networkfirewall_policy_rule_group_associated", + "wafv2_webacl_with_rules", + "wafv2_webacl_logging_enabled", + "elbv2_waf_acl_attached", + "cloudfront_distributions_using_waf", + "guardduty_is_enabled", + "shield_advanced_protection_in_cloudfront_distributions", + "shield_advanced_protection_in_internet_facing_load_balancers" + ] + }, + { + "Id": "LOG-02", + "Description": "Define, implement and evaluate processes, procedures and technical measures to ensure the security and retention of audit logs.", + "Name": "Audit Logs Protection", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.1", + "8.9", + "8.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "3.1.3", + "5.1.2", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3", + "27002: 18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.28", + "27001: A.5.33", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-4", + "AU-11" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.IP-4", + "PR.IP-6", + "PR.PT-1", + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.DS-01", + "PR.DS-02", + "ID.AM-08", + "PR.DS-11", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5", + "10.7" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4", + "10.5.1" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_log_file_validation_enabled", + "cloudtrail_kms_encryption_enabled", + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", + "cloudtrail_logs_s3_bucket_access_logging_enabled", + "cloudtrail_bucket_requires_mfa_delete", + "cloudwatch_log_group_kms_encryption_enabled", + "cloudwatch_log_group_not_publicly_accessible", + "s3_bucket_object_lock" + ] + }, + { + "Id": "LOG-03", + "Description": "Identify and monitor security-related events within applications and the underlying infrastructure. Define and implement a system to generate alerts to responsible stakeholders based on such events and corresponding metrics.", + "Name": "Security Monitoring and Alerting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-03", + "SEF-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4", + "5.2.7", + "1.6.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2", + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.28", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-5", + "AU-5(2)", + "AU-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.AE-2", + "DE.AE-3", + "DE.AE-5", + "DE.CM-1", + "DE.CM-2", + "DE.CM-3", + "DE.CM-4", + "DE.CM-5", + "DE.CM-6", + "DE.CM-7", + "DE.DP-1", + "DE.DP-4", + "DE.AE-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.AE-02", + "DE.AE-03", + "DE.AE-04", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1", + "10.2.2", + "10.4.1.1", + "10.4.2.1", + "10.4.3" + ] + } + ] + } + ], + "Checks": [ + "guardduty_is_enabled", + "securityhub_enabled", + "cloudwatch_alarm_actions_enabled", + "cloudwatch_alarm_actions_alarm_state_configured", + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_log_metric_filter_root_usage", + "cloudwatch_log_metric_filter_sign_in_without_mfa" + ] + }, + { + "Id": "LOG-04", + "Description": "Restrict audit logs access to authorized personnel and maintain records that provide unique access accountability.", + "Name": "Audit Logs Access and Accountability", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.14" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "3.1.1", + "4.1.2", + "4.1.3", + "4.2.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.2", + "27001: A.12.4.1", + "27002: 12.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.33", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(4)", + "AU-9(6)", + "AU-10" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.1", + "10.2.1", + "10.2.3", + "10.5.1", + "10.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1.3", + "10.3.1" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", + "cloudwatch_log_group_not_publicly_accessible" + ] + }, + { + "Id": "LOG-05", + "Description": "Monitor security audit logs to detect activity outside of typical or expected patterns. Establish and follow a defined process to review and take appropriate and timely actions on detected anomalies.", + "Name": "Audit Logs Monitoring and Response", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.8", + "8.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.1", + "1.6.2", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.3", + "27002: 12.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15", + "27001: A.8.16" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-6", + "AU-6(1)", + "AU-6(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-3", + "PR.PT-1", + "RS.AN-1", + "RS.CO-1.", + "DE.AE-1", + "DE.AE-5", + "DE.DP-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-03", + "PR.PS-04", + "DE.AE-02", + "DE.AE-03", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.6", + "10.6.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.4.1.1", + "10.4.2.1" + ] + } + ] + } + ], + "Checks": [ + "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_policy_changes", + "cloudwatch_log_metric_filter_security_group_changes", + "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_authentication_failures", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "guardduty_no_high_severity_findings" + ] + }, + { + "Id": "LOG-07", + "Description": "Establish, document and implement which information meta/data system events should be logged. Review and update the scope at least annually or whenever there is a change in the threat environment.", + "Name": "Logging Scope", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5.3", + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5.3", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-1", + "AU-14", + "AU-16" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.SC-3", + "ID.SC-4", + "PR.PT-1", + "ID.GV-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1", + "10.2.2" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_multi_region_enabled", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_s3_dataevents_read_enabled", + "cloudtrail_s3_dataevents_write_enabled", + "vpc_flow_logs_enabled", + "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled" + ] + }, + { + "Id": "LOG-08", + "Description": "Generate audit records containing relevant security information.", + "Name": "Log Records", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-3", + "AU-3(1)", + "AU-3(3)", + "AU-6", + "AU-6(8)", + "AU-12", + "AU-12(1)", + "AU-12(2)", + "AU-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.PT-1", + "DE.AE-3", + "DE.CM-1", + "DE.CM-2", + "DE.CM-3", + "DE.CM-6", + "DE.CM-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.2" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_multi_region_enabled", + "cloudtrail_cloudwatch_logging_enabled", + "vpc_flow_logs_enabled", + "s3_bucket_server_access_logging_enabled", + "elb_logging_enabled", + "elbv2_logging_enabled", + "cloudfront_distributions_logging_enabled", + "route53_public_hosted_zones_cloudwatch_logging_enabled", + "wafv2_webacl_logging_enabled", + "redshift_cluster_audit_logging", + "rds_cluster_integration_cloudwatch_logs", + "rds_instance_integration_cloudwatch_logs", + "opensearch_service_domains_audit_logging_enabled", + "eks_control_plane_logging_all_types_enabled", + "apigateway_restapi_logging_enabled", + "apigatewayv2_api_access_logging_enabled", + "networkfirewall_logging_enabled", + "mq_broker_logging_enabled", + "documentdb_cluster_cloudwatch_log_export", + "neptune_cluster_integration_cloudwatch_logs", + "codebuild_project_logging_enabled", + "glue_etl_jobs_logging_enabled", + "stepfunctions_statemachine_logging_enabled", + "datasync_task_logging_enabled", + "ec2_client_vpn_endpoint_connection_logging_enabled", + "elasticbeanstalk_environment_cloudwatch_logging_enabled" + ] + }, + { + "Id": "LOG-09", + "Description": "The information system protects audit records from unauthorized access, modification, and deletion.", + "Name": "Log Protection", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-04", + "IVS-01" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.2", + "27002: 12.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(2)", + "AU-9(3)", + "AU-9(4)", + "AU-12(3)", + "AU-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.IP-4", + "PR.IP-6", + "PR.PT-1", + "PR.DS-1", + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.DS-01", + "PR.DS-02", + "PR.DS-11" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5", + "10.5.1", + "10.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_log_file_validation_enabled", + "cloudtrail_kms_encryption_enabled", + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", + "cloudwatch_log_group_kms_encryption_enabled", + "cloudwatch_log_group_not_publicly_accessible" + ] + }, + { + "Id": "LOG-10", + "Description": "Establish and maintain a monitoring and internal reporting capability over the operations of cryptographic, encryption and key management policies, processes, procedures, and controls.", + "Name": "Encryption Monitoring and Reporting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-02", + "EKM-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1", + "27002: 10.1", + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-1", + "AU-9", + "AU-9(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "PR.PT-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.1.1", + "10.2.1", + "10.4.1" + ] + } + ] + } + ], + "Checks": [ + "kms_cmk_rotation_enabled", + "acm_certificates_expiration_check" + ] + }, + { + "Id": "LOG-11", + "Description": "Log and monitor key lifecycle management events to enable auditing and reporting on usage of cryptographic keys.", + "Name": "Transaction/Activity Logging", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.PT-1", + "DE.AE-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-09" + ] + } + ] + } + ], + "Checks": [ + "cloudtrail_s3_dataevents_read_enabled", + "cloudtrail_s3_dataevents_write_enabled", + "cloudtrail_multi_region_enabled_logging_management_events" + ] + }, + { + "Id": "LOG-13", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the reporting of anomalies and failures of the monitoring system and provide immediate notification to the accountable party.", + "Name": "Failures and Anomalies Reporting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.3", + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.1", + "27002: 16.1.1", + "27001: A.16.1.2", + "27017: 16.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.24", + "27001: A.6.8", + "27002: 6.8 (g)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-5", + "AU-5(2)", + "AU-6", + "AU-6(3)", + "AU-6(4)", + "AU-6(5)", + "AU-16" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-3", + "DE.DP-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.AE-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.4.3", + "10.7.1", + "10.7.2", + "10.7.3" + ] + } + ] + } + ], + "Checks": [ + "guardduty_is_enabled", + "guardduty_no_high_severity_findings", + "cloudwatch_alarm_actions_enabled", + "cloudwatch_alarm_actions_alarm_state_configured" + ] + }, + { + "Id": "SEF-03", + "Description": "'Establish, document, approve, communicate, apply, evaluate and maintain a security incident response plan, which includes but is not limited to: relevant internal departments, impacted CSCs, and other business critical relationships (such as supply-chain) that may be impacted.'", + "Name": "Incident Response Plans", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2", + "CC7.3", + "CC7.4" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "17.2", + "17.4" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27017: CLD.12.1.5", + "27018: 16.1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: A.5.26", + "27002: 5.26 (e,f)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IR-1", + "IR-2", + "IR-2(1)-(3)", + "IR-3", + "IR-3(1)-(3)", + "IR-4", + "IR-4(1)-(15)", + "IR-5", + "IR-5(1)", + "IR-6", + "IR-6(1)-(3)", + "IR-7", + "IR-7(1)", + "IR-7(2)", + "IR-8", + "IR-8(1)", + "IR-9", + "IR-9(1)-(4)", + "PM-12" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "RS.CO-1", + "RS.CO-4", + "ID.AM-6", + "ID.GV-2", + "ID.SC-5", + "PR.IP-9", + "PR.IP10" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AT-01", + "PR.AT-02", + "RS.MA-01", + "GV.SC-08", + "ID.IM-02", + "ID.IM-04", + "RC.RP-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.1", + "12.10.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1", + "12.10.5" + ] + } + ] + } + ], + "Checks": [ + "ssmincidents_enabled_with_plans" + ] + }, + { + "Id": "SEF-06", + "Description": "Define, implement and evaluate processes, procedures and technical measures supporting business processes to triage security-related events.", + "Name": "Event Triage Processes", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.4", + "27002: 16.1.4", + "27017: 16.1.4", + "27018: 16.1.4", + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27018: 16.1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.25" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-7", + "CA-7(3)", + "CA-7(4)", + "CA-7(5)", + "CA-7(6)", + "IR-4", + "IR-4(1)", + "IR-4(3)", + "IR-4(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.AE-2", + "DE.AE-4", + "RS.RP-1", + "RS.AN-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "RS.MA-02", + "RS.MA-03", + "RS.AN-03", + "DE.AE-02", + "DE.AE-04", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "RS.MI-02", + "RC.RP-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "guardduty_is_enabled", + "securityhub_enabled" + ] + }, + { + "Id": "SEF-08", + "Description": "Maintain points of contact for applicable regulation authorities, national and local law enforcement, and other legal jurisdictional authorities.", + "Name": "Points of Contact Maintenance", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "17.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 4.2", + "27001: A.6.1.3", + "27002: 6.1.3", + "27017: 6.1.3", + "27018: 6.1.3", + "27001: A.16.1.1", + "27002: 16.1.1", + "27001: A.18.1.1", + "27002: 18.1.1", + "27017: 18.1.1", + "27018: 18.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.5", + "27001: A.5.24", + "27002: 5.24 Incident management procedure (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IR-4", + "IR-4(8)", + "IR-6", + "IR-6(3)", + "IR-7", + "IR-7(2)", + "PM-21", + "PM-23", + "PM-26" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-2", + "RS.CO-3", + "RS.CO-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.RR-02", + "RS.CO-02", + "RS.CO-03" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "account_maintain_current_contact_details", + "account_security_contact_information_is_registered", + "account_maintain_different_contact_details_to_security_billing_and_operations" + ] + }, + { + "Id": "TVM-02", + "Description": "Establish, document, approve, communicate, apply, evaluate and maintain policies and procedures to protect against malware on managed assets. Review and update the policies and procedures at least annually.", + "Name": "Malware Protection Policy and Procedures", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC6.8" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-01", + "GRM-06", + "GRM-09" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "9.7", + "10.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.1.1", + "1.5.1", + "5.2.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS1.2", + "TS1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5", + "27002: 5", + "27001: A.12.2.1", + "27001: A.6.2.1", + "27002: 6.2.1 (h)", + "27001: A.6.2.2", + "27002: 6.2.2 (j)", + "27001: A.7.2.2", + "27002: 7.2.2 (d)", + "27001: A.10.1.1", + "27002: 10.1.1 (g)", + "27001: A.13.2.1", + "27002: 13.2.1 (b)", + "27001: A.15.1.2", + "27017: 15.1.2", + "27001: A.12.2.1", + "27002: 12.2.1 (a),(d)", + "27017: CLD.9.5.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5.1", + "27001: A.5.4", + "27001: A.5.7", + "27001: A.5.37", + "27001: A.8.7", + "27002: 5.7 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-3", + "RA-3(3)", + "RA-5", + "RA-5(3)", + "RA-5(5)", + "SI-3", + "SI-3(4)", + "SI-3(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "DE.CM-4", + "DE.CM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.PO-01", + "GV.PO-02", + "ID.IM-03", + "DE.CM-01", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.4", + "12.1", + "12.1.1", + "12.3.1", + "12.5.1", + "12.11" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.1.1", + "12.1.2", + "5.1.1", + "5.3.2.1" + ] + } + ] + } + ], + "Checks": [ + "guardduty_ec2_malware_protection_enabled" + ] + }, + { + "Id": "TVM-03", + "Description": "Define, implement and evaluate processes, procedures and technical measures to enable both scheduled and emergency responses to vulnerability identifications, based on the identified risk.", + "Name": "Vulnerability Remediation Schedule", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC7.1", + "CC7.4" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "7.2", + "7.7", + "17.9" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1", + "TM2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.12.2.1", + "27001: A.12.6.1", + "27002: 12.6.1(c)(d)(j)", + "27018: 12.6.1(k)(i)" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.8.7", + "27001: A.8.8", + "27001: A.8.32", + "27002: 8.7", + "27002: 8.8", + "27002: 8.32" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-31", + "RA-3", + "RA-3(1)", + "RA-5", + "RA-5(2)-(4)", + "RA-5(6)", + "SI-3", + "SI-3(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "RS.AN-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01", + "ID.RA-06", + "ID.RA-08", + "PR.PS-02", + "PR.PS-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "6.1.a", + "6.1.b" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.1.1", + "6.3.1", + "6.3.2", + "6.3.3", + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "ssm_managed_compliant_patching", + "rds_instance_minor_version_upgrade_enabled", + "rds_cluster_minor_version_upgrade_enabled", + "redshift_cluster_automatic_upgrades", + "elasticbeanstalk_environment_managed_updates_enabled", + "dms_instance_minor_version_upgrade_enabled", + "elasticache_redis_cluster_auto_minor_version_upgrades", + "memorydb_cluster_auto_minor_version_upgrades", + "mq_broker_auto_minor_version_upgrades", + "opensearch_service_domains_updated_to_the_latest_service_software_version", + "kafka_cluster_uses_latest_version" + ] + }, + { + "Id": "TVM-04", + "Description": "Define, implement and evaluate processes, procedures and technical measures to update detection tools, threat signatures, and indicators of compromise on a weekly, or more frequent basis.", + "Name": "Detection Updates", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "No mapping" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "10.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS1.3", + "TS1.4", + "TM1.3", + "TM1.4", + "IM1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.5.1.1", + "27002: 5.1.1 (h)", + "27001: A.12.6.1", + "27002: 12.6.1 (b),(c)" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.5.1", + "27001: A.8.8", + "27001: A.8.15", + "27001: A.8.16", + "27002: 5.1", + "27002: 5.37", + "27002: 8.8", + "27002: 8.15 (d)", + "27002: 8.16 (d,e)", + "27002: 8.31 2nd (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-7", + "CM-7(4)", + "RA-3", + "RA-3(3)", + "RA-5(2)", + "SA-10", + "SA-10(5)", + "SA-11", + "SA-11(2)", + "SI-2", + "SI-2(4)", + "SI-3", + "SI-3(4)", + "SI-4", + "SI-4(9)", + "SI-4(24)", + "SI-8", + "SI-8(2)", + "SI-8(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-02", + "ID.RA-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.2", + "5.2a", + "5.2b", + "5.2c" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "5.3.1" + ] + } + ] + } + ], + "Checks": [ + "guardduty_is_enabled", + "inspector2_is_enabled" + ] + }, + { + "Id": "TVM-05", + "Description": "Define, implement and evaluate processes, procedures and technical measures to identify updates for applications which use third party or open source libraries according to the organization's vulnerability management policy.", + "Name": "External Library Vulnerabilities", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC3.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "No mapping" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1", + "SD2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.12.6.2", + "27002: 12.6.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A 5.6", + "27001: A.8.19", + "27001: A.8.8", + "27001: A.8.28", + "27001: A.8.31", + "27002: 5.6 (c)", + "27001: 8.19", + "27001: 8.8", + "27001: 8.28", + "27001: 8.31" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-5", + "RA-5(3)", + "SA-11", + "SA-11(2)", + "SA-11(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01", + "ID.RA-03", + "PR.PS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "6.2", + "6.3.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "6.3.2", + "6.3.3" + ] + } + ] + } + ], + "Checks": [ + "inspector2_is_enabled", + "ecr_repositories_scan_vulnerabilities_in_latest_image", + "ecr_registry_scan_images_on_push_enabled" + ] + }, + { + "Id": "TVM-07", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the detection of vulnerabilities on organizationally managed assets at least monthly.", + "Name": "Vulnerability Identification", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "7.1", + "7.5", + "7.6" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.5", + "5.2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.6", + "27001: A.12.6.1", + "27002: 12.6.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.8", + "27002: 8.8" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-5", + "RA-5(4)", + "RA-5(5)", + "SA-11", + "SA-11(5)", + "SA-15(5)", + "SC-7", + "SC-7(10)", + "SI-3(8)", + "SI-3(10)", + "SI-7", + "SI-7(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.RA-1", + "DE.CM-8", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "11.2", + "11.2.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "6.3.2", + "6.3.3", + "11.3.2", + "11.3.2.1" + ] + } + ] + } + ], + "Checks": [ + "inspector2_is_enabled", + "inspector2_active_findings_exist", + "guardduty_is_enabled", + "ecr_repositories_scan_vulnerabilities_in_latest_image" + ] + }, + { + "Id": "UEM-08", + "Description": "Protect information from unauthorized disclosure on managed endpoint devices with storage encryption.", + "Name": "Storage Encryption", + "Attributes": [ + { + "Section": "Universal Endpoint Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "MOS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.6" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "3.1.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "PA1.2", + "PA1.3", + "PA1.5", + "PA2.2", + "PM1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.11.2.7", + "27002: 11.2.7", + "27001: A.18.1.1", + "27017: 18.1.1", + "27001: A.12.3.1", + "27017: 12.3.1", + "27018: A.11.4", + "27018: A.11.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.1", + "27002: 8.1 (h)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-19(5)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.4", + "3.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.5.1", + "3.6" + ] + } + ] + } + ], + "Checks": [ + "ec2_ebs_volume_encryption", + "ec2_ebs_default_encryption", + "workspaces_volume_encryption_enabled" + ] + }, + { + "Id": "UEM-11", + "Description": "Configure managed endpoints with Data Loss Prevention (DLP) technologies and rules in accordance with a risk assessment.", + "Name": "Data Loss Prevention", + "Attributes": [ + { + "Section": "Universal Endpoint Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.13" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.5", + "PA2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.3", + "27002: 12.3", + "27001: A.8.3.1", + "27002: 8.3.1", + "27001: A.12.2", + "27002: 12.2", + "27001: A.18.1.3", + "27002: 18.1.3", + "27001: A.6.1.1", + "27017: 6.1.1", + "27018: 12.3.1", + "27018: 10.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.12", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-7", + "SC-7(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02", + "PR.DS-10", + "PR.PS-01", + "ID.AM-08", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A3.2.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "A3.2.6" + ] + } + ] + } + ], + "Checks": [ + "macie_is_enabled", + "macie_automated_sensitive_data_discovery_enabled" + ] + } + ] +} diff --git a/prowler/compliance/azure/csa_ccm_4.0_azure.json b/prowler/compliance/azure/csa_ccm_4.0_azure.json new file mode 100644 index 0000000000..b4505ac089 --- /dev/null +++ b/prowler/compliance/azure/csa_ccm_4.0_azure.json @@ -0,0 +1,7548 @@ +{ + "Framework": "CSA-CCM", + "Name": "CSA Cloud Controls Matrix (CCM) v4.0.13", + "Version": "4.0", + "Provider": "Azure", + "Description": "The Cloud Security Alliance (CSA) Cloud Controls Matrix (CCM) is a cybersecurity control framework for cloud computing, composed of 197 control objectives structured in 17 domains covering all key aspects of cloud technology. The CCM can be used as a tool for the systematic assessment of a cloud implementation, and provides guidance on which security controls should be implemented by which actor within the cloud supply chain.", + "Requirements": [ + { + "Id": "A&A-02", + "Description": "Conduct independent audit and assurance assessments according to relevant standards at least annually.", + "Name": "Independent Assessments", + "Attributes": [ + { + "Section": "Audit & Assurance", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC4.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AAC-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.5.2", + "5.2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "AS1.1", + "AS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.2.1", + "27002: 18.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.35", + "27001: A.5.36" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-2", + "CA-2(1)", + "CA-2(2)", + "CA-7", + "CA-7(1)" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-01" + ] + } + ] + } + ], + "Checks": [ + "defender_ensure_defender_for_app_services_is_on", + "defender_ensure_defender_for_azure_sql_databases_is_on", + "defender_ensure_defender_for_databases_is_on", + "defender_ensure_defender_for_keyvault_is_on", + "defender_ensure_defender_for_server_is_on" + ] + }, + { + "Id": "A&A-04", + "Description": "Verify compliance with all relevant standards, regulations, legal/contractual, and statutory requirements applicable to the audit.", + "Name": "Requirements Compliance", + "Attributes": [ + { + "Section": "Audit & Assurance", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC3.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-01", + "GRM-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "7.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "AS1.1", + "AS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.3.2", + "27001: A.18.2.2", + "27002: 18.2.2", + "27001: A.18.2.3", + "27002: 18.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.3.2", + "27001: A.5.31", + "27001: A.5.32", + "27001: A.5.33", + "27001: A.5.34", + "27001: A.5.36" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-1" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-3", + "DE.DP-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-01" + ] + } + ] + } + ], + "Checks": [ + "defender_ensure_defender_for_app_services_is_on", + "defender_ensure_defender_for_azure_sql_databases_is_on", + "defender_ensure_defender_for_databases_is_on", + "defender_ensure_defender_for_server_is_on", + "defender_ensure_mcas_is_enabled", + "monitor_diagnostic_settings_exists", + "policy_ensure_asc_enforcement_enabled" + ] + }, + { + "Id": "AIS-04", + "Description": "Define and implement a SDLC process for application design, development, deployment, and operation in accordance with security requirements defined by the organization.", + "Name": "Secure Application Design and Development", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AIS-01", + "AIS-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.4", + "5.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.1.1", + "27002: 14.1.1", + "27017: 14.1.1", + "27001: A.14.1.2", + "27002: 14.1.2", + "27017: 14.1.2", + "27001: A.14.2.1", + "27002: 14.2.1", + "27017: 14.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.8", + "27001: A.8.25", + "27001: A.8.26", + "27001: A.8.28" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-2", + "PL-8", + "PL-8(1)", + "SA-3", + "SA-3(1)", + "SA-4", + "SA-4(2)", + "SA-4(3)", + "SA-4(8)", + "SA-4(9)", + "SA-5", + "SA-8", + "SA-8(1)-(7)", + "SA-8(9)-(13)", + "SA-8(15)-(20)", + "SA-8(22)", + "SA-8(24)-(28)", + "SA-8(30)-(33)", + "SA-17", + "SA-17(1)-(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-6", + "PR.DS-7", + "PR.IP-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "PR.IR-01", + "PR.PS-01", + "PR.PS-02", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.1", + "6.2.3", + "6.5.2" + ] + } + ] + } + ], + "Checks": [ + "app_ensure_auth_is_set_up", + "app_ftp_deployment_disabled", + "app_function_access_keys_configured", + "app_function_ftps_deployment_disabled", + "app_register_with_identity" + ] + }, + { + "Id": "AIS-05", + "Description": "Implement a testing strategy, including criteria for acceptance of new information systems, upgrades and new versions, which provides application security assurance and maintains compliance while enabling organizational speed of delivery goals. Automate when applicable and possible.", + "Name": "Automated Application Security Testing", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AIS-01", + "AIS-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.12", + "16.13" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD2.3", + "SD2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.2.8", + "27001: A.14.2.9", + "27001: A.12.1.2", + "27002: 12.1.2", + "27001: A.14.1.1", + "27002: 14.1.1", + "27001: A.14.2.2", + "27002: 14.2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.25", + "27001: A.8.29", + "27001: A.8.32", + "27002: 8.25 (e)", + "27002: 8.32 (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SA-11", + "SA-11(1)-(9)", + "SI-6", + "SI-6(2)", + "SI-6(3)", + "SI-10", + "SI-10(1)-(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.PT-3", + "PR.IP-12", + "DE.CM-8" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "ID.RA-01", + "PR.PS-01", + "PR.PS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A.3.2.2", + "A.3.2.2.1", + "6.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.4", + "6.4.1", + "6.4.2", + "6.5.1" + ] + } + ] + } + ], + "Checks": [ + "defender_auto_provisioning_vulnerabilty_assessments_machines_on", + "defender_container_images_resolved_vulnerabilities", + "defender_container_images_scan_enabled", + "defender_ensure_defender_for_containers_is_on", + "sqlserver_va_periodic_recurring_scans_enabled", + "sqlserver_vulnerability_assessment_enabled" + ] + }, + { + "Id": "AIS-07", + "Description": "Define and implement a process to remediate application security vulnerabilities, automating remediation when possible.", + "Name": "Application Vulnerability Remediation", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.1", + "CC7.4", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.2", + "16.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27001: A.12.6.1", + "27002: 12.6.1", + "27017: 12.6.1", + "27018: 12.6.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.26", + "27001: A.8.8", + "27002: 5.26 (j)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SI-2", + "SI-2(2)-(6)", + "SA-11", + "SA-11(2)", + "SA-15", + "SA-15(1)-(3)", + "SA-15(5)-(8)", + "SA-15(10)-(12)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.IP-12", + "DE.CM-8", + "RS.AN-5", + "RS.MI-3", + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "ID.RA-01", + "ID.RA-06", + "ID.RA-08", + "PR.PS-02", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.2", + "6.5", + "6.5.1-10" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "11.3.1", + "11.3.1.1" + ] + } + ] + } + ], + "Checks": [ + "defender_container_images_resolved_vulnerabilities", + "defender_container_images_scan_enabled", + "defender_ensure_defender_for_containers_is_on", + "sqlserver_va_scan_reports_configured", + "sqlserver_vulnerability_assessment_enabled" + ] + }, + { + "Id": "BCR-08", + "Description": "Periodically backup data stored in the cloud. Ensure the confidentiality, integrity and availability of the backup, and verify data restoration from backup for resiliency.", + "Name": "Backup", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "A1.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "11.1", + "11.2", + "11.3", + "11.4", + "11.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8", + "5.2.9" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.3", + "27017: 12.3", + "27018: 12.3.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.13", + "27001: A.5.23", + "27001: A.5.30", + "27002: 8.13", + "27002: 5.23 2nd (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-4", + "CP-4(4)", + "CP-6", + "CP-6(1)-(3)", + "CP-9", + "CP-9(1)", + "CP-9(2)", + "CP-10", + "CP-10(2)", + "CP-10(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-4", + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-11", + "RC.RP-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "9.5.1", + "12.10.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1", + "10.3.3" + ] + } + ] + } + ], + "Checks": [ + "storage_ensure_encryption_with_customer_managed_keys", + "vm_backup_enabled", + "vm_sufficient_daily_backup_retention_period" + ] + }, + { + "Id": "BCR-09", + "Description": "Establish, document, approve, communicate, apply, evaluate and maintain a disaster response plan to recover from natural and man-made disasters. Update the plan at least annually or upon significant changes.", + "Name": "Disaster Response Plan", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "CC3.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8", + "5.2.9", + "1.6.1", + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "BC1.4", + "BC2.1", + "BC2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.29", + "27001: A.5.30", + "27002: 5.29", + "27002: 5.30" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2(1)", + "CP-2(2)", + "CP-2(3)", + "CP-2(5)", + "CP-2(6)", + "CP-2(7)", + "CP-2(8)", + "PE-13", + "PE-13(1)", + "PE-13(2)", + "PE-13(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-9", + "PR.IP-10", + "RC.IM-1", + "RC.IM-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-04" + ] + } + ] + } + ], + "Checks": [ + "defender_ensure_defender_for_server_is_on", + "vm_backup_enabled" + ] + }, + { + "Id": "BCR-11", + "Description": "Supplement business-critical equipment with redundant equipment independently located at a reasonable minimum distance in accordance with applicable industry standards.", + "Name": "Equipment Redundancy", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "No", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "CC3.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-06" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "BC1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.20", + "27001: A.7.11", + "27001: A.8.14", + "27002: 5.20 (t)", + "27002: 8.14 (c)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2", + "CP-2(2)", + "CP-4(3)", + "CP-6", + "CP-6(1)", + "CP-7", + "CP-8", + "CP-8(1)-(3)", + "CP-9", + "CP-9(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.BE-4", + "ID.BE-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.OC-04", + "GV.OC-05", + "PR.IR-03" + ] + } + ] + } + ], + "Checks": [ + "storage_blob_versioning_is_enabled", + "storage_geo_redundant_enabled", + "vm_scaleset_associated_with_load_balancer", + "vm_scaleset_not_empty" + ] + }, + { + "Id": "CCC-04", + "Description": "Restrict the unauthorized addition, removal, update, and management of organization assets.", + "Name": "Unauthorized Change Protection", + "Attributes": [ + { + "Section": "Change Control and Configuration Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "CCC-04" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.1", + "1.3.4", + "5.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.4", + "SM2.6" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.1.4", + "27002: 12.1.4", + "27001: A.12.4.2", + "27002: 12.4.2", + "27001: A.14.2.2", + "27017: 14.2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.3", + "27001: A.8.4", + "27001: A.8.15", + "27001: A.8.31", + "27001: A.8.32" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-7", + "CA-7(4)", + "CM-3", + "CM-3(1)", + "CM-3(5)", + "CM-3(7)", + "CM-3(8)", + "CM-5", + "CM-5(1)", + "CM-5(4)", + "CM-5(5)", + "CM-6", + "CM-6(1)", + "CM-6(2)", + "CM-7", + "CM-7(1)", + "CM-7(4)", + "CM-7(5)", + "CM-7(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-1", + "ID.AM-2", + "ID.AM-4", + "PR.MA-1", + "PR.MA-2", + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-01", + "ID.AM-02", + "ID.AM-04", + "ID.AM-08", + "PR.PS-02", + "PR.PS-03", + "PR.PS-05", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.5.1", + "6.5.2" + ] + } + ] + } + ], + "Checks": [ + "iam_custom_role_has_permissions_to_administer_resource_locks", + "monitor_alert_create_policy_assignment", + "monitor_diagnostic_setting_with_appropriate_categories", + "monitor_diagnostic_settings_exists", + "policy_ensure_asc_enforcement_enabled", + "storage_ensure_soft_delete_is_enabled" + ] + }, + { + "Id": "CCC-07", + "Description": "Implement detection measures with proactive notification in case of changes deviating from the established baseline.", + "Name": "Detection of Baseline Deviation", + "Attributes": [ + { + "Section": "Change Control and Configuration Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-01" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.5.1", + "1.5.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.2.2", + "27001: A.14.2.4", + "27001: A.12.4.1", + "27002: 12.4.1 (g)", + "27001: A.5.1.1", + "27017: 5.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.9", + "27001: A.8.15", + "27002: 8.9" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-6", + "CM-6(2)", + "SI-2", + "SI-2(2)-(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.MA-1", + "PR.IP-1", + "DE.DP-4", + "PR.IP-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-01", + "DE.CM-09", + "DE.AE-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4.5.3", + "6.4.5.4", + "11.5", + "11.5.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "11.5.2", + "11.6.1" + ] + } + ] + } + ], + "Checks": [ + "defender_ensure_defender_for_app_services_is_on", + "defender_ensure_defender_for_azure_sql_databases_is_on", + "defender_ensure_defender_for_server_is_on", + "defender_ensure_wdatp_is_enabled", + "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_create_update_sqlserver_fr", + "monitor_alert_delete_nsg", + "monitor_alert_delete_policy_assignment", + "monitor_alert_delete_public_ip_address_rule", + "monitor_alert_delete_security_solution", + "monitor_alert_delete_sqlserver_fr", + "monitor_diagnostic_settings_exists", + "policy_ensure_asc_enforcement_enabled" + ] + }, + { + "Id": "CEK-03", + "Description": "Provide cryptographic protection to data at-rest and in-transit, using cryptographic libraries certified to approved standards.", + "Name": "Data Encryption", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-03", + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.6", + "3.1", + "3.11", + "11.3", + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.1", + "27001: A.18.1.2", + "27001: A.18.1.3", + "27001: A.18.1.4", + "27001: A.18.1.5", + "27001: A.10.1", + "27002: 10.1", + "27001: A.13.2.1", + "27002: 13.2.1", + "27001: A.18", + "27002: 18", + "27001: A.14.1.2", + "27002: 14.1.2", + "27001: A.14.1.3", + "27002 14.1.3 c)", + "27001 - A.10.1.1", + "27017 - 10.1.1", + "27001 - A.10.1.2", + "27017 - 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.8.24", + "27002: 8.24 Other Information (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-19", + "AC-19(5)", + "SC-8", + "SC-8(1)", + "SC-8(3)", + "SC-8(4)", + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-28", + "SC-28(1)-(3)", + "SI-4", + "SI-4(10)", + "SI-7", + "SI-7(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "Requirement 3", + "2.2.3", + "2.3", + "3.4", + "3.5.3", + "4.1", + "8.2.1", + "PCI Glossary - Strong Cryptography" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.7", + "3.5.1", + "4.2.1", + "4.2.1.2", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "app_minimum_tls_version_12", + "databricks_workspace_cmk_encryption_enabled", + "mysql_flexible_server_ssl_connection_enabled", + "postgresql_flexible_server_enforce_ssl_enabled", + "sqlserver_tde_encrypted_with_cmk", + "sqlserver_tde_encryption_enabled", + "storage_ensure_encryption_with_customer_managed_keys", + "storage_infrastructure_encryption_is_enabled", + "storage_secure_transfer_required_is_enabled", + "storage_smb_channel_encryption_with_secure_algorithm", + "vm_ensure_attached_disks_encrypted_with_cmk", + "vm_ensure_unattached_disks_encrypted_with_cmk" + ] + }, + { + "Id": "CEK-04", + "Description": "Use encryption algorithms that are appropriate for data protection, considering the classification of data, associated risks, and usability of the encryption technology.", + "Name": "Encryption Algorithm", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.2", + "27001: 6.1.3", + "27001: A.8.2", + "27002: 8.2", + "27001: A.8.3", + "27001: A.10.1.1", + "27002: 10.1.1 (b)", + "27001: A.10.1.2", + "27002: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.2", + "27001: 6.1.3", + "27001: A.8.24", + "27001: A.5.12", + "27001: A.5.13", + "27002: 8.24 General (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2", + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02", + "ID.AM-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A2", + "Requirement 3", + "2.3", + "2.2.3", + "3.4", + "3.5.3", + "4.1", + "8.2.1", + "PCI Glossary - Strong Cryptography" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.7", + "3.5.1", + "4.2.1", + "4.2.1.2", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "app_minimum_tls_version_12", + "keyvault_key_rotation_enabled", + "mysql_flexible_server_minimum_tls_version_12", + "postgresql_flexible_server_enforce_ssl_enabled", + "sqlserver_recommended_minimal_tls_version", + "storage_ensure_minimum_tls_version_12", + "storage_smb_protocol_version_is_latest" + ] + }, + { + "Id": "CEK-08", + "Description": "CSPs must provide the capability for CSCs to manage their own data encryption keys.", + "Name": "CSC Key Management Capability", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2", + "SC2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1", + "27017: 10.1", + "27001: A.10.1.1", + "27017: 10.1.1", + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.23", + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-9", + "CP-9(8)", + "SA-9", + "SA-9(6)", + "SC-12", + "SC-12(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.SC-3", + "ID.AM-6", + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.SC-05" + ] + } + ] + } + ], + "Checks": [ + "databricks_workspace_cmk_encryption_enabled", + "keyvault_access_only_through_private_endpoints", + "keyvault_private_endpoints", + "keyvault_rbac_enabled", + "storage_ensure_encryption_with_customer_managed_keys" + ] + }, + { + "Id": "CEK-10", + "Description": "Generate Cryptographic keys using industry accepted cryptographic libraries specifying the algorithm strength and the random number generator used.", + "Name": "Key Generation", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2", + "TS2.3", + "SY1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27002: 10.1.1 (e)", + "27017: 10.1.1", + "27001: A.10.1.2", + "27002: 10.1.2", + "27002: 10.1.2 (a)", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24", + "27002: 8.24 (d), Key management (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.2.3", + "3.6.1", + "PCI Glossary - Cryptographic Key Generation" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1", + "3.6.1.1", + "3.7.1" + ] + } + ] + } + ], + "Checks": [ + "keyvault_rbac_enabled", + "storage_ensure_encryption_with_customer_managed_keys" + ] + }, + { + "Id": "CEK-12", + "Description": "Rotate cryptographic keys in accordance with the calculated cryptoperiod, which includes provisions for considering the risk of information disclosure and legal and regulatory requirements.", + "Name": "Key Rotation", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27017: 10.1.1", + "27001: A.10.1.2", + "27002: 10.1.2 e)", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.31", + "27001: A.8.24", + "27002: 5.31 Cryptography", + "27002: 8.24 Key management (e,m)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "ID.GV-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05", + "GV.OC-03" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.7.4", + "3.7.5" + ] + } + ] + } + ], + "Checks": [ + "keyvault_key_expiration_set_in_non_rbac", + "keyvault_key_rotation_enabled", + "keyvault_non_rbac_secret_expiration_set", + "keyvault_rbac_key_expiration_set", + "keyvault_rbac_secret_expiration_set", + "storage_key_rotation_90_days" + ] + }, + { + "Id": "CEK-14", + "Description": "Define, implement and evaluate processes, procedures and technical measures to destroy keys stored outside a secure environment and revoke keys stored in Hardware Security Modules (HSMs) when they are no longer needed, which include provisions for legal and regulatory requirements.", + "Name": "Key Destruction", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27017: 10.1.1", + "27017: 10.1.2", + "27001: A.10.1.2", + "27002: 10.1.2 (j)", + "27001: A.18.1.3", + "27002: 18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.31", + "27001: A.8.24", + "27002: 5.31 Cryptography", + "27002: 8.24 Key management (j,m)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.IP-6", + "ID.GV-3", + "PR.DS-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05", + "ID.AM-08", + "GV.OC-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.6.4", + "3.6.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.7.4", + "3.7.5" + ] + } + ] + } + ], + "Checks": [ + "keyvault_recoverable" + ] + }, + { + "Id": "DCS-06", + "Description": "Catalogue and track all relevant physical and logical assets located at all of the CSP's sites within a secured system.", + "Name": "Assets Cataloguing and Tracking", + "Attributes": [ + { + "Section": "Datacenter Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DCS - 01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "1.1", + "2.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SM2.6" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.1.1", + "27002: 8.1.1", + "27017: 8.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.9" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-8", + "CM-8(1)", + "CM-8(2)", + "CM-8(4)", + "CM-8(7)", + "CM-8(8)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-1", + "ID.AM-2", + "ID.AM-4", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-01", + "ID.AM-02", + "ID.AM-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.4", + "9.7.1", + "9.9.1", + "9.9.1.a", + "9.9.1.b", + "9.9.1.c", + "12.3.3", + "12.3.4" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1.1", + "6.3.2", + "9.4.2", + "9.4.3", + "12.5.1" + ] + } + ] + } + ], + "Checks": [ + "defender_ensure_mcas_is_enabled", + "monitor_diagnostic_settings_exists", + "policy_ensure_asc_enforcement_enabled" + ] + }, + { + "Id": "DSP-02", + "Description": "Apply industry accepted methods for the secure disposal of data from storage media such that data is not recoverable by any forensic means.", + "Name": "Secure Disposal", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3", + "CC6.4", + "CC6.5", + "CC6.7", + "P4.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DSI-07" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.3.3", + "7.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.3.2", + "27002: 8.3.2", + "27001: A.11.2.7", + "27002: 11.2.7" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.7.10", + "27001: A.7.14", + "27001: A.8.10", + "27002: 7.10 (Secure reuse or disposal)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-22", + "SI-12", + "SI-12(3)", + "SI-18", + "SI-18(1)", + "SI-18(4)", + "SI-18(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.SC-10", + "PR.PS-02", + "PR.PS-03", + "ID.AM-08" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.1", + "9.8", + "9.8.1", + "9.8.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1", + "3.7.5", + "9.4.7" + ] + } + ] + } + ], + "Checks": [ + "storage_ensure_file_shares_soft_delete_is_enabled", + "storage_ensure_soft_delete_is_enabled", + "vm_sufficient_daily_backup_retention_period" + ] + }, + { + "Id": "DSP-03", + "Description": "Create and maintain a data inventory, at least for any sensitive data and personal data.", + "Name": "Data Inventory", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1", + "1.3.2", + "1.3.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.1.1", + "27002: 8.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.9", + "27001: A.8.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-12", + "CM-12(1)", + "PM-5", + "PM-5(1)", + "SI-12", + "SI-12(1)", + "SI-19", + "SI-19(1)", + "SI-19(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1", + "9.4.5" + ] + } + ] + } + ], + "Checks": [ + "defender_ensure_defender_for_storage_is_on", + "defender_ensure_mcas_is_enabled", + "monitor_diagnostic_settings_exists", + "policy_ensure_asc_enforcement_enabled" + ] + }, + { + "Id": "DSP-04", + "Description": "Classify data according to its type and sensitivity level.", + "Name": "Data Classification", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "C1.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DSI-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.7" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1", + "1.3.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.2.1", + "27002: 8.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-16", + "AC-16(9)", + "PM-22", + "PM-23", + "PT-2", + "PT-2(1)", + "SI-18", + "SI-18(2)", + "SI-19", + "SI-19(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-05", + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "9.6.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "9.4.2", + "9.4.3" + ] + } + ] + } + ], + "Checks": [ + "defender_ensure_defender_for_storage_is_on" + ] + }, + { + "Id": "DSP-07", + "Description": "Develop systems, products, and business practices based upon a principle of security by design and industry best practices.", + "Name": "Data Protection by Design and Default", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "PI1.2", + "PI1.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.3.1", + "5.3.2", + "5.3.3", + "5.3.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD2.2", + "IM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.1.1", + "27002:14.1.1", + "27001: A.14.2.5", + "27002:14.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.27", + "27001: A.8.28", + "27001: A.8.29", + "27002: 5.8 (Information security requirements a-i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-17", + "PM-24", + "PM-25", + "PT-2", + "PT-2(2)", + "SA-3", + "SA-4", + "SA-5", + "SA-8", + "SA-8(9)", + "SA-8(13)", + "SA-8(18)", + "SA-8(20)", + "SA-8(22)", + "SA-8(23)", + "SA-8(33)", + "SA-15", + "SA-15(12)", + "SC-3", + "SC-3(3)", + "SC-7", + "SC-7(24)", + "SC-8", + "SC-8(1)-(4)", + "SC-28", + "SC-28(1)", + "SI-12", + "SI-12(1)-(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.PT-3", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.1" + ] + } + ] + } + ], + "Checks": [ + "aisearch_service_not_publicly_accessible", + "aks_clusters_public_access_disabled", + "containerregistry_not_publicly_accessible", + "cosmosdb_account_firewall_use_selected_networks", + "sqlserver_unrestricted_inbound_access", + "storage_blob_public_access_level_is_disabled", + "storage_default_network_access_rule_is_denied", + "storage_ensure_private_endpoints_in_storage_accounts", + "vm_ensure_attached_disks_encrypted_with_cmk", + "vm_ensure_unattached_disks_encrypted_with_cmk" + ] + }, + { + "Id": "DSP-10", + "Description": "Define, implement and evaluate processes, procedures and technical measures that ensure any transfer of personal or sensitive data is protected from unauthorized access and only processed within scope as permitted by the respective laws and regulations.", + "Name": "Sensitive Data Transfer", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-02", + "EKM-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.1", + "3.12", + "3.13" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "9.5.1", + "9.5.2", + "9.5.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.4", + "IM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.13.2.1", + "27002: 13.2.1", + "27001: A.8.3.3", + "27002: 8.3.3", + "27001: A.13.2.3", + "27002: 13.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.7.10" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-4", + "AC-4(23)-(25)", + "CA-3", + "CA-3(6)", + "CA-6", + "CA-6(1)", + "CA-6(2)", + "SC-4", + "SC-4(2)", + "SC-7", + "SC-7(10)", + "SC-7(24)", + "SC-8", + "SC-8(1)-(5)", + "SC-16", + "SC-16(1)-(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2", + "PR.DS-5", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02", + "PR.IR-01", + "ID.AM-03", + "GV.OC-03", + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "4.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "4.1.1", + "4.2.1", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "app_ensure_http_is_redirected_to_https", + "app_ensure_using_http20", + "sqlserver_recommended_minimal_tls_version", + "storage_ensure_minimum_tls_version_12", + "storage_secure_transfer_required_is_enabled" + ] + }, + { + "Id": "DSP-16", + "Description": "Data retention, archiving and deletion is managed in accordance with business requirements, applicable laws and regulations.", + "Name": "Data Retention and Deletion", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "C1.1", + "C1.2", + "CC3.1", + "P4.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-02", + "BCR-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.4", + "3.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.3.1", + "7.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.33", + "27001: A.8.10", + "27002: 5.33 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SI-12", + "SI-12(1)-(3)", + "SI-18", + "SI-18(1)", + "SI-18(4)", + "SI-18(5)", + "SI-19", + "SI-19(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-3", + "PR.IP-6", + "ID.GV-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "GV.OC-03", + "GV.SC-10", + "PR.DS-11" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1" + ] + } + ] + } + ], + "Checks": [ + "monitor_storage_account_with_activity_logs_cmk_encrypted", + "monitor_storage_account_with_activity_logs_is_private", + "postgresql_flexible_server_log_retention_days_greater_3", + "sqlserver_auditing_retention_90_days", + "storage_ensure_file_shares_soft_delete_is_enabled", + "storage_ensure_soft_delete_is_enabled" + ] + }, + { + "Id": "DSP-17", + "Description": "Define and implement, processes, procedures and technical measures to protect sensitive data throughout it's lifecycle.", + "Name": "Sensitive Data Protection", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSC-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.1", + "CC6.1", + "CC6.3", + "CC6.7", + "CC8.1", + "C1.1", + "P2.0", + "P3.0", + "P4.0", + "P5.0", + "P6.0" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.1", + "3.1", + "3.14" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.3.3", + "9.1.1", + "9.2.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3", + "27002: 18.1.3", + "27001:A.18.1.4", + "27002:18.1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.11", + "27001: A.8.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-2", + "PM-22", + "PM-24", + "PT-7", + "PT-7(1)", + "PT-7(2)", + "PT-8", + "SC-8", + "SC-8(1)-(5)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2", + "PR.DS-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02", + "PR.DS-10" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.0 (including all subsections)", + "4.0 (including all subsections)" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.1.1", + "4.1.1" + ] + } + ] + } + ], + "Checks": [ + "containerregistry_not_publicly_accessible", + "cosmosdb_account_firewall_use_selected_networks", + "defender_ensure_defender_for_storage_is_on", + "sqlserver_tde_encrypted_with_cmk", + "sqlserver_tde_encryption_enabled", + "sqlserver_unrestricted_inbound_access", + "storage_account_key_access_disabled", + "storage_blob_public_access_level_is_disabled", + "storage_cross_tenant_replication_disabled", + "storage_default_network_access_rule_is_denied", + "storage_default_to_entra_authorization_enabled", + "storage_ensure_encryption_with_customer_managed_keys", + "vm_ensure_attached_disks_encrypted_with_cmk" + ] + }, + { + "Id": "GRC-05", + "Description": "Develop and implement an Information Security Program, which includes programs for all the relevant domains of the CCM.", + "Name": "Information Security Program", + "Attributes": [ + { + "Section": "Governance, Risk and Compliance", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "14.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SG2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 4.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-1", + "PM-3", + "PM-14", + "PL-2", + "PM-18", + "PM-31" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.4.1", + "A.3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.4.1", + "A3.1.1" + ] + } + ] + } + ], + "Checks": [ + "defender_ensure_defender_for_app_services_is_on", + "defender_ensure_defender_for_arm_is_on", + "defender_ensure_defender_for_azure_sql_databases_is_on", + "defender_ensure_defender_for_databases_is_on", + "defender_ensure_defender_for_dns_is_on", + "defender_ensure_defender_for_keyvault_is_on", + "defender_ensure_defender_for_server_is_on", + "defender_ensure_mcas_is_enabled", + "defender_ensure_wdatp_is_enabled" + ] + }, + { + "Id": "IAM-02", + "Description": "Establish, document, approve, communicate, implement, apply, evaluate and maintain strong password policies and procedures. Review and update the policies and procedures at least annually.", + "Name": "Strong Password Policy and Procedures", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-12", + "GRM-06", + "GRM-09" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.1.1", + "1.5.1", + "4.1.2", + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1", + "SA1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5", + "27002: 5", + "27001: A.9.4.3", + "27002: 9.4.3", + "27017: 9.4.3", + "27018: 9.4.3", + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27001: A.7.2.2", + "27002: 7.2.2", + "27001: A.9.2.6", + "27002: 9.2.6", + "27001: A.9.2.3", + "27002: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5.1", + "27001: A.5.4", + "27001: A.5.17", + "27001: A.6.3", + "27001: A.8.5", + "27001: A.5.37" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(3)", + "AC-2(11)", + "AC-3", + "AC-3(3)", + "AC-12", + "AC-12(1)", + "IA-2", + "IA-2(10)", + "IA-5", + "IA-5(1)", + "IA-5(18)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "PR.AC-1", + "PR.AC-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.PO-01", + "GV.PO-02", + "ID.IM-03", + "PR.AA-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.4", + "12.1", + "12.1.1", + "12.11" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.1.1", + "8.3.8" + ] + } + ] + } + ], + "Checks": [ + "entra_privileged_user_has_mfa", + "entra_security_defaults_enabled" + ] + }, + { + "Id": "IAM-03", + "Description": "Manage, store, and review the information of system identities, and level of access.", + "Name": "Identity Inventory", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-04", + "IAM-08", + "IAM-10" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1", + "5.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.2 (c)", + "27001: A.8.1.1", + "27002: 8.1.1", + "27001: A.9.4.1", + "27002: 9.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.2 (c)", + "27001: A.5.15", + "27001: A.5.16", + "27001: A.5.18", + "27001: A.7.4", + "27001: A.8.15", + "27001: A.8.2", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-10", + "AU-10(1)", + "AU-10(2)", + "AU-16", + "AU-16(1)", + "IA-4", + "IA-4(8)", + "IA-4(9)", + "IA-5", + "IA-5(5)", + "IA-8", + "IA-8(4)", + "PM-5(1)", + "SA-8", + "SA-8(22)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-04", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.4.a" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.5", + "7.2.5.1" + ] + } + ] + } + ], + "Checks": [ + "entra_global_admin_in_less_than_five_users" + ] + }, + { + "Id": "IAM-04", + "Description": "Employ the separation of duties principle when implementing information system access.", + "Name": "Separation of Duties", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC1.3", + "CC5.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.2.2", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.6.1.2", + "27002: 6.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.18", + "27001: A.5.3", + "27001: A.8.2" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(3)", + "AC-2(11)", + "AC-6", + "AC-6(1)-(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4", + "6.4.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.5.3", + "6.5.4", + "7.2.1", + "7.2.2" + ] + } + ] + } + ], + "Checks": [ + "entra_policy_default_users_cannot_create_security_groups", + "entra_policy_ensure_default_user_cannot_create_apps", + "iam_role_user_access_admin_restricted", + "iam_subscription_roles_owner_custom_not_created" + ] + }, + { + "Id": "IAM-05", + "Description": "Employ the least privilege principle when implementing information system access.", + "Name": "Least Privilege", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-06", + "IVS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.1.1", + "27002: 9.1.1", + "27001: A.9.1.2", + "27002: 9.1.2", + "27001: A.9.2.3", + "27002: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.8.2", + "27002: 5.15 (Other information 2nd (a))" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(4)", + "IA-12", + "IA-12(2)", + "IA-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "7.1", + "7.1.1", + "7.1.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "7.2.2", + "7.2.5", + "7.2.6" + ] + } + ] + } + ], + "Checks": [ + "app_function_identity_without_admin_privileges", + "entra_policy_ensure_default_user_cannot_create_apps", + "entra_policy_ensure_default_user_cannot_create_tenants", + "entra_policy_restricts_user_consent_for_apps", + "iam_custom_role_has_permissions_to_administer_resource_locks", + "iam_subscription_roles_owner_custom_not_created" + ] + }, + { + "Id": "IAM-07", + "Description": "De-provision or respectively modify access of movers / leavers or system identity changes in a timely manner in order to effectively adopt and communicate identity and access management policies.", + "Name": "User Access Changes and Revocation", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.3", + "6.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.18" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(1)", + "AC-2(2)", + "AC-2(6)", + "AC-2(8)", + "AC-3", + "AC-3(8)", + "AC-6", + "AC-6(7)", + "AU-10", + "AU-10(4)", + "AU-16", + "AU-16(1)", + "CM-7", + "CM-7(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4", + "PR.IP-11" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.RR-04", + "GV.SC-10", + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1.2", + "8.1.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.2.5", + "8.2.6" + ] + } + ] + } + ], + "Checks": [ + "entra_global_admin_in_less_than_five_users" + ] + }, + { + "Id": "IAM-08", + "Description": "Review and revalidate user access for least privilege and separation of duties with a frequency that is commensurate with organizational risk tolerance.", + "Name": "User Access Review", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-10" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.5", + "27001: A.9.2.6", + "27001: A.9.4.1", + "27017: 9.4.1", + "27001: A.6.1.2", + "27001: A 9.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.3", + "27001: A.5.18", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(4)", + "AC-6(8)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.5.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.5.1", + "7.2.5", + "7.2.4" + ] + } + ] + } + ], + "Checks": [ + "entra_global_admin_in_less_than_five_users", + "keyvault_non_rbac_secret_expiration_set", + "keyvault_rbac_secret_expiration_set", + "storage_key_rotation_90_days" + ] + }, + { + "Id": "IAM-09", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the segregation of privileged access roles such that administrative access to data, encryption and key management capabilities and logging capabilities are distinct and separated.", + "Name": "Segregation of Privileged Access Roles", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.1", + "CC6.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.3", + "27002: 9.2.3", + "27017: 9.2.3", + "27018: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.2", + "27001: A.8.18", + "27002: 8.2 (j)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-3(7)", + "AC-6(4)", + "AC-6(8)", + "IA-5", + "IA-5(6)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.3", + "3.5.2", + "7.1.2", + "7.1.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1", + "3.7.6", + "6.5.3", + "6.5.4", + "7.2.1", + "7.2.2", + "10.3.1" + ] + } + ] + } + ], + "Checks": [ + "entra_global_admin_in_less_than_five_users", + "entra_policy_guest_invite_only_for_admin_roles", + "entra_policy_guest_users_access_restrictions", + "iam_custom_role_has_permissions_to_administer_resource_locks", + "iam_subscription_roles_owner_custom_not_created" + ] + }, + { + "Id": "IAM-10", + "Description": "Define and implement an access process to ensure privileged access roles and rights are granted for a time limited period, and implement procedures to prevent the culmination of segregated privileged access.", + "Name": "Management of Privileged Access Roles", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1", + "6.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.3", + "27002: 9.2.3", + "27017: 9.2.3", + "27018: 9.2.3", + "27001: A.9.4.4", + "27002: 9.4.4", + "27017: 9.4.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.2", + "27001: A.8.18", + "27002: 8.2 (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(7)", + "AC-3", + "AC-3(4)", + "AC-3(11)", + "AC-3(13)", + "AC-3(14)", + "AC-6", + "AC-6(4)", + "AC-6(5)", + "AC-6(8)", + "AC-12", + "AC-12(3)", + "AC-17", + "AC-17(4)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "7.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "7.2.2" + ] + } + ] + } + ], + "Checks": [ + "entra_conditional_access_policy_require_mfa_for_management_api", + "entra_global_admin_in_less_than_five_users", + "entra_user_with_vm_access_has_mfa", + "iam_role_user_access_admin_restricted", + "iam_subscription_roles_owner_custom_not_created", + "vm_jit_access_enabled" + ] + }, + { + "Id": "IAM-12", + "Description": "Define, implement and evaluate processes, procedures and technical measures to ensure the logging infrastructure is read-only for all with write access, including privileged access roles, and that the ability to disable it is controlled through a procedure that ensures the segregation of duties and break glass procedures.", + "Name": "Safeguard Logs Integrity", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.3" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1", + "27018: 12.4.1", + "27001: A.12.4.2", + "27002: 12.4.2", + "27017: 12.4.2", + "27018: 12.4.2", + "27001: A.12.4.3", + "27002: 12.4.3", + "27017: 12.4.3", + "27018: 12.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15", + "27001: A.8.18", + "27002: 8.15 Protection of Logs" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(11)", + "AC-2(12)", + "IA-8", + "IA-8(4)", + "SA-8", + "SA-8(22)", + "SC-34", + "SC-34(1)", + "SC-34(2)", + "SC-36", + "SI-4", + "SI-4(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4" + ] + } + ] + } + ], + "Checks": [ + "entra_trusted_named_locations_exists", + "keyvault_logging_enabled", + "monitor_diagnostic_setting_with_appropriate_categories", + "monitor_storage_account_with_activity_logs_is_private" + ] + }, + { + "Id": "IAM-13", + "Description": "Define, implement and evaluate processes, procedures and technical measures that ensure users are identifiable through unique IDs or which can associate individuals to the usage of user IDs.", + "Name": "Uniquely Identifiable Users", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.1", + "27002: 9.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.16" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-3", + "AC-3(14)", + "AC-24", + "AC-24(2)", + "AU-10", + "AU-10(1)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(12)", + "IA-4", + "IA-4(1)", + "SA-8", + "SA-8(22)", + "SC-23", + "SC-23(3)", + "SC-40(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1", + "8.2", + "8.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.2.1", + "8.2.2", + "8.2.4" + ] + } + ] + } + ], + "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", + "postgresql_flexible_server_entra_id_authentication_enabled", + "sqlserver_azuread_administrator_enabled" + ] + }, + { + "Id": "IAM-14", + "Description": "Define, implement and evaluate processes, procedures and technical measures for authenticating access to systems, application and data assets, including multifactor authentication for at least privileged user and sensitive data access. Adopt digital certificates or alternatives which achieve an equivalent level of security for system identities.", + "Name": "Strong Authentication", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.3", + "6.5", + "12.5", + "12.7" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3", + "SA1.4", + "SA1.8" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.1.2", + "27002: 9.1.2", + "27017: 9.1.2", + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27001: A.9.4.2", + "27002: 9.4.2", + "27017: 9.4.2", + "27018: 9.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.17", + "27001: A.8.5", + "27001: A.8.24", + "27002: 8.5", + "27002: 8.24 other information (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(5)", + "AC-7", + "AC-7(4)", + "AU-10", + "AU-10(2)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(8)", + "IA-2(12)", + "IA-3", + "IA-3(1)", + "IA-5", + "IA-5(2)", + "IA-5(7)", + "IA-5(9)", + "IA-5(10)", + "IA-5(12)", + "IA-5(14)-(16)", + "IA-8", + "IA-8(1)", + "IA-8(6)", + "SC-23", + "SC-23(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6", + "PR.AC-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1.2", + "8.1.3", + "8.1.6", + "8.2", + "8.3", + "8.3.2", + "12.3.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "8.3.1", + "8.3.2", + "8.4.1", + "8.4.2", + "8.4.3" + ] + } + ] + } + ], + "Checks": [ + "entra_non_privileged_user_has_mfa", + "entra_privileged_user_has_mfa", + "entra_security_defaults_enabled", + "entra_user_with_vm_access_has_mfa" + ] + }, + { + "Id": "IAM-15", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the secure management of passwords.", + "Name": "Passwords Management", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27018: 9.2.4", + "27001: A.9.3.1", + "27002: 9.3.1", + "27017: 9.3.1", + "27018: 9.3.1", + "27001: A.9.4.3", + "27002: 9.4.3", + "27017: 9.4.3", + "27018: 9.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.17" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IA-4", + "IA-4(8)", + "IA-5", + "IA-5(1)", + "IA-5(8)", + "IA-5(18)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.2", + "8.2.1-6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.2", + "2.3.1", + "8.3.5", + "8.3.6", + "8.3.7", + "8.3.8", + "8.3.9", + "8.3.10", + "8.3.10.1", + "8.6.2" + ] + } + ] + } + ], + "Checks": [ + "entra_privileged_user_has_mfa", + "entra_security_defaults_enabled" + ] + }, + { + "Id": "IAM-16", + "Description": "Define, implement and evaluate processes, procedures and technical measures to verify access to data and system functions is authorized.", + "Name": "Authorization Mechanisms", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3", + "SA1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.5", + "27002: 9.2.5", + "27017: 9.2.5", + "27018: 9.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.18" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-3", + "AC-3(5)", + "AC-4", + "AC-4(17)", + "AC-4(21)", + "AC-4(22)", + "AC-6", + "AC-6(8)", + "AC-6(9)", + "AC-12", + "AC-12(1)", + "AC-20", + "AC-20(1)", + "AU-10", + "AU-10(1)", + "AU-10(2)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(12)", + "IA-3", + "IA-3(1)", + "IA-5(1)", + "IA-5(2)", + "IA-5(5)", + "IA-5(8)", + "IA-5(10)", + "IA-5(12)", + "IA-8", + "IA-8(1)", + "IA-8(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4", + "PR.AC-6", + "PR.AC-7", + "PR.PT-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-03", + "PR.AA-04", + "PR.AA-05", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.3", + "7.1.4" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.4", + "7.2.3", + "7.2.5.1" + ] + } + ] + } + ], + "Checks": [ + "aks_cluster_rbac_enabled", + "app_function_identity_is_configured", + "app_function_identity_without_admin_privileges", + "app_function_not_publicly_accessible", + "entra_policy_user_consent_for_verified_apps", + "iam_subscription_roles_owner_custom_not_created" + ] + }, + { + "Id": "IPY-03", + "Description": "Implement cryptographically secure and standardized network protocols for the management, import and export of data.", + "Name": "Secure Interoperability and Portability Management", + "Attributes": [ + { + "Section": "Interoperability & Portability", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IPY-04" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY1.1", + "SY1.2", + "NC1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1", + "27001: A.15.1.1", + "27002: 15.1.1", + "27017: 15.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.19", + "27001: A.5.23", + "27001: A.5.31", + "27001: A.5.32", + "27001: A.5.33", + "27001: A.5.34" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PT-2", + "PT-2(2)", + "SA-4", + "SC-16", + "SC-16(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.2.1", + "1.2.5", + "1.2.6", + "2.2.4", + "2.2.5", + "2.2.7", + "4.2.1" + ] + } + ] + } + ], + "Checks": [ + "storage_ensure_azure_services_are_trusted_to_access_is_enabled", + "storage_secure_transfer_required_is_enabled" + ] + }, + { + "Id": "IVS-02", + "Description": "Plan and monitor the availability, quality, and adequate capacity of resources in order to deliver the required system performance as determined by the business.", + "Name": "Capacity and Resource Planning", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "No", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-04" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.3", + "27001: 6.1", + "27001: 9.1", + "27001: A.12.1.3", + "27002: 12.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.3 (b)", + "27001: 6.1", + "27001: 9.1", + "27001: A.8.6", + "27001: A.8.14" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2", + "CP-2(2)", + "SC-5", + "SC-5(2)", + "SC-4", + "SI-4" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-4", + "ID.BE-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.IR-04", + "GV.OC-04" + ] + } + ] + } + ], + "Checks": [ + "vm_scaleset_associated_with_load_balancer", + "vm_scaleset_not_empty" + ] + }, + { + "Id": "IVS-03", + "Description": "Monitor, encrypt and restrict communications between environments to only authenticated and authorized connections, as justified by the business. Review these configurations at least annually, and support them by a documented justification of all allowed services, protocols, ports, and compensating controls.", + "Name": "Network Security", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-06" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.8", + "3.1", + "12.2", + "13.6", + "13.9" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "NC1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.13.1.1", + "27002: 13.1.1", + "27001: A.13.1.2", + "27002: 13.1.2", + "27001: A.13.1.3", + "27002: 13.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.5.15", + "27001: A.5.37", + "27001: A.8.5", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.21", + "27001: A.8.22", + "27001: A.8.24", + "27002: A.5.15 2nd c)", + "27002: 8.20", + "27002: 8.21", + "27002: 8.22", + "27002: 8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-1", + "SC-4", + "SC-7", + "SC-7(4)", + "SC-7(5)", + "SC-7(8)", + "SC-7(9)", + "SC-7(11)", + "SC-8", + "SC-8(1)", + "SC-11", + "SC-12", + "SC-16", + "SC-23", + "SC-29", + "SC-29(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-5", + "PR.AC-7", + "PR.PT-4", + "DE.CM-1", + "DE.CM-7", + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.IR-01", + "PR.AA-03", + "PR.AA-05", + "DE.CM-01", + "PR.DS-02", + "ID.AM-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "1.1.6", + "1.2", + "1.2.3", + "2.2", + "4.1.1", + "10.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.2.5", + "1.2.6", + "1.2.7", + "1.4.2", + "2.2.4", + "2.2.5", + "2.2.7", + "4.2.1", + "10.1.1" + ] + } + ] + } + ], + "Checks": [ + "aks_clusters_created_with_private_nodes", + "aks_clusters_public_access_disabled", + "aks_network_policy_enabled", + "network_bastion_host_exists", + "network_flow_log_captured_sent", + "network_flow_log_more_than_90_days", + "network_http_internet_access_restricted", + "network_rdp_internet_access_restricted", + "network_ssh_internet_access_restricted", + "network_udp_internet_access_restricted", + "network_watcher_enabled" + ] + }, + { + "Id": "IVS-04", + "Description": "Harden host and guest OS, hypervisor or infrastructure control plane according to their respective best practices, and supported by technical controls, as part of a security baseline.", + "Name": "OS Hardening and Base Controls", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.8", + "CC7.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-07", + "IVS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "4.1", + "4.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3", + "5.2.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY1.1", + "SY1.3", + "SY1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.14.2.2", + "27002: 14.2.2", + "27001: A.14.2.3", + "27001 A.14.2.4", + "27018: 12.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.5.37", + "27001: A.8.5", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.22", + "27001: A.8.24", + "27002: 8.20", + "27002: 8.22", + "27002: 8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-6", + "CM-6(1)", + "SC-29", + "SC-29(1)", + "SC-2", + "SC-7", + "SC-7(12)", + "SC-30", + "SC-34", + "SC-35", + "SC-39", + "SC-44" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-1", + "PR.PT-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.1" + ] + } + ] + } + ], + "Checks": [ + "defender_assessments_vm_endpoint_protection_installed", + "defender_ensure_system_updates_are_applied", + "vm_ensure_using_managed_disks", + "vm_linux_enforce_ssh_authentication", + "vm_trusted_launch_enabled" + ] + }, + { + "Id": "IVS-06", + "Description": "Design, develop, deploy and configure applications and infrastructures such that CSP and CSC (tenant) user access and intra-tenant access is appropriately segmented and segregated, monitored and restricted from other tenants.", + "Name": "Segmentation and Segregation", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-09" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.3.4", + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SC2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.1", + "27001: A.13.1.3", + "27002: 13.1.3", + "27017: 13.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.1", + "27001: A.5.15", + "27001: A.5.20", + "27001: A.8.3", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.22", + "27002: 5.15 (b)", + "27002: 8.3 (b)", + "27002: 8.16 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-3", + "SC-7", + "SC-7(20)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.AC-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.IR-01", + "PR.PS-01", + "PR.PS-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.6", + "8.3.1", + "10.8", + "11.3", + "A3.2.1", + "A3.3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "A1.1.1", + "A1.1.2", + "A1.1.3" + ] + } + ] + } + ], + "Checks": [ + "app_function_not_publicly_accessible", + "app_function_vnet_integration_enabled", + "containerregistry_uses_private_link", + "cosmosdb_account_use_private_endpoints", + "databricks_workspace_vnet_injection_enabled", + "network_http_internet_access_restricted", + "storage_ensure_private_endpoints_in_storage_accounts" + ] + }, + { + "Id": "IVS-07", + "Description": "Use secure and encrypted communication channels when migrating servers, services, applications, or data to cloud environments. Such channels must include only up-to-date and approved protocols.", + "Name": "Migration to Cloud Environments", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-10" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.4", + "IM1.4", + "NC1.4", + "SC2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.13.1.1", + "27002: 13.1.1", + "27017: 13.1.1", + "27018: 13.1.1", + "27001: A.13.1.2", + "27002: 13.1.2", + "27017: 13.1.2", + "27018: 13.1.2", + "27001: A.13.1.3", + "27002: 13.1.3", + "27017: 13.1.3", + "27018: 13.1.3", + "27001: A.13.2.1", + "27002: 13.2.1", + "27017: 13.2.1", + "27018: 13.2.1", + "27001: A.13.2.2", + "27002: 13.2.2", + "27017: 13.2.2", + "27018: 13.2.2", + "27001: A.13.2.3", + "27002: 13.2.3", + "27017: 13.2.3", + "27018: 13.2.3", + "27001: A.13.2.4", + "27002: 13.2.4", + "27017: 13.2.4", + "27018: 13.2.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.8.20", + "27001: A.8.24", + "27002: 8.20 (e)", + "27002: 8.24 Guidance (b,f), other information (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-17", + "AC-20", + "SC-7", + "SC-7(28)", + "SC-8", + "SC-8(1)", + "SC-12", + "SC-23", + "SC-29", + "SI-7", + "SI-7(1)-(3)", + "SI-7(5)-(10)", + "SI-7(12)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "4.2.1" + ] + } + ] + } + ], + "Checks": [ + "mysql_flexible_server_ssl_connection_enabled", + "postgresql_flexible_server_enforce_ssl_enabled" + ] + }, + { + "Id": "IVS-09", + "Description": "Define, implement and evaluate processes, procedures and defense-in-depth techniques for protection, detection, and timely response to network-based attacks.", + "Name": "Network Defense", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.6", + "CC6.8", + "CC7.1", + "CC7.2", + "CC7.5" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-13" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "13.3", + "13.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.3", + "5.2.4", + "5.2.5", + "5.2.7", + "5.3.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "NC1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1", + "27001: 6.2", + "27001: A.14.1.2", + "27002: 14.1.2", + "27017: 14.1.2", + "27001: A.11.1.4", + "27002: 11.1.4", + "27017: 11.1.4", + "27018: 16.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1", + "27001: 6.2", + "27001: A.5.24", + "27001: A.5.26", + "27001: A.8.8", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.21", + "27001: A.8.22", + "27001: A.8.26", + "27002: 8.8 (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-8", + "PL-8(1)", + "SC-5", + "SC-5(1)", + "SC-5(3)", + "SC-7", + "SC-7(13)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.DP-1", + "DE.CM-1", + "DE.CM-7", + "PR.AC-5", + "RS.MI-2", + "PR.DS-2", + "RS.RP-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-03", + "DE.CM-01", + "PR.IR-01", + "RS.MA-01", + "RS.MI-01", + "RS.MI-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.6", + "1.1", + "1.2", + "1.3", + "1.5", + "12.10.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.1.1", + "1.3.1", + "1.3.2", + "1.3.3", + "1.4.1", + "1.4.2", + "1.4.3", + "1.4.4", + "1.4.5", + "1.5.1", + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "defender_ensure_defender_for_arm_is_on", + "defender_ensure_defender_for_dns_is_on", + "defender_ensure_defender_for_server_is_on", + "defender_ensure_iot_hub_defender_is_on", + "defender_ensure_wdatp_is_enabled", + "network_flow_log_captured_sent", + "network_watcher_enabled", + "sqlserver_microsoft_defender_enabled", + "vm_jit_access_enabled" + ] + }, + { + "Id": "LOG-02", + "Description": "Define, implement and evaluate processes, procedures and technical measures to ensure the security and retention of audit logs.", + "Name": "Audit Logs Protection", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.1", + "8.9", + "8.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "3.1.3", + "5.1.2", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3", + "27002: 18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.28", + "27001: A.5.33", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-4", + "AU-11" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.IP-4", + "PR.IP-6", + "PR.PT-1", + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.DS-01", + "PR.DS-02", + "ID.AM-08", + "PR.DS-11", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5", + "10.7" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4", + "10.5.1" + ] + } + ] + } + ], + "Checks": [ + "keyvault_logging_enabled", + "monitor_diagnostic_setting_with_appropriate_categories", + "monitor_storage_account_with_activity_logs_cmk_encrypted", + "monitor_storage_account_with_activity_logs_is_private", + "storage_ensure_encryption_with_customer_managed_keys", + "storage_ensure_soft_delete_is_enabled" + ] + }, + { + "Id": "LOG-03", + "Description": "Identify and monitor security-related events within applications and the underlying infrastructure. Define and implement a system to generate alerts to responsible stakeholders based on such events and corresponding metrics.", + "Name": "Security Monitoring and Alerting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-03", + "SEF-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4", + "5.2.7", + "1.6.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2", + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.28", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-5", + "AU-5(2)", + "AU-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.AE-2", + "DE.AE-3", + "DE.AE-5", + "DE.CM-1", + "DE.CM-2", + "DE.CM-3", + "DE.CM-4", + "DE.CM-5", + "DE.CM-6", + "DE.CM-7", + "DE.DP-1", + "DE.DP-4", + "DE.AE-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.AE-02", + "DE.AE-03", + "DE.AE-04", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1", + "10.2.2", + "10.4.1.1", + "10.4.2.1", + "10.4.3" + ] + } + ] + } + ], + "Checks": [ + "defender_attack_path_notifications_properly_configured", + "defender_ensure_defender_for_app_services_is_on", + "defender_ensure_defender_for_azure_sql_databases_is_on", + "defender_ensure_defender_for_server_is_on", + "defender_ensure_notify_alerts_severity_is_high", + "defender_ensure_notify_emails_to_owners", + "defender_ensure_wdatp_is_enabled", + "monitor_alert_create_update_security_solution", + "monitor_alert_service_health_exists" + ] + }, + { + "Id": "LOG-04", + "Description": "Restrict audit logs access to authorized personnel and maintain records that provide unique access accountability.", + "Name": "Audit Logs Access and Accountability", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.14" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "3.1.1", + "4.1.2", + "4.1.3", + "4.2.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.2", + "27001: A.12.4.1", + "27002: 12.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.33", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(4)", + "AU-9(6)", + "AU-10" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.1", + "10.2.1", + "10.2.3", + "10.5.1", + "10.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1.3", + "10.3.1" + ] + } + ] + } + ], + "Checks": [ + "monitor_storage_account_with_activity_logs_is_private" + ] + }, + { + "Id": "LOG-05", + "Description": "Monitor security audit logs to detect activity outside of typical or expected patterns. Establish and follow a defined process to review and take appropriate and timely actions on detected anomalies.", + "Name": "Audit Logs Monitoring and Response", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.8", + "8.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.1", + "1.6.2", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.3", + "27002: 12.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15", + "27001: A.8.16" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-6", + "AU-6(1)", + "AU-6(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-3", + "PR.PT-1", + "RS.AN-1", + "RS.CO-1.", + "DE.AE-1", + "DE.AE-5", + "DE.DP-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-03", + "PR.PS-04", + "DE.AE-02", + "DE.AE-03", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.6", + "10.6.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.4.1.1", + "10.4.2.1" + ] + } + ] + } + ], + "Checks": [ + "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_create_update_sqlserver_fr", + "monitor_alert_delete_nsg", + "monitor_alert_delete_policy_assignment", + "monitor_alert_delete_public_ip_address_rule", + "monitor_alert_delete_security_solution", + "monitor_alert_delete_sqlserver_fr", + "monitor_alert_service_health_exists" + ] + }, + { + "Id": "LOG-07", + "Description": "Establish, document and implement which information meta/data system events should be logged. Review and update the scope at least annually or whenever there is a change in the threat environment.", + "Name": "Logging Scope", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5.3", + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5.3", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-1", + "AU-14", + "AU-16" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.SC-3", + "ID.SC-4", + "PR.PT-1", + "ID.GV-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1", + "10.2.2" + ] + } + ] + } + ], + "Checks": [ + "app_function_application_insights_enabled", + "appinsights_ensure_is_configured", + "monitor_diagnostic_setting_with_appropriate_categories", + "monitor_diagnostic_settings_exists", + "network_flow_log_captured_sent", + "network_flow_log_more_than_90_days" + ] + }, + { + "Id": "LOG-08", + "Description": "Generate audit records containing relevant security information.", + "Name": "Log Records", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-3", + "AU-3(1)", + "AU-3(3)", + "AU-6", + "AU-6(8)", + "AU-12", + "AU-12(1)", + "AU-12(2)", + "AU-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.PT-1", + "DE.AE-3", + "DE.CM-1", + "DE.CM-2", + "DE.CM-3", + "DE.CM-6", + "DE.CM-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.2" + ] + } + ] + } + ], + "Checks": [ + "app_function_application_insights_enabled", + "app_http_logs_enabled", + "appinsights_ensure_is_configured", + "keyvault_logging_enabled", + "monitor_diagnostic_setting_with_appropriate_categories", + "monitor_diagnostic_settings_exists", + "mysql_flexible_server_audit_log_connection_activated", + "mysql_flexible_server_audit_log_enabled", + "network_flow_log_captured_sent", + "network_flow_log_more_than_90_days", + "postgresql_flexible_server_log_checkpoints_on", + "postgresql_flexible_server_log_connections_on", + "postgresql_flexible_server_log_disconnections_on", + "postgresql_flexible_server_log_retention_days_greater_3", + "sqlserver_auditing_enabled", + "sqlserver_auditing_retention_90_days" + ] + }, + { + "Id": "LOG-09", + "Description": "The information system protects audit records from unauthorized access, modification, and deletion.", + "Name": "Log Protection", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-04", + "IVS-01" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.2", + "27002: 12.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(2)", + "AU-9(3)", + "AU-9(4)", + "AU-12(3)", + "AU-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.IP-4", + "PR.IP-6", + "PR.PT-1", + "PR.DS-1", + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.DS-01", + "PR.DS-02", + "PR.DS-11" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5", + "10.5.1", + "10.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4" + ] + } + ] + } + ], + "Checks": [ + "keyvault_logging_enabled", + "monitor_diagnostic_setting_with_appropriate_categories", + "monitor_storage_account_with_activity_logs_cmk_encrypted", + "monitor_storage_account_with_activity_logs_is_private", + "storage_ensure_encryption_with_customer_managed_keys" + ] + }, + { + "Id": "LOG-10", + "Description": "Establish and maintain a monitoring and internal reporting capability over the operations of cryptographic, encryption and key management policies, processes, procedures, and controls.", + "Name": "Encryption Monitoring and Reporting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-02", + "EKM-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1", + "27002: 10.1", + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-1", + "AU-9", + "AU-9(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "PR.PT-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.1.1", + "10.2.1", + "10.4.1" + ] + } + ] + } + ], + "Checks": [ + "keyvault_key_expiration_set_in_non_rbac", + "keyvault_key_rotation_enabled", + "keyvault_rbac_key_expiration_set" + ] + }, + { + "Id": "LOG-11", + "Description": "Log and monitor key lifecycle management events to enable auditing and reporting on usage of cryptographic keys.", + "Name": "Transaction/Activity Logging", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.PT-1", + "DE.AE-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-09" + ] + } + ] + } + ], + "Checks": [ + "monitor_diagnostic_setting_with_appropriate_categories", + "monitor_diagnostic_settings_exists" + ] + }, + { + "Id": "LOG-13", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the reporting of anomalies and failures of the monitoring system and provide immediate notification to the accountable party.", + "Name": "Failures and Anomalies Reporting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.3", + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.1", + "27002: 16.1.1", + "27001: A.16.1.2", + "27017: 16.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.24", + "27001: A.6.8", + "27002: 6.8 (g)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-5", + "AU-5(2)", + "AU-6", + "AU-6(3)", + "AU-6(4)", + "AU-6(5)", + "AU-16" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-3", + "DE.DP-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.AE-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.4.3", + "10.7.1", + "10.7.2", + "10.7.3" + ] + } + ] + } + ], + "Checks": [ + "defender_container_images_resolved_vulnerabilities", + "defender_ensure_defender_for_server_is_on", + "defender_ensure_notify_alerts_severity_is_high", + "defender_ensure_notify_emails_to_owners", + "defender_ensure_wdatp_is_enabled", + "monitor_alert_service_health_exists" + ] + }, + { + "Id": "SEF-03", + "Description": "'Establish, document, approve, communicate, apply, evaluate and maintain a security incident response plan, which includes but is not limited to: relevant internal departments, impacted CSCs, and other business critical relationships (such as supply-chain) that may be impacted.'", + "Name": "Incident Response Plans", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2", + "CC7.3", + "CC7.4" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "17.2", + "17.4" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27017: CLD.12.1.5", + "27018: 16.1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: A.5.26", + "27002: 5.26 (e,f)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IR-1", + "IR-2", + "IR-2(1)-(3)", + "IR-3", + "IR-3(1)-(3)", + "IR-4", + "IR-4(1)-(15)", + "IR-5", + "IR-5(1)", + "IR-6", + "IR-6(1)-(3)", + "IR-7", + "IR-7(1)", + "IR-7(2)", + "IR-8", + "IR-8(1)", + "IR-9", + "IR-9(1)-(4)", + "PM-12" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "RS.CO-1", + "RS.CO-4", + "ID.AM-6", + "ID.GV-2", + "ID.SC-5", + "PR.IP-9", + "PR.IP10" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AT-01", + "PR.AT-02", + "RS.MA-01", + "GV.SC-08", + "ID.IM-02", + "ID.IM-04", + "RC.RP-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.1", + "12.10.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1", + "12.10.5" + ] + } + ] + } + ], + "Checks": [ + "defender_attack_path_notifications_properly_configured", + "defender_ensure_defender_for_server_is_on", + "defender_ensure_notify_alerts_severity_is_high" + ] + }, + { + "Id": "SEF-06", + "Description": "Define, implement and evaluate processes, procedures and technical measures supporting business processes to triage security-related events.", + "Name": "Event Triage Processes", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.4", + "27002: 16.1.4", + "27017: 16.1.4", + "27018: 16.1.4", + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27018: 16.1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.25" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-7", + "CA-7(3)", + "CA-7(4)", + "CA-7(5)", + "CA-7(6)", + "IR-4", + "IR-4(1)", + "IR-4(3)", + "IR-4(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.AE-2", + "DE.AE-4", + "RS.RP-1", + "RS.AN-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "RS.MA-02", + "RS.MA-03", + "RS.AN-03", + "DE.AE-02", + "DE.AE-04", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "RS.MI-02", + "RC.RP-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "defender_ensure_defender_for_app_services_is_on", + "defender_ensure_defender_for_azure_sql_databases_is_on", + "defender_ensure_defender_for_databases_is_on", + "defender_ensure_defender_for_keyvault_is_on", + "defender_ensure_defender_for_server_is_on", + "defender_ensure_mcas_is_enabled", + "defender_ensure_wdatp_is_enabled" + ] + }, + { + "Id": "SEF-08", + "Description": "Maintain points of contact for applicable regulation authorities, national and local law enforcement, and other legal jurisdictional authorities.", + "Name": "Points of Contact Maintenance", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "17.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 4.2", + "27001: A.6.1.3", + "27002: 6.1.3", + "27017: 6.1.3", + "27018: 6.1.3", + "27001: A.16.1.1", + "27002: 16.1.1", + "27001: A.18.1.1", + "27002: 18.1.1", + "27017: 18.1.1", + "27018: 18.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.5", + "27001: A.5.24", + "27002: 5.24 Incident management procedure (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IR-4", + "IR-4(8)", + "IR-6", + "IR-6(3)", + "IR-7", + "IR-7(2)", + "PM-21", + "PM-23", + "PM-26" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-2", + "RS.CO-3", + "RS.CO-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.RR-02", + "RS.CO-02", + "RS.CO-03" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "defender_additional_email_configured_with_a_security_contact", + "defender_ensure_notify_emails_to_owners" + ] + }, + { + "Id": "TVM-02", + "Description": "Establish, document, approve, communicate, apply, evaluate and maintain policies and procedures to protect against malware on managed assets. Review and update the policies and procedures at least annually.", + "Name": "Malware Protection Policy and Procedures", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC6.8" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-01", + "GRM-06", + "GRM-09" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "9.7", + "10.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.1.1", + "1.5.1", + "5.2.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS1.2", + "TS1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5", + "27002: 5", + "27001: A.12.2.1", + "27001: A.6.2.1", + "27002: 6.2.1 (h)", + "27001: A.6.2.2", + "27002: 6.2.2 (j)", + "27001: A.7.2.2", + "27002: 7.2.2 (d)", + "27001: A.10.1.1", + "27002: 10.1.1 (g)", + "27001: A.13.2.1", + "27002: 13.2.1 (b)", + "27001: A.15.1.2", + "27017: 15.1.2", + "27001: A.12.2.1", + "27002: 12.2.1 (a),(d)", + "27017: CLD.9.5.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5.1", + "27001: A.5.4", + "27001: A.5.7", + "27001: A.5.37", + "27001: A.8.7", + "27002: 5.7 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-3", + "RA-3(3)", + "RA-5", + "RA-5(3)", + "RA-5(5)", + "SI-3", + "SI-3(4)", + "SI-3(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "DE.CM-4", + "DE.CM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.PO-01", + "GV.PO-02", + "ID.IM-03", + "DE.CM-01", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.4", + "12.1", + "12.1.1", + "12.3.1", + "12.5.1", + "12.11" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.1.1", + "12.1.2", + "5.1.1", + "5.3.2.1" + ] + } + ] + } + ], + "Checks": [ + "defender_assessments_vm_endpoint_protection_installed", + "defender_auto_provisioning_log_analytics_agent_vms_on", + "defender_ensure_defender_for_server_is_on", + "defender_ensure_wdatp_is_enabled" + ] + }, + { + "Id": "TVM-03", + "Description": "Define, implement and evaluate processes, procedures and technical measures to enable both scheduled and emergency responses to vulnerability identifications, based on the identified risk.", + "Name": "Vulnerability Remediation Schedule", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC7.1", + "CC7.4" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "7.2", + "7.7", + "17.9" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1", + "TM2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.12.2.1", + "27001: A.12.6.1", + "27002: 12.6.1(c)(d)(j)", + "27018: 12.6.1(k)(i)" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.8.7", + "27001: A.8.8", + "27001: A.8.32", + "27002: 8.7", + "27002: 8.8", + "27002: 8.32" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-31", + "RA-3", + "RA-3(1)", + "RA-5", + "RA-5(2)-(4)", + "RA-5(6)", + "SI-3", + "SI-3(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "RS.AN-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01", + "ID.RA-06", + "ID.RA-08", + "PR.PS-02", + "PR.PS-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "6.1.a", + "6.1.b" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.1.1", + "6.3.1", + "6.3.2", + "6.3.3", + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "app_ensure_java_version_is_latest", + "app_ensure_php_version_is_latest", + "app_ensure_python_version_is_latest", + "app_function_latest_runtime_version", + "defender_ensure_system_updates_are_applied" + ] + }, + { + "Id": "TVM-04", + "Description": "Define, implement and evaluate processes, procedures and technical measures to update detection tools, threat signatures, and indicators of compromise on a weekly, or more frequent basis.", + "Name": "Detection Updates", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "No mapping" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "10.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS1.3", + "TS1.4", + "TM1.3", + "TM1.4", + "IM1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.5.1.1", + "27002: 5.1.1 (h)", + "27001: A.12.6.1", + "27002: 12.6.1 (b),(c)" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.5.1", + "27001: A.8.8", + "27001: A.8.15", + "27001: A.8.16", + "27002: 5.1", + "27002: 5.37", + "27002: 8.8", + "27002: 8.15 (d)", + "27002: 8.16 (d,e)", + "27002: 8.31 2nd (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-7", + "CM-7(4)", + "RA-3", + "RA-3(3)", + "RA-5(2)", + "SA-10", + "SA-10(5)", + "SA-11", + "SA-11(2)", + "SI-2", + "SI-2(4)", + "SI-3", + "SI-3(4)", + "SI-4", + "SI-4(9)", + "SI-4(24)", + "SI-8", + "SI-8(2)", + "SI-8(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-02", + "ID.RA-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.2", + "5.2a", + "5.2b", + "5.2c" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "5.3.1" + ] + } + ] + } + ], + "Checks": [ + "defender_auto_provisioning_vulnerabilty_assessments_machines_on", + "defender_container_images_scan_enabled", + "defender_ensure_defender_for_containers_is_on", + "defender_ensure_defender_for_cosmosdb_is_on", + "defender_ensure_defender_for_os_relational_databases_is_on", + "defender_ensure_defender_for_server_is_on", + "defender_ensure_defender_for_sql_servers_is_on", + "defender_ensure_wdatp_is_enabled" + ] + }, + { + "Id": "TVM-05", + "Description": "Define, implement and evaluate processes, procedures and technical measures to identify updates for applications which use third party or open source libraries according to the organization's vulnerability management policy.", + "Name": "External Library Vulnerabilities", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC3.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "No mapping" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1", + "SD2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.12.6.2", + "27002: 12.6.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A 5.6", + "27001: A.8.19", + "27001: A.8.8", + "27001: A.8.28", + "27001: A.8.31", + "27002: 5.6 (c)", + "27001: 8.19", + "27001: 8.8", + "27001: 8.28", + "27001: 8.31" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-5", + "RA-5(3)", + "SA-11", + "SA-11(2)", + "SA-11(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01", + "ID.RA-03", + "PR.PS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "6.2", + "6.3.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "6.3.2", + "6.3.3" + ] + } + ] + } + ], + "Checks": [ + "defender_container_images_resolved_vulnerabilities", + "defender_container_images_scan_enabled", + "defender_ensure_defender_for_containers_is_on", + "sqlserver_va_emails_notifications_admins_enabled", + "sqlserver_va_periodic_recurring_scans_enabled", + "sqlserver_va_scan_reports_configured", + "sqlserver_vulnerability_assessment_enabled" + ] + }, + { + "Id": "TVM-07", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the detection of vulnerabilities on organizationally managed assets at least monthly.", + "Name": "Vulnerability Identification", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "7.1", + "7.5", + "7.6" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.5", + "5.2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.6", + "27001: A.12.6.1", + "27002: 12.6.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.8", + "27002: 8.8" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-5", + "RA-5(4)", + "RA-5(5)", + "SA-11", + "SA-11(5)", + "SA-15(5)", + "SC-7", + "SC-7(10)", + "SI-3(8)", + "SI-3(10)", + "SI-7", + "SI-7(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.RA-1", + "DE.CM-8", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "11.2", + "11.2.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "6.3.2", + "6.3.3", + "11.3.2", + "11.3.2.1" + ] + } + ] + } + ], + "Checks": [ + "defender_auto_provisioning_vulnerabilty_assessments_machines_on", + "defender_container_images_resolved_vulnerabilities", + "defender_container_images_scan_enabled", + "defender_ensure_defender_for_containers_is_on", + "defender_ensure_defender_for_server_is_on", + "defender_ensure_wdatp_is_enabled", + "sqlserver_microsoft_defender_enabled", + "sqlserver_vulnerability_assessment_enabled" + ] + }, + { + "Id": "UEM-08", + "Description": "Protect information from unauthorized disclosure on managed endpoint devices with storage encryption.", + "Name": "Storage Encryption", + "Attributes": [ + { + "Section": "Universal Endpoint Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "MOS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.6" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "3.1.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "PA1.2", + "PA1.3", + "PA1.5", + "PA2.2", + "PM1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.11.2.7", + "27002: 11.2.7", + "27001: A.18.1.1", + "27017: 18.1.1", + "27001: A.12.3.1", + "27017: 12.3.1", + "27018: A.11.4", + "27018: A.11.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.1", + "27002: 8.1 (h)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-19(5)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.4", + "3.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.5.1", + "3.6" + ] + } + ] + } + ], + "Checks": [ + "databricks_workspace_cmk_encryption_enabled", + "storage_infrastructure_encryption_is_enabled", + "vm_ensure_attached_disks_encrypted_with_cmk", + "vm_ensure_unattached_disks_encrypted_with_cmk" + ] + }, + { + "Id": "UEM-11", + "Description": "Configure managed endpoints with Data Loss Prevention (DLP) technologies and rules in accordance with a risk assessment.", + "Name": "Data Loss Prevention", + "Attributes": [ + { + "Section": "Universal Endpoint Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.13" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.5", + "PA2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.3", + "27002: 12.3", + "27001: A.8.3.1", + "27002: 8.3.1", + "27001: A.12.2", + "27002: 12.2", + "27001: A.18.1.3", + "27002: 18.1.3", + "27001: A.6.1.1", + "27017: 6.1.1", + "27018: 12.3.1", + "27018: 10.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.12", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-7", + "SC-7(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02", + "PR.DS-10", + "PR.PS-01", + "ID.AM-08", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A3.2.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "A3.2.6" + ] + } + ] + } + ], + "Checks": [ + "defender_ensure_defender_for_storage_is_on", + "defender_ensure_mcas_is_enabled" + ] + } + ] +} diff --git a/prowler/compliance/gcp/csa_ccm_4.0_gcp.json b/prowler/compliance/gcp/csa_ccm_4.0_gcp.json new file mode 100644 index 0000000000..6623fe5eca --- /dev/null +++ b/prowler/compliance/gcp/csa_ccm_4.0_gcp.json @@ -0,0 +1,7386 @@ +{ + "Framework": "CSA-CCM", + "Name": "CSA Cloud Controls Matrix (CCM) v4.0.13", + "Version": "4.0", + "Provider": "GCP", + "Description": "The Cloud Security Alliance (CSA) Cloud Controls Matrix (CCM) is a cybersecurity control framework for cloud computing, composed of 197 control objectives structured in 17 domains covering all key aspects of cloud technology. The CCM can be used as a tool for the systematic assessment of a cloud implementation, and provides guidance on which security controls should be implemented by which actor within the cloud supply chain.", + "Requirements": [ + { + "Id": "A&A-02", + "Description": "Conduct independent audit and assurance assessments according to relevant standards at least annually.", + "Name": "Independent Assessments", + "Attributes": [ + { + "Section": "Audit & Assurance", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC4.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AAC-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.5.2", + "5.2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "AS1.1", + "AS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.2.1", + "27002: 18.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.35", + "27001: A.5.36" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-2", + "CA-2(1)", + "CA-2(2)", + "CA-7", + "CA-7(1)" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-01" + ] + } + ] + } + ], + "Checks": [ + "iam_audit_logs_enabled" + ] + }, + { + "Id": "A&A-04", + "Description": "Verify compliance with all relevant standards, regulations, legal/contractual, and statutory requirements applicable to the audit.", + "Name": "Requirements Compliance", + "Attributes": [ + { + "Section": "Audit & Assurance", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC3.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-01", + "GRM-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "7.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "AS1.1", + "AS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.3.2", + "27001: A.18.2.2", + "27002: 18.2.2", + "27001: A.18.2.3", + "27002: 18.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.3.2", + "27001: A.5.31", + "27001: A.5.32", + "27001: A.5.33", + "27001: A.5.34", + "27001: A.5.36" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-1" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-3", + "DE.DP-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-01" + ] + } + ] + } + ], + "Checks": [ + "iam_audit_logs_enabled", + "iam_cloud_asset_inventory_enabled" + ] + }, + { + "Id": "AIS-04", + "Description": "Define and implement a SDLC process for application design, development, deployment, and operation in accordance with security requirements defined by the organization.", + "Name": "Secure Application Design and Development", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AIS-01", + "AIS-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.4", + "5.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.1.1", + "27002: 14.1.1", + "27017: 14.1.1", + "27001: A.14.1.2", + "27002: 14.1.2", + "27017: 14.1.2", + "27001: A.14.2.1", + "27002: 14.2.1", + "27017: 14.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.8", + "27001: A.8.25", + "27001: A.8.26", + "27001: A.8.28" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-2", + "PL-8", + "PL-8(1)", + "SA-3", + "SA-3(1)", + "SA-4", + "SA-4(2)", + "SA-4(3)", + "SA-4(8)", + "SA-4(9)", + "SA-5", + "SA-8", + "SA-8(1)-(7)", + "SA-8(9)-(13)", + "SA-8(15)-(20)", + "SA-8(22)", + "SA-8(24)-(28)", + "SA-8(30)-(33)", + "SA-17", + "SA-17(1)-(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-6", + "PR.DS-7", + "PR.IP-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "PR.IR-01", + "PR.PS-01", + "PR.PS-02", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.1", + "6.2.3", + "6.5.2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "AIS-05", + "Description": "Implement a testing strategy, including criteria for acceptance of new information systems, upgrades and new versions, which provides application security assurance and maintains compliance while enabling organizational speed of delivery goals. Automate when applicable and possible.", + "Name": "Automated Application Security Testing", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AIS-01", + "AIS-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.12", + "16.13" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD2.3", + "SD2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.2.8", + "27001: A.14.2.9", + "27001: A.12.1.2", + "27002: 12.1.2", + "27001: A.14.1.1", + "27002: 14.1.1", + "27001: A.14.2.2", + "27002: 14.2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.25", + "27001: A.8.29", + "27001: A.8.32", + "27002: 8.25 (e)", + "27002: 8.32 (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SA-11", + "SA-11(1)-(9)", + "SI-6", + "SI-6(2)", + "SI-6(3)", + "SI-10", + "SI-10(1)-(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.PT-3", + "PR.IP-12", + "DE.CM-8" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "ID.RA-01", + "PR.PS-01", + "PR.PS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A.3.2.2", + "A.3.2.2.1", + "6.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.4", + "6.4.1", + "6.4.2", + "6.5.1" + ] + } + ] + } + ], + "Checks": [ + "gcr_container_scanning_enabled", + "artifacts_container_analysis_enabled" + ] + }, + { + "Id": "AIS-07", + "Description": "Define and implement a process to remediate application security vulnerabilities, automating remediation when possible.", + "Name": "Application Vulnerability Remediation", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.1", + "CC7.4", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.2", + "16.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27001: A.12.6.1", + "27002: 12.6.1", + "27017: 12.6.1", + "27018: 12.6.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.26", + "27001: A.8.8", + "27002: 5.26 (j)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SI-2", + "SI-2(2)-(6)", + "SA-11", + "SA-11(2)", + "SA-15", + "SA-15(1)-(3)", + "SA-15(5)-(8)", + "SA-15(10)-(12)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.IP-12", + "DE.CM-8", + "RS.AN-5", + "RS.MI-3", + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "ID.RA-01", + "ID.RA-06", + "ID.RA-08", + "PR.PS-02", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.2", + "6.5", + "6.5.1-10" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "11.3.1", + "11.3.1.1" + ] + } + ] + } + ], + "Checks": [ + "gcr_container_scanning_enabled", + "artifacts_container_analysis_enabled" + ] + }, + { + "Id": "BCR-08", + "Description": "Periodically backup data stored in the cloud. Ensure the confidentiality, integrity and availability of the backup, and verify data restoration from backup for resiliency.", + "Name": "Backup", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "A1.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "11.1", + "11.2", + "11.3", + "11.4", + "11.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8", + "5.2.9" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.3", + "27017: 12.3", + "27018: 12.3.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.13", + "27001: A.5.23", + "27001: A.5.30", + "27002: 8.13", + "27002: 5.23 2nd (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-4", + "CP-4(4)", + "CP-6", + "CP-6(1)-(3)", + "CP-9", + "CP-9(1)", + "CP-9(2)", + "CP-10", + "CP-10(2)", + "CP-10(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-4", + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-11", + "RC.RP-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "9.5.1", + "12.10.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1", + "10.3.3" + ] + } + ] + } + ], + "Checks": [ + "cloudsql_instance_automated_backups", + "cloudstorage_bucket_versioning_enabled", + "cloudstorage_bucket_soft_delete_enabled" + ] + }, + { + "Id": "BCR-09", + "Description": "Establish, document, approve, communicate, apply, evaluate and maintain a disaster response plan to recover from natural and man-made disasters. Update the plan at least annually or upon significant changes.", + "Name": "Disaster Response Plan", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "CC3.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8", + "5.2.9", + "1.6.1", + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "BC1.4", + "BC2.1", + "BC2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.29", + "27001: A.5.30", + "27002: 5.29", + "27002: 5.30" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2(1)", + "CP-2(2)", + "CP-2(3)", + "CP-2(5)", + "CP-2(6)", + "CP-2(7)", + "CP-2(8)", + "PE-13", + "PE-13(1)", + "PE-13(2)", + "PE-13(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-9", + "PR.IP-10", + "RC.IM-1", + "RC.IM-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-04" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "BCR-11", + "Description": "Supplement business-critical equipment with redundant equipment independently located at a reasonable minimum distance in accordance with applicable industry standards.", + "Name": "Equipment Redundancy", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "No", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "CC3.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-06" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "BC1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.20", + "27001: A.7.11", + "27001: A.8.14", + "27002: 5.20 (t)", + "27002: 8.14 (c)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2", + "CP-2(2)", + "CP-4(3)", + "CP-6", + "CP-6(1)", + "CP-7", + "CP-8", + "CP-8(1)-(3)", + "CP-9", + "CP-9(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.BE-4", + "ID.BE-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.OC-04", + "GV.OC-05", + "PR.IR-03" + ] + } + ] + } + ], + "Checks": [ + "compute_instance_automatic_restart_enabled", + "compute_instance_group_autohealing_enabled", + "compute_instance_group_load_balancer_attached", + "compute_instance_group_multiple_zones", + "compute_instance_on_host_maintenance_migrate" + ] + }, + { + "Id": "CCC-04", + "Description": "Restrict the unauthorized addition, removal, update, and management of organization assets.", + "Name": "Unauthorized Change Protection", + "Attributes": [ + { + "Section": "Change Control and Configuration Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "CCC-04" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.1", + "1.3.4", + "5.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.4", + "SM2.6" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.1.4", + "27002: 12.1.4", + "27001: A.12.4.2", + "27002: 12.4.2", + "27001: A.14.2.2", + "27017: 14.2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.3", + "27001: A.8.4", + "27001: A.8.15", + "27001: A.8.31", + "27001: A.8.32" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-7", + "CA-7(4)", + "CM-3", + "CM-3(1)", + "CM-3(5)", + "CM-3(7)", + "CM-3(8)", + "CM-5", + "CM-5(1)", + "CM-5(4)", + "CM-5(5)", + "CM-6", + "CM-6(1)", + "CM-6(2)", + "CM-7", + "CM-7(1)", + "CM-7(4)", + "CM-7(5)", + "CM-7(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-1", + "ID.AM-2", + "ID.AM-4", + "PR.MA-1", + "PR.MA-2", + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-01", + "ID.AM-02", + "ID.AM-04", + "ID.AM-08", + "PR.PS-02", + "PR.PS-03", + "PR.PS-05", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.5.1", + "6.5.2" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_log_retention_policy_lock", + "iam_audit_logs_enabled", + "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", + "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled" + ] + }, + { + "Id": "CCC-07", + "Description": "Implement detection measures with proactive notification in case of changes deviating from the established baseline.", + "Name": "Detection of Baseline Deviation", + "Attributes": [ + { + "Section": "Change Control and Configuration Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-01" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.5.1", + "1.5.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.2.2", + "27001: A.14.2.4", + "27001: A.12.4.1", + "27002: 12.4.1 (g)", + "27001: A.5.1.1", + "27017: 5.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.9", + "27001: A.8.15", + "27002: 8.9" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-6", + "CM-6(2)", + "SI-2", + "SI-2(2)-(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.MA-1", + "PR.IP-1", + "DE.DP-4", + "PR.IP-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-01", + "DE.CM-09", + "DE.AE-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4.5.3", + "6.4.5.4", + "11.5", + "11.5.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "11.5.2", + "11.6.1" + ] + } + ] + } + ], + "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_compute_configuration_changes_enabled", + "logging_log_metric_filter_and_alert_for_custom_role_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_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": "CEK-03", + "Description": "Provide cryptographic protection to data at-rest and in-transit, using cryptographic libraries certified to approved standards.", + "Name": "Data Encryption", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-03", + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.6", + "3.1", + "3.11", + "11.3", + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.1", + "27001: A.18.1.2", + "27001: A.18.1.3", + "27001: A.18.1.4", + "27001: A.18.1.5", + "27001: A.10.1", + "27002: 10.1", + "27001: A.13.2.1", + "27002: 13.2.1", + "27001: A.18", + "27002: 18", + "27001: A.14.1.2", + "27002: 14.1.2", + "27001: A.14.1.3", + "27002 14.1.3 c)", + "27001 - A.10.1.1", + "27017 - 10.1.1", + "27001 - A.10.1.2", + "27017 - 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.8.24", + "27002: 8.24 Other Information (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-19", + "AC-19(5)", + "SC-8", + "SC-8(1)", + "SC-8(3)", + "SC-8(4)", + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-28", + "SC-28(1)-(3)", + "SI-4", + "SI-4(10)", + "SI-7", + "SI-7(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "Requirement 3", + "2.2.3", + "2.3", + "3.4", + "3.5.3", + "4.1", + "8.2.1", + "PCI Glossary - Strong Cryptography" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.7", + "3.5.1", + "4.2.1", + "4.2.1.2", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "compute_instance_encryption_with_csek_enabled", + "compute_instance_confidential_computing_enabled", + "bigquery_dataset_cmk_encryption", + "bigquery_table_cmk_encryption", + "cloudsql_instance_ssl_connections", + "dataproc_encrypted_with_cmks_disabled" + ] + }, + { + "Id": "CEK-04", + "Description": "Use encryption algorithms that are appropriate for data protection, considering the classification of data, associated risks, and usability of the encryption technology.", + "Name": "Encryption Algorithm", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.2", + "27001: 6.1.3", + "27001: A.8.2", + "27002: 8.2", + "27001: A.8.3", + "27001: A.10.1.1", + "27002: 10.1.1 (b)", + "27001: A.10.1.2", + "27002: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.2", + "27001: 6.1.3", + "27001: A.8.24", + "27001: A.5.12", + "27001: A.5.13", + "27002: 8.24 General (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2", + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02", + "ID.AM-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A2", + "Requirement 3", + "2.3", + "2.2.3", + "3.4", + "3.5.3", + "4.1", + "8.2.1", + "PCI Glossary - Strong Cryptography" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.7", + "3.5.1", + "4.2.1", + "4.2.1.2", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "dns_rsasha1_in_use_to_key_sign_in_dnssec", + "dns_rsasha1_in_use_to_zone_sign_in_dnssec" + ] + }, + { + "Id": "CEK-08", + "Description": "CSPs must provide the capability for CSCs to manage their own data encryption keys.", + "Name": "CSC Key Management Capability", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2", + "SC2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1", + "27017: 10.1", + "27001: A.10.1.1", + "27017: 10.1.1", + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.23", + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-9", + "CP-9(8)", + "SA-9", + "SA-9(6)", + "SC-12", + "SC-12(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.SC-3", + "ID.AM-6", + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.SC-05" + ] + } + ] + } + ], + "Checks": [ + "bigquery_dataset_cmk_encryption", + "bigquery_table_cmk_encryption", + "compute_instance_encryption_with_csek_enabled", + "dataproc_encrypted_with_cmks_disabled", + "kms_key_not_publicly_accessible", + "kms_key_rotation_enabled" + ] + }, + { + "Id": "CEK-10", + "Description": "Generate Cryptographic keys using industry accepted cryptographic libraries specifying the algorithm strength and the random number generator used.", + "Name": "Key Generation", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2", + "TS2.3", + "SY1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27002: 10.1.1 (e)", + "27017: 10.1.1", + "27001: A.10.1.2", + "27002: 10.1.2", + "27002: 10.1.2 (a)", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24", + "27002: 8.24 (d), Key management (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.2.3", + "3.6.1", + "PCI Glossary - Cryptographic Key Generation" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1", + "3.6.1.1", + "3.7.1" + ] + } + ] + } + ], + "Checks": [ + "bigquery_dataset_cmk_encryption", + "bigquery_table_cmk_encryption", + "dataproc_encrypted_with_cmks_disabled" + ] + }, + { + "Id": "CEK-12", + "Description": "Rotate cryptographic keys in accordance with the calculated cryptoperiod, which includes provisions for considering the risk of information disclosure and legal and regulatory requirements.", + "Name": "Key Rotation", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27017: 10.1.1", + "27001: A.10.1.2", + "27002: 10.1.2 e)", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.31", + "27001: A.8.24", + "27002: 5.31 Cryptography", + "27002: 8.24 Key management (e,m)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "ID.GV-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05", + "GV.OC-03" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.7.4", + "3.7.5" + ] + } + ] + } + ], + "Checks": [ + "kms_key_rotation_enabled", + "iam_sa_user_managed_key_rotate_90_days", + "apikeys_key_rotated_in_90_days" + ] + }, + { + "Id": "CEK-14", + "Description": "Define, implement and evaluate processes, procedures and technical measures to destroy keys stored outside a secure environment and revoke keys stored in Hardware Security Modules (HSMs) when they are no longer needed, which include provisions for legal and regulatory requirements.", + "Name": "Key Destruction", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27017: 10.1.1", + "27017: 10.1.2", + "27001: A.10.1.2", + "27002: 10.1.2 (j)", + "27001: A.18.1.3", + "27002: 18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.31", + "27001: A.8.24", + "27002: 5.31 Cryptography", + "27002: 8.24 Key management (j,m)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.IP-6", + "ID.GV-3", + "PR.DS-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05", + "ID.AM-08", + "GV.OC-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.6.4", + "3.6.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.7.4", + "3.7.5" + ] + } + ] + } + ], + "Checks": [ + "iam_role_kms_enforce_separation_of_duties" + ] + }, + { + "Id": "DCS-06", + "Description": "Catalogue and track all relevant physical and logical assets located at all of the CSP's sites within a secured system.", + "Name": "Assets Cataloguing and Tracking", + "Attributes": [ + { + "Section": "Datacenter Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DCS - 01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "1.1", + "2.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SM2.6" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.1.1", + "27002: 8.1.1", + "27017: 8.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.9" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-8", + "CM-8(1)", + "CM-8(2)", + "CM-8(4)", + "CM-8(7)", + "CM-8(8)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-1", + "ID.AM-2", + "ID.AM-4", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-01", + "ID.AM-02", + "ID.AM-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.4", + "9.7.1", + "9.9.1", + "9.9.1.a", + "9.9.1.b", + "9.9.1.c", + "12.3.3", + "12.3.4" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1.1", + "6.3.2", + "9.4.2", + "9.4.3", + "12.5.1" + ] + } + ] + } + ], + "Checks": [ + "iam_cloud_asset_inventory_enabled" + ] + }, + { + "Id": "DSP-02", + "Description": "Apply industry accepted methods for the secure disposal of data from storage media such that data is not recoverable by any forensic means.", + "Name": "Secure Disposal", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3", + "CC6.4", + "CC6.5", + "CC6.7", + "P4.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DSI-07" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.3.3", + "7.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.3.2", + "27002: 8.3.2", + "27001: A.11.2.7", + "27002: 11.2.7" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.7.10", + "27001: A.7.14", + "27001: A.8.10", + "27002: 7.10 (Secure reuse or disposal)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-22", + "SI-12", + "SI-12(3)", + "SI-18", + "SI-18(1)", + "SI-18(4)", + "SI-18(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.SC-10", + "PR.PS-02", + "PR.PS-03", + "ID.AM-08" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.1", + "9.8", + "9.8.1", + "9.8.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1", + "3.7.5", + "9.4.7" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_lifecycle_management_enabled" + ] + }, + { + "Id": "DSP-03", + "Description": "Create and maintain a data inventory, at least for any sensitive data and personal data.", + "Name": "Data Inventory", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1", + "1.3.2", + "1.3.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.1.1", + "27002: 8.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.9", + "27001: A.8.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-12", + "CM-12(1)", + "PM-5", + "PM-5(1)", + "SI-12", + "SI-12(1)", + "SI-19", + "SI-19(1)", + "SI-19(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1", + "9.4.5" + ] + } + ] + } + ], + "Checks": [ + "iam_cloud_asset_inventory_enabled", + "iam_audit_logs_enabled" + ] + }, + { + "Id": "DSP-04", + "Description": "Classify data according to its type and sensitivity level.", + "Name": "Data Classification", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "C1.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DSI-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.7" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1", + "1.3.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.2.1", + "27002: 8.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-16", + "AC-16(9)", + "PM-22", + "PM-23", + "PT-2", + "PT-2(1)", + "SI-18", + "SI-18(2)", + "SI-19", + "SI-19(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-05", + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "9.6.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "9.4.2", + "9.4.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "DSP-07", + "Description": "Develop systems, products, and business practices based upon a principle of security by design and industry best practices.", + "Name": "Data Protection by Design and Default", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "PI1.2", + "PI1.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.3.1", + "5.3.2", + "5.3.3", + "5.3.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD2.2", + "IM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.1.1", + "27002:14.1.1", + "27001: A.14.2.5", + "27002:14.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.27", + "27001: A.8.28", + "27001: A.8.29", + "27002: 5.8 (Information security requirements a-i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-17", + "PM-24", + "PM-25", + "PT-2", + "PT-2(2)", + "SA-3", + "SA-4", + "SA-5", + "SA-8", + "SA-8(9)", + "SA-8(13)", + "SA-8(18)", + "SA-8(20)", + "SA-8(22)", + "SA-8(23)", + "SA-8(33)", + "SA-15", + "SA-15(12)", + "SC-3", + "SC-3(3)", + "SC-7", + "SC-7(24)", + "SC-8", + "SC-8(1)-(4)", + "SC-28", + "SC-28(1)", + "SI-12", + "SI-12(1)-(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.PT-3", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.1" + ] + } + ] + } + ], + "Checks": [ + "bigquery_dataset_public_access", + "cloudsql_instance_public_access", + "cloudsql_instance_public_ip", + "cloudstorage_bucket_public_access", + "compute_image_not_publicly_shared", + "compute_instance_public_ip", + "kms_key_not_publicly_accessible" + ] + }, + { + "Id": "DSP-10", + "Description": "Define, implement and evaluate processes, procedures and technical measures that ensure any transfer of personal or sensitive data is protected from unauthorized access and only processed within scope as permitted by the respective laws and regulations.", + "Name": "Sensitive Data Transfer", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-02", + "EKM-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.1", + "3.12", + "3.13" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "9.5.1", + "9.5.2", + "9.5.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.4", + "IM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.13.2.1", + "27002: 13.2.1", + "27001: A.8.3.3", + "27002: 8.3.3", + "27001: A.13.2.3", + "27002: 13.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.7.10" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-4", + "AC-4(23)-(25)", + "CA-3", + "CA-3(6)", + "CA-6", + "CA-6(1)", + "CA-6(2)", + "SC-4", + "SC-4(2)", + "SC-7", + "SC-7(10)", + "SC-7(24)", + "SC-8", + "SC-8(1)-(5)", + "SC-16", + "SC-16(1)-(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2", + "PR.DS-5", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02", + "PR.IR-01", + "ID.AM-03", + "GV.OC-03", + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "4.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "4.1.1", + "4.2.1", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "cloudsql_instance_ssl_connections" + ] + }, + { + "Id": "DSP-16", + "Description": "Data retention, archiving and deletion is managed in accordance with business requirements, applicable laws and regulations.", + "Name": "Data Retention and Deletion", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "C1.1", + "C1.2", + "CC3.1", + "P4.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-02", + "BCR-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.4", + "3.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.3.1", + "7.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.33", + "27001: A.8.10", + "27002: 5.33 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SI-12", + "SI-12(1)-(3)", + "SI-18", + "SI-18(1)", + "SI-18(4)", + "SI-18(5)", + "SI-19", + "SI-19(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-3", + "PR.IP-6", + "ID.GV-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "GV.OC-03", + "GV.SC-10", + "PR.DS-11" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_lifecycle_management_enabled", + "cloudstorage_bucket_sufficient_retention_period" + ] + }, + { + "Id": "DSP-17", + "Description": "Define and implement, processes, procedures and technical measures to protect sensitive data throughout it's lifecycle.", + "Name": "Sensitive Data Protection", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSC-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.1", + "CC6.1", + "CC6.3", + "CC6.7", + "CC8.1", + "C1.1", + "P2.0", + "P3.0", + "P4.0", + "P5.0", + "P6.0" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.1", + "3.1", + "3.14" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.3.3", + "9.1.1", + "9.2.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3", + "27002: 18.1.3", + "27001:A.18.1.4", + "27002:18.1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.11", + "27001: A.8.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-2", + "PM-22", + "PM-24", + "PT-7", + "PT-7(1)", + "PT-7(2)", + "PT-8", + "SC-8", + "SC-8(1)-(5)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2", + "PR.DS-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02", + "PR.DS-10" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.0 (including all subsections)", + "4.0 (including all subsections)" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.1.1", + "4.1.1" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_public_access", + "bigquery_dataset_public_access", + "cloudsql_instance_public_access", + "cloudsql_instance_public_ip", + "compute_instance_public_ip", + "compute_image_not_publicly_shared", + "kms_key_not_publicly_accessible" + ] + }, + { + "Id": "GRC-05", + "Description": "Develop and implement an Information Security Program, which includes programs for all the relevant domains of the CCM.", + "Name": "Information Security Program", + "Attributes": [ + { + "Section": "Governance, Risk and Compliance", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "14.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SG2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 4.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-1", + "PM-3", + "PM-14", + "PL-2", + "PM-18", + "PM-31" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.4.1", + "A.3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.4.1", + "A3.1.1" + ] + } + ] + } + ], + "Checks": [ + "iam_account_access_approval_enabled", + "iam_audit_logs_enabled", + "iam_organization_essential_contacts_configured" + ] + }, + { + "Id": "IAM-02", + "Description": "Establish, document, approve, communicate, implement, apply, evaluate and maintain strong password policies and procedures. Review and update the policies and procedures at least annually.", + "Name": "Strong Password Policy and Procedures", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-12", + "GRM-06", + "GRM-09" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.1.1", + "1.5.1", + "4.1.2", + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1", + "SA1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5", + "27002: 5", + "27001: A.9.4.3", + "27002: 9.4.3", + "27017: 9.4.3", + "27018: 9.4.3", + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27001: A.7.2.2", + "27002: 7.2.2", + "27001: A.9.2.6", + "27002: 9.2.6", + "27001: A.9.2.3", + "27002: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5.1", + "27001: A.5.4", + "27001: A.5.17", + "27001: A.6.3", + "27001: A.8.5", + "27001: A.5.37" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(3)", + "AC-2(11)", + "AC-3", + "AC-3(3)", + "AC-12", + "AC-12(1)", + "IA-2", + "IA-2(10)", + "IA-5", + "IA-5(1)", + "IA-5(18)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "PR.AC-1", + "PR.AC-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.PO-01", + "GV.PO-02", + "ID.IM-03", + "PR.AA-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.4", + "12.1", + "12.1.1", + "12.11" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.1.1", + "8.3.8" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "IAM-03", + "Description": "Manage, store, and review the information of system identities, and level of access.", + "Name": "Identity Inventory", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-04", + "IAM-08", + "IAM-10" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1", + "5.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.2 (c)", + "27001: A.8.1.1", + "27002: 8.1.1", + "27001: A.9.4.1", + "27002: 9.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.2 (c)", + "27001: A.5.15", + "27001: A.5.16", + "27001: A.5.18", + "27001: A.7.4", + "27001: A.8.15", + "27001: A.8.2", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-10", + "AU-10(1)", + "AU-10(2)", + "AU-16", + "AU-16(1)", + "IA-4", + "IA-4(8)", + "IA-4(9)", + "IA-5", + "IA-5(5)", + "IA-8", + "IA-8(4)", + "PM-5(1)", + "SA-8", + "SA-8(22)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-04", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.4.a" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.5", + "7.2.5.1" + ] + } + ] + } + ], + "Checks": [ + "iam_sa_user_managed_key_unused", + "iam_service_account_unused" + ] + }, + { + "Id": "IAM-04", + "Description": "Employ the separation of duties principle when implementing information system access.", + "Name": "Separation of Duties", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC1.3", + "CC5.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.2.2", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.6.1.2", + "27002: 6.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.18", + "27001: A.5.3", + "27001: A.8.2" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(3)", + "AC-2(11)", + "AC-6", + "AC-6(1)-(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4", + "6.4.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.5.3", + "6.5.4", + "7.2.1", + "7.2.2" + ] + } + ] + } + ], + "Checks": [ + "iam_role_sa_enforce_separation_of_duties", + "iam_role_kms_enforce_separation_of_duties", + "iam_no_service_roles_at_project_level" + ] + }, + { + "Id": "IAM-05", + "Description": "Employ the least privilege principle when implementing information system access.", + "Name": "Least Privilege", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-06", + "IVS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.1.1", + "27002: 9.1.1", + "27001: A.9.1.2", + "27002: 9.1.2", + "27001: A.9.2.3", + "27002: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.8.2", + "27002: 5.15 (Other information 2nd (a))" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(4)", + "IA-12", + "IA-12(2)", + "IA-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "7.1", + "7.1.1", + "7.1.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "7.2.2", + "7.2.5", + "7.2.6" + ] + } + ] + } + ], + "Checks": [ + "apikeys_api_restrictions_configured", + "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" + ] + }, + { + "Id": "IAM-07", + "Description": "De-provision or respectively modify access of movers / leavers or system identity changes in a timely manner in order to effectively adopt and communicate identity and access management policies.", + "Name": "User Access Changes and Revocation", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.3", + "6.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.18" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(1)", + "AC-2(2)", + "AC-2(6)", + "AC-2(8)", + "AC-3", + "AC-3(8)", + "AC-6", + "AC-6(7)", + "AU-10", + "AU-10(4)", + "AU-16", + "AU-16(1)", + "CM-7", + "CM-7(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4", + "PR.IP-11" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.RR-04", + "GV.SC-10", + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1.2", + "8.1.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.2.5", + "8.2.6" + ] + } + ] + } + ], + "Checks": [ + "iam_sa_user_managed_key_unused", + "iam_service_account_unused" + ] + }, + { + "Id": "IAM-08", + "Description": "Review and revalidate user access for least privilege and separation of duties with a frequency that is commensurate with organizational risk tolerance.", + "Name": "User Access Review", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-10" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.5", + "27001: A.9.2.6", + "27001: A.9.4.1", + "27017: 9.4.1", + "27001: A.6.1.2", + "27001: A 9.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.3", + "27001: A.5.18", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(4)", + "AC-6(8)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.5.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.5.1", + "7.2.5", + "7.2.4" + ] + } + ] + } + ], + "Checks": [ + "iam_sa_user_managed_key_unused", + "iam_service_account_unused", + "iam_sa_user_managed_key_rotate_90_days" + ] + }, + { + "Id": "IAM-09", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the segregation of privileged access roles such that administrative access to data, encryption and key management capabilities and logging capabilities are distinct and separated.", + "Name": "Segregation of Privileged Access Roles", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.1", + "CC6.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.3", + "27002: 9.2.3", + "27017: 9.2.3", + "27018: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.2", + "27001: A.8.18", + "27002: 8.2 (j)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-3(7)", + "AC-6(4)", + "AC-6(8)", + "IA-5", + "IA-5(6)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.3", + "3.5.2", + "7.1.2", + "7.1.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1", + "3.7.6", + "6.5.3", + "6.5.4", + "7.2.1", + "7.2.2", + "10.3.1" + ] + } + ] + } + ], + "Checks": [ + "iam_role_kms_enforce_separation_of_duties", + "iam_role_sa_enforce_separation_of_duties", + "iam_sa_no_administrative_privileges" + ] + }, + { + "Id": "IAM-10", + "Description": "Define and implement an access process to ensure privileged access roles and rights are granted for a time limited period, and implement procedures to prevent the culmination of segregated privileged access.", + "Name": "Management of Privileged Access Roles", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1", + "6.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.3", + "27002: 9.2.3", + "27017: 9.2.3", + "27018: 9.2.3", + "27001: A.9.4.4", + "27002: 9.4.4", + "27017: 9.4.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.2", + "27001: A.8.18", + "27002: 8.2 (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(7)", + "AC-3", + "AC-3(4)", + "AC-3(11)", + "AC-3(13)", + "AC-3(14)", + "AC-6", + "AC-6(4)", + "AC-6(5)", + "AC-6(8)", + "AC-12", + "AC-12(3)", + "AC-17", + "AC-17(4)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "7.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "7.2.2" + ] + } + ] + } + ], + "Checks": [ + "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": "IAM-12", + "Description": "Define, implement and evaluate processes, procedures and technical measures to ensure the logging infrastructure is read-only for all with write access, including privileged access roles, and that the ability to disable it is controlled through a procedure that ensures the segregation of duties and break glass procedures.", + "Name": "Safeguard Logs Integrity", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.3" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1", + "27018: 12.4.1", + "27001: A.12.4.2", + "27002: 12.4.2", + "27017: 12.4.2", + "27018: 12.4.2", + "27001: A.12.4.3", + "27002: 12.4.3", + "27017: 12.4.3", + "27018: 12.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15", + "27001: A.8.18", + "27002: 8.15 Protection of Logs" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(11)", + "AC-2(12)", + "IA-8", + "IA-8(4)", + "SA-8", + "SA-8(22)", + "SC-34", + "SC-34(1)", + "SC-34(2)", + "SC-36", + "SI-4", + "SI-4(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_log_retention_policy_lock", + "cloudstorage_bucket_logging_enabled", + "logging_sink_created" + ] + }, + { + "Id": "IAM-13", + "Description": "Define, implement and evaluate processes, procedures and technical measures that ensure users are identifiable through unique IDs or which can associate individuals to the usage of user IDs.", + "Name": "Uniquely Identifiable Users", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.1", + "27002: 9.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.16" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-3", + "AC-3(14)", + "AC-24", + "AC-24(2)", + "AU-10", + "AU-10(1)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(12)", + "IA-4", + "IA-4(1)", + "SA-8", + "SA-8(22)", + "SC-23", + "SC-23(3)", + "SC-40(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1", + "8.2", + "8.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.2.1", + "8.2.2", + "8.2.4" + ] + } + ] + } + ], + "Checks": [ + "compute_project_os_login_enabled" + ] + }, + { + "Id": "IAM-14", + "Description": "Define, implement and evaluate processes, procedures and technical measures for authenticating access to systems, application and data assets, including multifactor authentication for at least privileged user and sensitive data access. Adopt digital certificates or alternatives which achieve an equivalent level of security for system identities.", + "Name": "Strong Authentication", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.3", + "6.5", + "12.5", + "12.7" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3", + "SA1.4", + "SA1.8" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.1.2", + "27002: 9.1.2", + "27017: 9.1.2", + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27001: A.9.4.2", + "27002: 9.4.2", + "27017: 9.4.2", + "27018: 9.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.17", + "27001: A.8.5", + "27001: A.8.24", + "27002: 8.5", + "27002: 8.24 other information (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(5)", + "AC-7", + "AC-7(4)", + "AU-10", + "AU-10(2)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(8)", + "IA-2(12)", + "IA-3", + "IA-3(1)", + "IA-5", + "IA-5(2)", + "IA-5(7)", + "IA-5(9)", + "IA-5(10)", + "IA-5(12)", + "IA-5(14)-(16)", + "IA-8", + "IA-8(1)", + "IA-8(6)", + "SC-23", + "SC-23(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6", + "PR.AC-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1.2", + "8.1.3", + "8.1.6", + "8.2", + "8.3", + "8.3.2", + "12.3.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "8.3.1", + "8.3.2", + "8.4.1", + "8.4.2", + "8.4.3" + ] + } + ] + } + ], + "Checks": [ + "compute_project_os_login_2fa_enabled", + "compute_project_os_login_enabled" + ] + }, + { + "Id": "IAM-15", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the secure management of passwords.", + "Name": "Passwords Management", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27018: 9.2.4", + "27001: A.9.3.1", + "27002: 9.3.1", + "27017: 9.3.1", + "27018: 9.3.1", + "27001: A.9.4.3", + "27002: 9.4.3", + "27017: 9.4.3", + "27018: 9.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.17" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IA-4", + "IA-4(8)", + "IA-5", + "IA-5(1)", + "IA-5(8)", + "IA-5(18)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.2", + "8.2.1-6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.2", + "2.3.1", + "8.3.5", + "8.3.6", + "8.3.7", + "8.3.8", + "8.3.9", + "8.3.10", + "8.3.10.1", + "8.6.2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "IAM-16", + "Description": "Define, implement and evaluate processes, procedures and technical measures to verify access to data and system functions is authorized.", + "Name": "Authorization Mechanisms", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3", + "SA1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.5", + "27002: 9.2.5", + "27017: 9.2.5", + "27018: 9.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.18" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-3", + "AC-3(5)", + "AC-4", + "AC-4(17)", + "AC-4(21)", + "AC-4(22)", + "AC-6", + "AC-6(8)", + "AC-6(9)", + "AC-12", + "AC-12(1)", + "AC-20", + "AC-20(1)", + "AU-10", + "AU-10(1)", + "AU-10(2)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(12)", + "IA-3", + "IA-3(1)", + "IA-5(1)", + "IA-5(2)", + "IA-5(5)", + "IA-5(8)", + "IA-5(10)", + "IA-5(12)", + "IA-8", + "IA-8(1)", + "IA-8(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4", + "PR.AC-6", + "PR.AC-7", + "PR.PT-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-03", + "PR.AA-04", + "PR.AA-05", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.3", + "7.1.4" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.4", + "7.2.3", + "7.2.5.1" + ] + } + ] + } + ], + "Checks": [ + "apikeys_api_restrictions_configured", + "cloudstorage_bucket_uniform_bucket_level_access", + "compute_instance_default_service_account_in_use_with_full_api_access", + "iam_sa_no_administrative_privileges" + ] + }, + { + "Id": "IPY-03", + "Description": "Implement cryptographically secure and standardized network protocols for the management, import and export of data.", + "Name": "Secure Interoperability and Portability Management", + "Attributes": [ + { + "Section": "Interoperability & Portability", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IPY-04" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY1.1", + "SY1.2", + "NC1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1", + "27001: A.15.1.1", + "27002: 15.1.1", + "27017: 15.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.19", + "27001: A.5.23", + "27001: A.5.31", + "27001: A.5.32", + "27001: A.5.33", + "27001: A.5.34" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PT-2", + "PT-2(2)", + "SA-4", + "SC-16", + "SC-16(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.2.1", + "1.2.5", + "1.2.6", + "2.2.4", + "2.2.5", + "2.2.7", + "4.2.1" + ] + } + ] + } + ], + "Checks": [ + "cloudsql_instance_ssl_connections" + ] + }, + { + "Id": "IVS-02", + "Description": "Plan and monitor the availability, quality, and adequate capacity of resources in order to deliver the required system performance as determined by the business.", + "Name": "Capacity and Resource Planning", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "No", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-04" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.3", + "27001: 6.1", + "27001: 9.1", + "27001: A.12.1.3", + "27002: 12.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.3 (b)", + "27001: 6.1", + "27001: 9.1", + "27001: A.8.6", + "27001: A.8.14" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2", + "CP-2(2)", + "SC-5", + "SC-5(2)", + "SC-4", + "SI-4" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-4", + "ID.BE-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.IR-04", + "GV.OC-04" + ] + } + ] + } + ], + "Checks": [ + "compute_instance_group_autohealing_enabled", + "compute_instance_group_load_balancer_attached", + "compute_instance_group_multiple_zones" + ] + }, + { + "Id": "IVS-03", + "Description": "Monitor, encrypt and restrict communications between environments to only authenticated and authorized connections, as justified by the business. Review these configurations at least annually, and support them by a documented justification of all allowed services, protocols, ports, and compensating controls.", + "Name": "Network Security", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-06" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.8", + "3.1", + "12.2", + "13.6", + "13.9" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "NC1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.13.1.1", + "27002: 13.1.1", + "27001: A.13.1.2", + "27002: 13.1.2", + "27001: A.13.1.3", + "27002: 13.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.5.15", + "27001: A.5.37", + "27001: A.8.5", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.21", + "27001: A.8.22", + "27001: A.8.24", + "27002: A.5.15 2nd c)", + "27002: 8.20", + "27002: 8.21", + "27002: 8.22", + "27002: 8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-1", + "SC-4", + "SC-7", + "SC-7(4)", + "SC-7(5)", + "SC-7(8)", + "SC-7(9)", + "SC-7(11)", + "SC-8", + "SC-8(1)", + "SC-11", + "SC-12", + "SC-16", + "SC-23", + "SC-29", + "SC-29(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-5", + "PR.AC-7", + "PR.PT-4", + "DE.CM-1", + "DE.CM-7", + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.IR-01", + "PR.AA-03", + "PR.AA-05", + "DE.CM-01", + "PR.DS-02", + "ID.AM-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "1.1.6", + "1.2", + "1.2.3", + "2.2", + "4.1.1", + "10.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.2.5", + "1.2.6", + "1.2.7", + "1.4.2", + "2.2.4", + "2.2.5", + "2.2.7", + "4.2.1", + "10.1.1" + ] + } + ] + } + ], + "Checks": [ + "compute_firewall_rdp_access_from_the_internet_allowed", + "compute_firewall_ssh_access_from_the_internet_allowed", + "compute_instance_ip_forwarding_is_enabled", + "compute_network_default_in_use", + "compute_network_dns_logging_enabled", + "compute_network_not_legacy", + "compute_subnet_flow_logs_enabled" + ] + }, + { + "Id": "IVS-04", + "Description": "Harden host and guest OS, hypervisor or infrastructure control plane according to their respective best practices, and supported by technical controls, as part of a security baseline.", + "Name": "OS Hardening and Base Controls", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.8", + "CC7.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-07", + "IVS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "4.1", + "4.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3", + "5.2.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY1.1", + "SY1.3", + "SY1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.14.2.2", + "27002: 14.2.2", + "27001: A.14.2.3", + "27001 A.14.2.4", + "27018: 12.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.5.37", + "27001: A.8.5", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.22", + "27001: A.8.24", + "27002: 8.20", + "27002: 8.22", + "27002: 8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-6", + "CM-6(1)", + "SC-29", + "SC-29(1)", + "SC-2", + "SC-7", + "SC-7(12)", + "SC-30", + "SC-34", + "SC-35", + "SC-39", + "SC-44" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-1", + "PR.PT-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.1" + ] + } + ] + } + ], + "Checks": [ + "compute_instance_shielded_vm_enabled", + "compute_project_os_login_enabled", + "compute_instance_serial_ports_in_use", + "compute_instance_block_project_wide_ssh_keys_disabled" + ] + }, + { + "Id": "IVS-06", + "Description": "Design, develop, deploy and configure applications and infrastructures such that CSP and CSC (tenant) user access and intra-tenant access is appropriately segmented and segregated, monitored and restricted from other tenants.", + "Name": "Segmentation and Segregation", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-09" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.3.4", + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SC2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.1", + "27001: A.13.1.3", + "27002: 13.1.3", + "27017: 13.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.1", + "27001: A.5.15", + "27001: A.5.20", + "27001: A.8.3", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.22", + "27002: 5.15 (b)", + "27002: 8.3 (b)", + "27002: 8.16 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-3", + "SC-7", + "SC-7(20)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.AC-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.IR-01", + "PR.PS-01", + "PR.PS-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.6", + "8.3.1", + "10.8", + "11.3", + "A3.2.1", + "A3.3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "A1.1.1", + "A1.1.2", + "A1.1.3" + ] + } + ] + } + ], + "Checks": [ + "cloudsql_instance_private_ip_assignment", + "cloudstorage_uses_vpc_service_controls", + "compute_instance_public_ip", + "compute_network_default_in_use", + "compute_network_not_legacy" + ] + }, + { + "Id": "IVS-07", + "Description": "Use secure and encrypted communication channels when migrating servers, services, applications, or data to cloud environments. Such channels must include only up-to-date and approved protocols.", + "Name": "Migration to Cloud Environments", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-10" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.4", + "IM1.4", + "NC1.4", + "SC2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.13.1.1", + "27002: 13.1.1", + "27017: 13.1.1", + "27018: 13.1.1", + "27001: A.13.1.2", + "27002: 13.1.2", + "27017: 13.1.2", + "27018: 13.1.2", + "27001: A.13.1.3", + "27002: 13.1.3", + "27017: 13.1.3", + "27018: 13.1.3", + "27001: A.13.2.1", + "27002: 13.2.1", + "27017: 13.2.1", + "27018: 13.2.1", + "27001: A.13.2.2", + "27002: 13.2.2", + "27017: 13.2.2", + "27018: 13.2.2", + "27001: A.13.2.3", + "27002: 13.2.3", + "27017: 13.2.3", + "27018: 13.2.3", + "27001: A.13.2.4", + "27002: 13.2.4", + "27017: 13.2.4", + "27018: 13.2.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.8.20", + "27001: A.8.24", + "27002: 8.20 (e)", + "27002: 8.24 Guidance (b,f), other information (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-17", + "AC-20", + "SC-7", + "SC-7(28)", + "SC-8", + "SC-8(1)", + "SC-12", + "SC-23", + "SC-29", + "SI-7", + "SI-7(1)-(3)", + "SI-7(5)-(10)", + "SI-7(12)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "4.2.1" + ] + } + ] + } + ], + "Checks": [ + "cloudsql_instance_ssl_connections" + ] + }, + { + "Id": "IVS-09", + "Description": "Define, implement and evaluate processes, procedures and defense-in-depth techniques for protection, detection, and timely response to network-based attacks.", + "Name": "Network Defense", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.6", + "CC6.8", + "CC7.1", + "CC7.2", + "CC7.5" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-13" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "13.3", + "13.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.3", + "5.2.4", + "5.2.5", + "5.2.7", + "5.3.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "NC1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1", + "27001: 6.2", + "27001: A.14.1.2", + "27002: 14.1.2", + "27017: 14.1.2", + "27001: A.11.1.4", + "27002: 11.1.4", + "27017: 11.1.4", + "27018: 16.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1", + "27001: 6.2", + "27001: A.5.24", + "27001: A.5.26", + "27001: A.8.8", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.21", + "27001: A.8.22", + "27001: A.8.26", + "27002: 8.8 (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-8", + "PL-8(1)", + "SC-5", + "SC-5(1)", + "SC-5(3)", + "SC-7", + "SC-7(13)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.DP-1", + "DE.CM-1", + "DE.CM-7", + "PR.AC-5", + "RS.MI-2", + "PR.DS-2", + "RS.RP-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-03", + "DE.CM-01", + "PR.IR-01", + "RS.MA-01", + "RS.MI-01", + "RS.MI-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.6", + "1.1", + "1.2", + "1.3", + "1.5", + "12.10.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.1.1", + "1.3.1", + "1.3.2", + "1.3.3", + "1.4.1", + "1.4.2", + "1.4.3", + "1.4.4", + "1.4.5", + "1.5.1", + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "compute_firewall_rdp_access_from_the_internet_allowed", + "compute_firewall_ssh_access_from_the_internet_allowed", + "compute_loadbalancer_logging_enabled", + "compute_public_address_shodan", + "dns_dnssec_disabled" + ] + }, + { + "Id": "LOG-02", + "Description": "Define, implement and evaluate processes, procedures and technical measures to ensure the security and retention of audit logs.", + "Name": "Audit Logs Protection", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.1", + "8.9", + "8.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "3.1.3", + "5.1.2", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3", + "27002: 18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.28", + "27001: A.5.33", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-4", + "AU-11" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.IP-4", + "PR.IP-6", + "PR.PT-1", + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.DS-01", + "PR.DS-02", + "ID.AM-08", + "PR.DS-11", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5", + "10.7" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4", + "10.5.1" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_log_retention_policy_lock", + "cloudstorage_bucket_logging_enabled", + "logging_sink_created" + ] + }, + { + "Id": "LOG-03", + "Description": "Identify and monitor security-related events within applications and the underlying infrastructure. Define and implement a system to generate alerts to responsible stakeholders based on such events and corresponding metrics.", + "Name": "Security Monitoring and Alerting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-03", + "SEF-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4", + "5.2.7", + "1.6.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2", + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.28", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-5", + "AU-5(2)", + "AU-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.AE-2", + "DE.AE-3", + "DE.AE-5", + "DE.CM-1", + "DE.CM-2", + "DE.CM-3", + "DE.CM-4", + "DE.CM-5", + "DE.CM-6", + "DE.CM-7", + "DE.DP-1", + "DE.DP-4", + "DE.AE-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.AE-02", + "DE.AE-03", + "DE.AE-04", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1", + "10.2.2", + "10.4.1.1", + "10.4.2.1", + "10.4.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_compute_configuration_changes_enabled", + "logging_log_metric_filter_and_alert_for_custom_role_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_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": "LOG-04", + "Description": "Restrict audit logs access to authorized personnel and maintain records that provide unique access accountability.", + "Name": "Audit Logs Access and Accountability", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.14" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "3.1.1", + "4.1.2", + "4.1.3", + "4.2.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.2", + "27001: A.12.4.1", + "27002: 12.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.33", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(4)", + "AU-9(6)", + "AU-10" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.1", + "10.2.1", + "10.2.3", + "10.5.1", + "10.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1.3", + "10.3.1" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_public_access", + "cloudstorage_bucket_uniform_bucket_level_access", + "iam_audit_logs_enabled", + "kms_key_not_publicly_accessible" + ] + }, + { + "Id": "LOG-05", + "Description": "Monitor security audit logs to detect activity outside of typical or expected patterns. Establish and follow a defined process to review and take appropriate and timely actions on detected anomalies.", + "Name": "Audit Logs Monitoring and Response", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.8", + "8.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.1", + "1.6.2", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.3", + "27002: 12.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15", + "27001: A.8.16" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-6", + "AU-6(1)", + "AU-6(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-3", + "PR.PT-1", + "RS.AN-1", + "RS.CO-1.", + "DE.AE-1", + "DE.AE-5", + "DE.DP-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-03", + "PR.PS-04", + "DE.AE-02", + "DE.AE-03", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.6", + "10.6.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.4.1.1", + "10.4.2.1" + ] + } + ] + } + ], + "Checks": [ + "logging_log_metric_filter_and_alert_for_project_ownership_changes_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_compute_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": "LOG-07", + "Description": "Establish, document and implement which information meta/data system events should be logged. Review and update the scope at least annually or whenever there is a change in the threat environment.", + "Name": "Logging Scope", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5.3", + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5.3", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-1", + "AU-14", + "AU-16" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.SC-3", + "ID.SC-4", + "PR.PT-1", + "ID.GV-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1", + "10.2.2" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_audit_logs_enabled", + "cloudstorage_bucket_logging_enabled", + "compute_network_dns_logging_enabled", + "compute_subnet_flow_logs_enabled", + "iam_audit_logs_enabled" + ] + }, + { + "Id": "LOG-08", + "Description": "Generate audit records containing relevant security information.", + "Name": "Log Records", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-3", + "AU-3(1)", + "AU-3(3)", + "AU-6", + "AU-6(8)", + "AU-12", + "AU-12(1)", + "AU-12(2)", + "AU-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.PT-1", + "DE.AE-3", + "DE.CM-1", + "DE.CM-2", + "DE.CM-3", + "DE.CM-6", + "DE.CM-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.2" + ] + } + ] + } + ], + "Checks": [ + "iam_audit_logs_enabled", + "compute_subnet_flow_logs_enabled", + "compute_loadbalancer_logging_enabled", + "compute_network_dns_logging_enabled", + "cloudstorage_audit_logs_enabled", + "cloudstorage_bucket_logging_enabled", + "logging_sink_created", + "cloudsql_instance_postgres_log_connections_flag", + "cloudsql_instance_postgres_log_disconnections_flag", + "cloudsql_instance_postgres_log_statement_flag", + "cloudsql_instance_postgres_enable_pgaudit_flag" + ] + }, + { + "Id": "LOG-09", + "Description": "The information system protects audit records from unauthorized access, modification, and deletion.", + "Name": "Log Protection", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-04", + "IVS-01" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.2", + "27002: 12.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(2)", + "AU-9(3)", + "AU-9(4)", + "AU-12(3)", + "AU-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.IP-4", + "PR.IP-6", + "PR.PT-1", + "PR.DS-1", + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.DS-01", + "PR.DS-02", + "PR.DS-11" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5", + "10.5.1", + "10.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_bucket_log_retention_policy_lock", + "cloudstorage_bucket_uniform_bucket_level_access" + ] + }, + { + "Id": "LOG-10", + "Description": "Establish and maintain a monitoring and internal reporting capability over the operations of cryptographic, encryption and key management policies, processes, procedures, and controls.", + "Name": "Encryption Monitoring and Reporting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-02", + "EKM-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1", + "27002: 10.1", + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-1", + "AU-9", + "AU-9(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "PR.PT-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.1.1", + "10.2.1", + "10.4.1" + ] + } + ] + } + ], + "Checks": [ + "kms_key_rotation_enabled" + ] + }, + { + "Id": "LOG-11", + "Description": "Log and monitor key lifecycle management events to enable auditing and reporting on usage of cryptographic keys.", + "Name": "Transaction/Activity Logging", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.PT-1", + "DE.AE-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-09" + ] + } + ] + } + ], + "Checks": [ + "cloudstorage_audit_logs_enabled", + "iam_audit_logs_enabled", + "logging_sink_created" + ] + }, + { + "Id": "LOG-13", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the reporting of anomalies and failures of the monitoring system and provide immediate notification to the accountable party.", + "Name": "Failures and Anomalies Reporting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.3", + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.1", + "27002: 16.1.1", + "27001: A.16.1.2", + "27017: 16.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.24", + "27001: A.6.8", + "27002: 6.8 (g)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-5", + "AU-5(2)", + "AU-6", + "AU-6(3)", + "AU-6(4)", + "AU-6(5)", + "AU-16" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-3", + "DE.DP-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.AE-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.4.3", + "10.7.1", + "10.7.2", + "10.7.3" + ] + } + ] + } + ], + "Checks": [ + "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_project_ownership_changes_enabled" + ] + }, + { + "Id": "SEF-03", + "Description": "'Establish, document, approve, communicate, apply, evaluate and maintain a security incident response plan, which includes but is not limited to: relevant internal departments, impacted CSCs, and other business critical relationships (such as supply-chain) that may be impacted.'", + "Name": "Incident Response Plans", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2", + "CC7.3", + "CC7.4" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "17.2", + "17.4" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27017: CLD.12.1.5", + "27018: 16.1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: A.5.26", + "27002: 5.26 (e,f)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IR-1", + "IR-2", + "IR-2(1)-(3)", + "IR-3", + "IR-3(1)-(3)", + "IR-4", + "IR-4(1)-(15)", + "IR-5", + "IR-5(1)", + "IR-6", + "IR-6(1)-(3)", + "IR-7", + "IR-7(1)", + "IR-7(2)", + "IR-8", + "IR-8(1)", + "IR-9", + "IR-9(1)-(4)", + "PM-12" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "RS.CO-1", + "RS.CO-4", + "ID.AM-6", + "ID.GV-2", + "ID.SC-5", + "PR.IP-9", + "PR.IP10" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AT-01", + "PR.AT-02", + "RS.MA-01", + "GV.SC-08", + "ID.IM-02", + "ID.IM-04", + "RC.RP-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.1", + "12.10.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1", + "12.10.5" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "SEF-06", + "Description": "Define, implement and evaluate processes, procedures and technical measures supporting business processes to triage security-related events.", + "Name": "Event Triage Processes", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.4", + "27002: 16.1.4", + "27017: 16.1.4", + "27018: 16.1.4", + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27018: 16.1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.25" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-7", + "CA-7(3)", + "CA-7(4)", + "CA-7(5)", + "CA-7(6)", + "IR-4", + "IR-4(1)", + "IR-4(3)", + "IR-4(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.AE-2", + "DE.AE-4", + "RS.RP-1", + "RS.AN-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "RS.MA-02", + "RS.MA-03", + "RS.AN-03", + "DE.AE-02", + "DE.AE-04", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "RS.MI-02", + "RC.RP-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "iam_audit_logs_enabled", + "logging_sink_created" + ] + }, + { + "Id": "SEF-08", + "Description": "Maintain points of contact for applicable regulation authorities, national and local law enforcement, and other legal jurisdictional authorities.", + "Name": "Points of Contact Maintenance", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "17.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 4.2", + "27001: A.6.1.3", + "27002: 6.1.3", + "27017: 6.1.3", + "27018: 6.1.3", + "27001: A.16.1.1", + "27002: 16.1.1", + "27001: A.18.1.1", + "27002: 18.1.1", + "27017: 18.1.1", + "27018: 18.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.5", + "27001: A.5.24", + "27002: 5.24 Incident management procedure (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IR-4", + "IR-4(8)", + "IR-6", + "IR-6(3)", + "IR-7", + "IR-7(2)", + "PM-21", + "PM-23", + "PM-26" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-2", + "RS.CO-3", + "RS.CO-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.RR-02", + "RS.CO-02", + "RS.CO-03" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "iam_organization_essential_contacts_configured" + ] + }, + { + "Id": "TVM-02", + "Description": "Establish, document, approve, communicate, apply, evaluate and maintain policies and procedures to protect against malware on managed assets. Review and update the policies and procedures at least annually.", + "Name": "Malware Protection Policy and Procedures", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC6.8" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-01", + "GRM-06", + "GRM-09" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "9.7", + "10.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.1.1", + "1.5.1", + "5.2.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS1.2", + "TS1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5", + "27002: 5", + "27001: A.12.2.1", + "27001: A.6.2.1", + "27002: 6.2.1 (h)", + "27001: A.6.2.2", + "27002: 6.2.2 (j)", + "27001: A.7.2.2", + "27002: 7.2.2 (d)", + "27001: A.10.1.1", + "27002: 10.1.1 (g)", + "27001: A.13.2.1", + "27002: 13.2.1 (b)", + "27001: A.15.1.2", + "27017: 15.1.2", + "27001: A.12.2.1", + "27002: 12.2.1 (a),(d)", + "27017: CLD.9.5.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5.1", + "27001: A.5.4", + "27001: A.5.7", + "27001: A.5.37", + "27001: A.8.7", + "27002: 5.7 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-3", + "RA-3(3)", + "RA-5", + "RA-5(3)", + "RA-5(5)", + "SI-3", + "SI-3(4)", + "SI-3(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "DE.CM-4", + "DE.CM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.PO-01", + "GV.PO-02", + "ID.IM-03", + "DE.CM-01", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.4", + "12.1", + "12.1.1", + "12.3.1", + "12.5.1", + "12.11" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.1.1", + "12.1.2", + "5.1.1", + "5.3.2.1" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "TVM-03", + "Description": "Define, implement and evaluate processes, procedures and technical measures to enable both scheduled and emergency responses to vulnerability identifications, based on the identified risk.", + "Name": "Vulnerability Remediation Schedule", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC7.1", + "CC7.4" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "7.2", + "7.7", + "17.9" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1", + "TM2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.12.2.1", + "27001: A.12.6.1", + "27002: 12.6.1(c)(d)(j)", + "27018: 12.6.1(k)(i)" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.8.7", + "27001: A.8.8", + "27001: A.8.32", + "27002: 8.7", + "27002: 8.8", + "27002: 8.32" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-31", + "RA-3", + "RA-3(1)", + "RA-5", + "RA-5(2)-(4)", + "RA-5(6)", + "SI-3", + "SI-3(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "RS.AN-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01", + "ID.RA-06", + "ID.RA-08", + "PR.PS-02", + "PR.PS-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "6.1.a", + "6.1.b" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.1.1", + "6.3.1", + "6.3.2", + "6.3.3", + "12.10.1" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "TVM-04", + "Description": "Define, implement and evaluate processes, procedures and technical measures to update detection tools, threat signatures, and indicators of compromise on a weekly, or more frequent basis.", + "Name": "Detection Updates", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "No mapping" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "10.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS1.3", + "TS1.4", + "TM1.3", + "TM1.4", + "IM1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.5.1.1", + "27002: 5.1.1 (h)", + "27001: A.12.6.1", + "27002: 12.6.1 (b),(c)" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.5.1", + "27001: A.8.8", + "27001: A.8.15", + "27001: A.8.16", + "27002: 5.1", + "27002: 5.37", + "27002: 8.8", + "27002: 8.15 (d)", + "27002: 8.16 (d,e)", + "27002: 8.31 2nd (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-7", + "CM-7(4)", + "RA-3", + "RA-3(3)", + "RA-5(2)", + "SA-10", + "SA-10(5)", + "SA-11", + "SA-11(2)", + "SI-2", + "SI-2(4)", + "SI-3", + "SI-3(4)", + "SI-4", + "SI-4(9)", + "SI-4(24)", + "SI-8", + "SI-8(2)", + "SI-8(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-02", + "ID.RA-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.2", + "5.2a", + "5.2b", + "5.2c" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "5.3.1" + ] + } + ] + } + ], + "Checks": [ + "artifacts_container_analysis_enabled", + "gcr_container_scanning_enabled" + ] + }, + { + "Id": "TVM-05", + "Description": "Define, implement and evaluate processes, procedures and technical measures to identify updates for applications which use third party or open source libraries according to the organization's vulnerability management policy.", + "Name": "External Library Vulnerabilities", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC3.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "No mapping" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1", + "SD2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.12.6.2", + "27002: 12.6.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A 5.6", + "27001: A.8.19", + "27001: A.8.8", + "27001: A.8.28", + "27001: A.8.31", + "27002: 5.6 (c)", + "27001: 8.19", + "27001: 8.8", + "27001: 8.28", + "27001: 8.31" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-5", + "RA-5(3)", + "SA-11", + "SA-11(2)", + "SA-11(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01", + "ID.RA-03", + "PR.PS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "6.2", + "6.3.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "6.3.2", + "6.3.3" + ] + } + ] + } + ], + "Checks": [ + "artifacts_container_analysis_enabled", + "gcr_container_scanning_enabled" + ] + }, + { + "Id": "TVM-07", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the detection of vulnerabilities on organizationally managed assets at least monthly.", + "Name": "Vulnerability Identification", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "7.1", + "7.5", + "7.6" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.5", + "5.2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.6", + "27001: A.12.6.1", + "27002: 12.6.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.8", + "27002: 8.8" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-5", + "RA-5(4)", + "RA-5(5)", + "SA-11", + "SA-11(5)", + "SA-15(5)", + "SC-7", + "SC-7(10)", + "SI-3(8)", + "SI-3(10)", + "SI-7", + "SI-7(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.RA-1", + "DE.CM-8", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "11.2", + "11.2.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "6.3.2", + "6.3.3", + "11.3.2", + "11.3.2.1" + ] + } + ] + } + ], + "Checks": [ + "artifacts_container_analysis_enabled", + "compute_public_address_shodan", + "gcr_container_scanning_enabled" + ] + }, + { + "Id": "UEM-08", + "Description": "Protect information from unauthorized disclosure on managed endpoint devices with storage encryption.", + "Name": "Storage Encryption", + "Attributes": [ + { + "Section": "Universal Endpoint Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "MOS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.6" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "3.1.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "PA1.2", + "PA1.3", + "PA1.5", + "PA2.2", + "PM1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.11.2.7", + "27002: 11.2.7", + "27001: A.18.1.1", + "27017: 18.1.1", + "27001: A.12.3.1", + "27017: 12.3.1", + "27018: A.11.4", + "27018: A.11.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.1", + "27002: 8.1 (h)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-19(5)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.4", + "3.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.5.1", + "3.6" + ] + } + ] + } + ], + "Checks": [ + "bigquery_dataset_cmk_encryption", + "bigquery_table_cmk_encryption", + "compute_instance_confidential_computing_enabled", + "compute_instance_encryption_with_csek_enabled", + "dataproc_encrypted_with_cmks_disabled" + ] + }, + { + "Id": "UEM-11", + "Description": "Configure managed endpoints with Data Loss Prevention (DLP) technologies and rules in accordance with a risk assessment.", + "Name": "Data Loss Prevention", + "Attributes": [ + { + "Section": "Universal Endpoint Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.13" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.5", + "PA2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.3", + "27002: 12.3", + "27001: A.8.3.1", + "27002: 8.3.1", + "27001: A.12.2", + "27002: 12.2", + "27001: A.18.1.3", + "27002: 18.1.3", + "27001: A.6.1.1", + "27017: 6.1.1", + "27018: 12.3.1", + "27018: 10.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.12", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-7", + "SC-7(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02", + "PR.DS-10", + "PR.PS-01", + "ID.AM-08", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A3.2.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "A3.2.6" + ] + } + ] + } + ], + "Checks": [] + } + ] +} diff --git a/prowler/compliance/github/cis_1.0_github.json b/prowler/compliance/github/cis_1.0_github.json index f016acaabb..e675470169 100644 --- a/prowler/compliance/github/cis_1.0_github.json +++ b/prowler/compliance/github/cis_1.0_github.json @@ -778,7 +778,9 @@ { "Id": "1.3.9", "Description": "Confirm the domains an organization owns with a \"Verified\" badge.", - "Checks": [], + "Checks": [ + "organization_verified_badge" + ], "Attributes": [ { "Section": "1 Source Code", diff --git a/prowler/compliance/m365/cis_4.0_m365.json b/prowler/compliance/m365/cis_4.0_m365.json index 156c17e26d..df9b6edfb5 100644 --- a/prowler/compliance/m365/cis_4.0_m365.json +++ b/prowler/compliance/m365/cis_4.0_m365.json @@ -318,7 +318,9 @@ { "Id": "2.1.1", "Description": "Enabling Safe Links policy for Office applications allows URL's that exist inside of Office documents and email applications opened by Office, Office Online and Office mobile to be processed against Defender for Office time-of-click verification and rewritten if required.**Note:** E5 Licensing includes a number of Built-in Protection policies. When auditing policies note which policy you are viewing, and keep in mind CIS recommendations often extend the Default or Build-in Policies provided by MS. In order to **Pass** the highest priority policy must match all settings recommended.", - "Checks": [], + "Checks": [ + "defender_safelinks_policy_enabled" + ], "Attributes": [ { "Section": "2 Microsoft 365 Defender", @@ -385,7 +387,9 @@ { "Id": "2.1.4", "Description": "The Safe Attachments policy helps protect users from malware in email attachments by scanning attachments for viruses, malware, and other malicious content. When an email attachment is received by a user, Safe Attachments will scan the attachment in a secure environment and provide a verdict on whether the attachment is safe or not.", - "Checks": [], + "Checks": [ + "defender_safe_attachments_policy_enabled" + ], "Attributes": [ { "Section": "2 Microsoft 365 Defender", @@ -406,7 +410,9 @@ { "Id": "2.1.5", "Description": "Safe Attachments for SharePoint, OneDrive, and Microsoft Teams scans these services for malicious files.", - "Checks": [], + "Checks": [ + "defender_atp_safe_attachments_and_docs_configured" + ], "Attributes": [ { "Section": "2 Microsoft 365 Defender", diff --git a/prowler/compliance/m365/cis_6.0_m365.json b/prowler/compliance/m365/cis_6.0_m365.json index 93fe2e2226..192e49dcfd 100644 --- a/prowler/compliance/m365/cis_6.0_m365.json +++ b/prowler/compliance/m365/cis_6.0_m365.json @@ -339,7 +339,9 @@ { "Id": "2.1.1", "Description": "Safe Links for Office Applications is a feature in Microsoft 365 that provides URL scanning and rewriting of inbound email messages in mail flow, and time-of-click verification of URLs and links in email messages, other Microsoft 365 applications such as Teams, and other locations such as SharePoint Online. It can help protect organizations from malicious links used in phishing and other attacks.", - "Checks": [], + "Checks": [ + "defender_safelinks_policy_enabled" + ], "Attributes": [ { "Section": "2 Microsoft 365 Defender", @@ -404,7 +406,9 @@ { "Id": "2.1.4", "Description": "The Safe Attachments policy helps protect users from malware in email attachments by scanning attachments for viruses, malware, and other malicious content. When an email attachment is received by a user, Safe Attachments will scan the attachment in a secure environment and provide a verdict on whether the attachment is safe or not.", - "Checks": [], + "Checks": [ + "defender_safe_attachments_policy_enabled" + ], "Attributes": [ { "Section": "2 Microsoft 365 Defender", @@ -425,7 +429,9 @@ { "Id": "2.1.5", "Description": "Safe Attachments for SharePoint, OneDrive, and Microsoft Teams scans these services for malicious files.", - "Checks": [], + "Checks": [ + "defender_atp_safe_attachments_and_docs_configured" + ], "Attributes": [ { "Section": "2 Microsoft 365 Defender", diff --git a/prowler/compliance/m365/iso27001_2022_m365.json b/prowler/compliance/m365/iso27001_2022_m365.json index 81ae9dea2a..3335fe0f19 100644 --- a/prowler/compliance/m365/iso27001_2022_m365.json +++ b/prowler/compliance/m365/iso27001_2022_m365.json @@ -115,6 +115,7 @@ "defender_antiphishing_policy_configured", "defender_antispam_outbound_policy_configured", "defender_malware_policy_notifications_internal_users_malware_enabled", + "defender_safelinks_policy_enabled", "defender_zap_for_teams_enabled", "entra_admin_users_phishing_resistant_mfa_enabled", "entra_identity_protection_sign_in_risk_enabled", @@ -692,6 +693,8 @@ "defender_malware_policy_common_attachments_filter_enabled", "defender_malware_policy_comprehensive_attachments_filter_applied", "defender_malware_policy_notifications_internal_users_malware_enabled", + "defender_safe_attachments_policy_enabled", + "defender_safelinks_policy_enabled", "defender_zap_for_teams_enabled", "teams_external_domains_restricted", "teams_external_users_cannot_start_conversations" @@ -729,6 +732,7 @@ ], "Checks": [ "defender_antiphishing_policy_configured", + "defender_safelinks_policy_enabled", "entra_admin_users_phishing_resistant_mfa_enabled" ] }, @@ -833,10 +837,11 @@ } ], "Checks": [ - "teams_external_domains_restricted", - "teams_external_users_cannot_start_conversations", + "defender_safelinks_policy_enabled", + "sharepoint_external_sharing_managed", "sharepoint_external_sharing_restricted", - "sharepoint_external_sharing_managed" + "teams_external_domains_restricted", + "teams_external_users_cannot_start_conversations" ] }, { diff --git a/prowler/compliance/oraclecloud/cis_3.1_oraclecloud.json b/prowler/compliance/oraclecloud/cis_3.1_oraclecloud.json new file mode 100644 index 0000000000..080aa60476 --- /dev/null +++ b/prowler/compliance/oraclecloud/cis_3.1_oraclecloud.json @@ -0,0 +1,1141 @@ +{ + "Framework": "CIS", + "Name": "CIS Oracle Cloud Infrastructure Foundations Benchmark v3.1.0", + "Version": "3.1", + "Provider": "OracleCloud", + "Description": "The CIS Oracle Cloud Infrastructure Foundations Benchmark provides prescriptive guidance for configuring security options for Oracle Cloud Infrastructure with an emphasis on foundational, testable, and architecture agnostic settings.", + "Requirements": [ + { + "Id": "1.1", + "Description": "Ensure service level admins are created to manage resources of particular service", + "Checks": [ + "identity_service_level_admins_exist" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "To apply least-privilege security principle, one can create service-level administrators in corresponding groups and assigning specific users to each service-level administrative group in a tenancy. This limits administrative access in a tenancy. It means service-level administrators can only manage resources of a specific service.Example policies for global/tenant level service-administrators```Allow group VolumeAdmins to manage volume-family in tenancyAllow group ComputeAdmins to manage instance-family in tenancyAllow group NetworkAdmins to manage virtual-network-family in tenancy``````A tenancy with identity domains : An Identity Domain is a container of users, groups, Apps and other security configurations. A tenancy that has Identity Domains available comes seeded with a 'Default' identity domain. If a group belongs to a domain different than the default domain, use a domain prefix in the policy statements.Example - Allow group / to in compartment If you do not include the before the , then the policy statement is evaluated as though the group belongs to the default identity domain.```Organizations have various ways of defining service-administrators. Some may prefer creating service administrators at a tenant level and some per department or per project or even per application environment ( dev/test/production etc.). Either approach works so long as the policies are written to limit access given to the service-administrators. Example policies for compartment level service-administrators ```Allow group NonProdComputeAdmins to manage instance-family in compartment devAllow group ProdComputeAdmins to manage instance-family in compartment productionAllow group A-Admins to manage instance-family in compartment Project-AAllow group A-Admins to manage volume-family in compartment Project-A``````A tenancy with identity domains : An Identity Domain is a container of users, groups, Apps and other security configurations. A tenancy that has Identity Domains available comes seeded with a 'Default' identity domain. If a group belongs to a domain different than the default domain, use a domain prefix in the policy statements.Example - Allow group / to in compartment If you do not include the before the , then the policy statement is evaluated as though the group belongs to the default identity domain.```", + "RationaleStatement": "Creating service-level administrators helps in tightly controlling access to Oracle Cloud Infrastructure (OCI) services to implement the least-privileged security principle.", + "ImpactStatement": "", + "RemediationProcedure": "Refer to the [policy syntax document](https://docs.cloud.oracle.com/en-us/iaas/Content/Identity/Concepts/policysyntax.htm) and create new policies if the audit results indicate that the required policies are missing.This can be done via OCI console or OCI CLI/SDK or API.Creating a new policy:***From CLI:***```oci iam policy create [OPTIONS]```Creates a new policy in the specified compartment (either the tenancy or another of your compartments). If you're new to policies, see [Getting Started with Policies](https://docs.cloud.oracle.com/Content/Identity/Concepts/policygetstarted.htm) You must specify a name for the policy, which must be unique across all policies in your tenancy and cannot be changed.You must also specify a description for the policy (although it can be an empty string). It does not have to be unique, and you can change it anytime with UpdatePolicy.You must specify one or more policy statements in the statements array.For information about writing policies, see How [Policies Work](https://docs.cloud.oracle.com/Content/Identity/Concepts/policies.htm) and [Common Policies](https://docs.cloud.oracle.com/Content/Identity/Concepts/commonpolicies.htm).", + "AuditProcedure": "***From CLI:***1) [Set up OCI CLI](https://docs.cloud.oracle.com/iaas/Content/API/SDKDocs/cliinstall.htm) with an IAM administrator user who has read access to IAM resources such as groups and policies.2) Run OCI CLI command providing the root_compartment_OCIDGet the list of groups in a tenancy```oci iam group list --compartment-id | grep name``````A tenancy with identity domains : The above CLI commands work with the default identity domain only.For IaaS resource management, users and groups created in the default domain are sufficient. ```3) Ensure distinct administrative groups are created as per your organization's definition of service-administrators.4) Verify the appropriate policies are created for the service-administrators groups to have the right access to the corresponding services. Retrieve the policy statements scoped at the tenancy level and/or per compartment. ```oci iam policy list --compartment-id | grep in tenancyoci iam policy list --compartment-id | grep in compartment```The --compartment-id parameter can be changed to a child compartment to get policies associated with child compartments.```oci iam policy list --compartment-id | grep in compartment```Verify the results to ensure the right policies are created for service-administrators to have the necessary access.", + "AdditionalInformation": "", + "References": "" + } + ] + }, + { + "Id": "1.2", + "Description": "Ensure permissions on all resources are given only to the tenancy administrator group", + "Checks": [ + "identity_tenancy_admin_permissions_limited" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "There is a built-in OCI IAM policy enabling the Administrators group to perform any action within a tenancy. In the OCI IAM console, this policy reads:```Allow group Administrators to manage all-resources in tenancy```Administrators create more users, groups, and policies to provide appropriate access to other groups.Administrators should not allow any-other-group full access to the tenancy by writing a policy like this - ```Allow group any-other-group to manage all-resources in tenancy```The access should be narrowed down to ensure the least-privileged principle is applied.", + "RationaleStatement": "Permission to manage all resources in a tenancy should be limited to a small number of users in the `Administrators` group for break-glass situations and to set up users/groups/policies when a tenancy is created.No group other than `Administrators` in a tenancy should need access to all resources in a tenancy, as this violates the enforcement of the least privilege principle.", + "ImpactStatement": "", + "RemediationProcedure": "**From Console:**1) Login to OCI console.2) Go to `Identity` -> `Policies`, In the compartment dropdown, choose the root compartment. Open each policy to view the policy statements. 2) Remove any policy statement that allows any group other than `Administrators` or any service access to manage all resources in the tenancy. **From CLI:**The policies can also be updated via OCI CLI, SDK and API, with an example of the CLI commands below: * Delete a policy via the CLI: `oci iam policy delete --policy-id ` * Update a policy via the CLI: `oci iam policy update --policy-id --statements `Note: You should generally **not** delete the policy that allows the `Administrators` group the ability to manage all resources in the tenancy.", + "AuditProcedure": "**From CLI:**1) Run OCI CLI command providing the root compartment OCID to get the list of groups having access to manage all resources in your tenancy. ```oci iam policy list --compartment-id | grep -i to manage all-resources in tenancy ```2) Verify the results to ensure only the `Administrators` group has access to manage all resources in tenancy. Allow group Administrators to manage all-resources in tenancy", + "AdditionalInformation": "", + "References": "" + } + ] + }, + { + "Id": "1.3", + "Description": "Ensure IAM administrators cannot update tenancy Administrators group", + "Checks": [ + "identity_iam_admins_cannot_update_tenancy_admins" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Tenancy administrators can create more users, groups, and policies to provide other service administrators access to OCI resources.For example, an IAM administrator will need to have access to manage resources like compartments, users, groups, dynamic-groups, policies, identity-providers, tenancy tag-namespaces, tag-definitions in the tenancy.The policy that gives IAM-Administrators or any other group full access to 'groups' resources should not allow access to the tenancy 'Administrators' group.The policy statements would look like -```Allow group IAMAdmins to inspect users in tenancyAllow group IAMAdmins to use users in tenancy where target.group.name != 'Administrators'Allow group IAMAdmins to inspect groups in tenancyAllow group IAMAdmins to use groups in tenancy where target.group.name != 'Administrators'```**Note:** You must include separate statements for 'inspect' access, because the target.group.name variable is not used by the ListUsers and ListGroups operations", + "RationaleStatement": "These policy statements ensure that no other group can manage tenancy administrator users or the membership to the 'Administrators' group thereby gain or remove tenancy administrator access.", + "ImpactStatement": "", + "RemediationProcedure": "**From Console:**1. Login to OCI Console.2. Select `Identity` from Services Menu.3. Select `Policies` from Identity Menu.4. Click on an individual policy under the Name heading.5. Ensure Policy statements look like this -```Allow group IAMAdmins to use users in tenancy where target.group.name != 'Administrators'Allow group IAMAdmins to use groups in tenancy where target.group.name != 'Administrators'```", + "AuditProcedure": "**From CLI:**1) Run the following OCI CLI commands providing the root_compartment_OCID ```oci iam policy list --compartment-id | grep -i to use users in tenancyoci iam policy list --compartment-id | grep -i to use groups in tenancy```2) Verify the results to ensure that the policy statements that grant access to use or manage users or groups in the tenancy have a condition that excludes access to `Administrators` group or to users in the Administrators group.", + "AdditionalInformation": "", + "References": "" + } + ] + }, + { + "Id": "1.4", + "Description": "Ensure IAM password policy requires minimum length of 14 or greater", + "Checks": [ + "identity_password_policy_minimum_length_14" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Password policies are used to enforce password complexity requirements. IAM password policies can be used to ensure passwords are at least a certain length and are composed of certain characters. It is recommended the password policy require a minimum password length 14 characters and contain 1 non-alphabeticcharacter (Number or “Special Character”).", + "RationaleStatement": "In keeping with the overall goal of having users create a password that is not overly weak, an eight-character minimum password length is recommended for an MFA account, and 14 characters for a password only account. In addition, maximum password length should be made as long as possible based on system/software capabilities and not restricted by policy.In general, it is true that longer passwords are better (harder to crack), but it is also true that forced password length requirements can cause user behavior that is predictable and undesirable. For example, requiring users to have a minimum 16-character password may cause them to choose repeating patterns like fourfourfourfour or passwordpassword that meet the requirement but aren’t hard to guess. Additionally, length requirements increase the chances that users will adopt other insecure practices, like writing them down, re-using them or storing them unencrypted in their documents. Password composition requirements are a poor defense against guessing attacks. Forcing users to choose some combination of upper-case, lower-case, numbers, and special characters has a negative impact. It places an extra burden on users and manywill use predictable patterns (for example, a capital letter in the first position, followed by lowercase letters, then one or two numbers, and a “special character” at the end). Attackers know this, so dictionary attacks will often contain these common patterns and use the most common substitutions like, $ for s, @ for a, 1 for l, 0 for o.Passwords that are too complex in nature make it harder for users to remember, leading to bad practices. In addition, composition requirements provide no defense against common attack types such as social engineering or insecure storage of passwords.", + "ImpactStatement": "", + "RemediationProcedure": "1. Go to Identity Domains: [https://cloud.oracle.com/identity/domains/](https://cloud.oracle.com/identity/domains/)1. Select the Compartment the Domain to remediate is in1. Click on the Domain to remediate1. Click on Settings1. Click on Password policy to remediate1. Click Edit password rules1. Update the `Password length (minimum)` setting to 14 or greater6. Under The `Passwords must meet the following character requirements` section, update the number given in `Special (minimum)` setting to `1` or greateror Under The `Passwords must meet the following character requirements` section, update the number given in `Numeric (minimum)` setting to `1` or greater7. Click `Save changes`", + "AuditProcedure": "1. Go to Identity Domains: [https://cloud.oracle.com/identity/domains/](https://cloud.oracle.com/identity/domains/)1. Select the `Compartment` your Domain to review is in1. Click on the Domain to review1. Click on `Settings`1. Click on `Password policy`1. Click each Password policy in the domain1. Ensure `Password length (minimum)` is greater than or equal to 141. Under The `The following criteria apply to passwords` section, ensure that the number given in `Numeric (minimum)` setting is `1`, or the `Special (minimum)` setting is `1`.The following criteria apply to passwords:6. Ensure that 1 or more is selected for `Numeric (minimum)` OR `Special (minimum)`**From Cloud Guard:**To Enable Cloud Guard Auditing:Ensure Cloud Guard is enabled in the root compartment of the tenancy. For more information about enabling Cloud Guard, please look at the instructions included in Ensure Cloud Guard is enabled in the root compartment of the tenancy Recommendation in the Logging and Monitoring section. **From Console:**1. Type `Cloud Guard` into the Search box at the top of the Console.2. Click `Cloud Guard` from the “Services” submenu.3. Click `Detector Recipes` in the Cloud Guard menu.4. Click `OCI Configuration Detector Recipe (Oracle Managed)` under the Recipe Name column.5. Find Password policy does not meet complexity requirements in the Detector Rules column.6. Select the vertical ellipsis icon and chose `Edit` on the Password policy does not meet complexity requirements row.7. In the Edit Detector Rule window, find the Input Setting box and verify/change the Required password length setting to 14.8. Click the `Save` button.**From CLI:**1. Update the Password policy does not meet complexity requirements Detector Rule in Cloud Guard to generate Problems if IAM password policy isn’t configured to enforce a password length of at least 14 characters with the following command:```oci cloud-guard detector-recipe-detector-rule update --detector-recipe-id --detector-rule-id PASSWORD_POLICY_NOT_COMPLEX --details '{configurations:[{ configKey : passwordPolicyMinLength, name : Required password length, value : 14, dataType : null, values : null }]}'```", + "AdditionalInformation": "The Audit Procedure and Remediation Procedure for OCI IAM without Identity Domains can be found in the CIS OCI Foundation Benchmark 2.0.0 under the respective recommendations.", + "References": "https://www.cisecurity.org/white-papers/cis-password-policy-guide/" + } + ] + }, + { + "Id": "1.5", + "Description": "Ensure IAM password policy expires passwords within 365 days", + "Checks": [ + "identity_password_policy_expires_within_365_days" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "IAM password policies can require passwords to be rotated or expired after a given number of days. It is recommended that the password policy expire passwords after 365 and are changed immediately based on events.", + "RationaleStatement": "Excessive password expiration requirements do more harm than good, because these requirements make users select predictable passwords, composed of sequential words and numbers that are closely related to each other. In these cases, the next password can be predicted based on the previous one (incrementing a number used in the password for example). Also, password expiration requirements offer no containment benefits because attackers will often use credentials as soon as they compromise them. Instead, immediate password changes should be based on key events including, but notlimited to:1. Indication of compromise1. Change of user roles1. When a user leaves the organization.Not only does changing passwords every few weeks or months frustrate the user, it's been suggested that it does more harm than good, because it could lead to bad practices by the user such as adding a character to the end of their existing password.In addition, we also recommend a yearly password change. This is primarily because for all their good intentions users will share credentials across accounts. Therefore, even if a breach is publicly identified, the user may not see this notification, or forget they have an account on that site. This could leave a shared credential vulnerable indefinitely. Having an organizational policy of a 1-year (annual) password expiration is a reasonable compromise to mitigate this with minimal user burden.", + "ImpactStatement": "", + "RemediationProcedure": "1. Go to Identity Domains: [https://cloud.oracle.com/identity/domains/](https://cloud.oracle.com/identity/domains/)1. Select the `Compartment` the Domain to remediate is in1. Click on the Domain to remediate1. Click on `Settings`1. Click on `Password policy` to remediate1. Click `Edit password rules`1. Change `Expires after (days)` to 365", + "AuditProcedure": "1. Go to Identity Domains: [https://cloud.oracle.com/identity/domains/](https://cloud.oracle.com/identity/domains/)1. Select the `Compartment` your Domain to review is in1. Click on the Domain to review1. Click on `Settings`1. Click on `Password policy`1. Click each Password policy in the domain1. Ensure `Expires after (days)` is less than or equal to 365 days", + "AdditionalInformation": "The Audit Procedure and Remediation Procedure for OCI IAM without Identity Domains can be found in the CIS OCI Foundation Benchmark 2.0.0 under the respective recommendations.", + "References": "https://www.cisecurity.org/white-papers/cis-password-policy-guide/" + } + ] + }, + { + "Id": "1.6", + "Description": "Ensure IAM password policy prevents password reuse", + "Checks": [ + "identity_password_policy_prevents_reuse" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "IAM password policies can prevent the reuse of a given password by the same user. It is recommended the password policy prevent the reuse of passwords.", + "RationaleStatement": "Enforcing password history ensures that passwords are not reused in for a certain period of time by the same user. If a user is not allowed to use last 24 passwords, that window of time is greater. This helps maintain the effectiveness of password security.", + "ImpactStatement": "", + "RemediationProcedure": "1. Go to Identity Domains: [https://cloud.oracle.com/identity/domains/](https://cloud.oracle.com/identity/domains/)1. Select the Compartment the Domain to remediate is in1. Click on the Domain to remediate1. Click on Settings1. Click on Password policy to remediate1. Click Edit password rules1. Update the number of remembered passwords in `Previous passwords remembered` setting to 24 or greater.", + "AuditProcedure": "1. Go to Identity Domains: [https://cloud.oracle.com/identity/domains/](https://cloud.oracle.com/identity/domains/)1. Select the `Compartment` your Domain to review is in1. Click on the Domain to review1. Click on `Settings`1. Click on `Password policy`1. Click each Password policy in the domain1. Ensure `Previous passwords remembered` is set 24 or greater", + "AdditionalInformation": "The Audit Procedure and Remediation Procedure for OCI IAM without Identity Domains can be found in the CIS OCI Foundation Benchmark 2.0.0 under the respective recommendations.", + "References": "" + } + ] + }, + { + "Id": "1.7", + "Description": "Ensure MFA is enabled for all users with a console password", + "Checks": [ + "identity_user_mfa_enabled_console_access" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Multi-factor authentication is a method of authentication that requires the use of more than one factor to verify a user’s identity.With MFA enabled in the IAM service, when a user signs in to Oracle Cloud Infrastructure, they are prompted for their user name and password, which is the first factor (something that they know). The user is then prompted to provide a verification code from a registered MFA device, which is the second factor (something that they have). The two factors work together, requiring an extra layer of security to verify the user’s identity and complete the sign-in process.OCI IAM supports two-factor authentication using a password (first factor) and a device that can generate a time-based one-time password (TOTP) (second factor).See [OCI documentation](https://docs.cloud.oracle.com/en-us/iaas/Content/Identity/Tasks/usingmfa.htm) for more details.", + "RationaleStatement": "Multi factor authentication adds an extra layer of security during the login process and makes it harder for unauthorized users to gain access to OCI resources.", + "ImpactStatement": "", + "RemediationProcedure": "Each user must enable MFA for themselves using a device they will have access to every time they sign in. An administrator cannot enable MFA for another user but can enforce MFA by identifying the list of non-complaint users, notifying them or disabling access by resetting the password for non-complaint accounts.**Disabling access from Console:**1. Go to [https://cloud.oracle.com/identity/](https://cloud.oracle.com/identity/).1. Select `Domains` from Identity menu.1. Select the domain1. Click `Security`1. Click `Sign-on polices` then the `Default Sign-on Policy`1. Under the sign-on rules header, click the three dots on the rule with the highest priority.1. Select `Edit sign-on rule`1. Make a change to ensure that `allow access` is selected and `prompt for an additional factor` is enabled", + "AuditProcedure": "**From Console:**1. Go to Identity Domains: [https://cloud.oracle.com/identity/domains/](https://cloud.oracle.com/identity/domains/)1. Select the `Compartment` your Domain to review is in1. Click on the Domain to review1. Click on `Security`1. Click `Sign-on policies` 1. Select the sign-on policy to review6. Under the sign-on rules header, click the three dots on the rule with the highest priority.7. Select `Edit sign-on rule`8. Verify that `allow access` is selected and `prompt for an additional factor` is enabled* This requires users to enable MFA when they next login next however, to determine users have enabled MFA use the below CLI.**From the CLI:*** This CLI command checks which users have enabled MFA for their accounts1. Execute the below:```tenancy_ocid=`oci iam compartment list --raw-output --query data[?contains(\\compartment-id\\,'.tenancy.')].\\compartment-id\\ | [0]`for id_domain_url in `oci iam domain list --compartment-id $tenancy_ocid --all | jq -r '.data[] | .url'`do oci identity-domains users list --endpoint $id_domain_url 2>/dev/null | jq -r '.data.resources[] | select(.urn-ietf-params-scim-schemas-oracle-idcs-extension-mfa-user.mfa-status!=ENROLLED)' 2>/dev/null | jq -r '.ocid'donefor region in `oci iam region-subscription list | jq -r '.data[] | .region-name'`; do for compid in `oci iam compartment list --compartment-id-in-subtree TRUE --all 2>/dev/null | jq -r '.data[] | .id'` do for id_domain_url in `oci iam domain list --compartment-id $compid --region $region --all 2>/dev/null | jq -r '.data[] | .url'` do oci identity-domains users list --endpoint $id_domain_url 2>/dev/null | jq -r '.data.resources[] | select(.urn-ietf-params-scim-schemas-oracle-idcs-extension-mfa-user.mfa-status!=ENROLLED)' 2>/dev/null | jq -r '.ocid' done done done```2. Ensure no results are returned", + "AdditionalInformation": "The Audit Procedure and Remediation Procedure for OCI IAM without Identity Domains can be found in the CIS OCI Foundation Benchmark 2.0.0 under the respective recommendations.", + "References": "https://docs.cloud.oracle.com/en-us/iaas/Content/Identity/Tasks/usingmfa.htm:https://docs.oracle.com/en-us/iaas/Content/Security/Reference/iam_security_topic-IAM_MFA.htm" + } + ] + }, + { + "Id": "1.8", + "Description": "Ensure user API keys rotate within 90 days", + "Checks": [ + "identity_user_api_keys_rotated_90_days" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "API keys are used by administrators, developers, services and scripts for accessing OCI APIs directly or via SDKs/OCI CLI to search, create, update or delete OCI resources.The API key is an RSA key pair. The private key is used for signing the API requests and the public key is associated with a local or synchronized user's profile.", + "RationaleStatement": "It is important to secure and rotate an API key every 90 days or less as it provides the same level of access that a user it is associated with has.In addition to a security engineering best practice, this is also a compliance requirement. For example, PCI-DSS Section 3.6.4 states, Verify that key-management procedures include a defined cryptoperiod for each key type in use and define a process for key changes at the end of the defined crypto period(s).", + "ImpactStatement": "", + "RemediationProcedure": "**From Console:**1. Login to OCI Console.2. Select `Identity & Security` from the Services menu.3. Select `Domains` from the Identity menu.4. For each domain listed, click on the name and select `Users`.5. Click on an individual user under the Name heading.6. Click on `API Keys` in the lower left-hand corner of the page.7. Delete any API Keys that are older than 90 days under the `Created` column of the API Key table.**From CLI:**```oci iam user api-key delete --user-id __ --fingerprint ```", + "AuditProcedure": "**From Console:**1. Login to OCI Console.2. Select `Identity & Security` from the Services menu.3. Select `Domains` from the Identity menu.4. For each domain listed, click on the name and select `Users`.5. Click on an individual user under the Name heading.6. Click on `API Keys` in the lower left-hand corner of the page.7. Ensure the date of the API key under the `Created` column of the API Key is no more than 90 days old.", + "AdditionalInformation": "The Audit Procedure and Remediation Procedure for OCI IAM without Identity Domains can be found in the CIS OCI Foundation Benchmark 2.0.0 under the respective recommendations.", + "References": "" + } + ] + }, + { + "Id": "1.9", + "Description": "Ensure user customer secret keys rotate within 90 days", + "Checks": [ + "identity_user_customer_secret_keys_rotated_90_days" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Object Storage provides an API to enable interoperability with Amazon S3. To use this Amazon S3 Compatibility API, you need to generate the signing key required to authenticate with Amazon S3.This special signing key is an Access Key/Secret Key pair. Oracle generates the Customer Secret key to pair with the Access Key.", + "RationaleStatement": "It is important to rotate customer secret keys at least every 90 days, as they provide the same level of object storage access that the user they are associated with has.", + "ImpactStatement": "", + "RemediationProcedure": "**From Console:**1. Login to OCI Console.1. Select `Identity & Security` from the Services menu.1. Select Domains from the Identity menu.1. For each domain listed, click on the name and select `Users`.1. Click on an individual user under the `Username` heading.1. Click on `Customer Secret Keys` in the lower left-hand corner of the page.1. Delete any Access Keys with a date older than 90 days under the `Created` column of the Customer Secret Keys.", + "AuditProcedure": "**From Console:**1. Login to OCI Console.1. Select `Identity & Security` from the Services menu.1. Select Domains from the Identity menu.1. For each domain listed, click on the name and select `Users`.1. Click on an individual user under the `Username` heading.1. Click on `Customer Secret Keys` in the lower left-hand corner of the page.1. Ensure the date of the Customer Secret Key under the `Created` column of the Customer Secret Key is no more than 90 days old.", + "AdditionalInformation": "The Audit Procedure and Remediation Procedure for OCI IAM without Identity Domains can be found in the CIS OCI Foundation Benchmark 2.0.0 under the respective recommendations.", + "References": "" + } + ] + }, + { + "Id": "1.10", + "Description": "Ensure user auth tokens rotate within 90 days", + "Checks": [ + "identity_user_auth_tokens_rotated_90_days" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Auth tokens are authentication tokens generated by Oracle. You use auth tokens to authenticate with APIs that do not support the Oracle Cloud Infrastructure signature-based authentication. If the service requires an auth token, the service-specific documentation instructs you to generate one and how to use it.", + "RationaleStatement": "It is important to secure and rotate an auth token every 90 days or less as it provides the same level of access to APIs that do not support the OCI signature-based authentication as the user associated to it.", + "ImpactStatement": "", + "RemediationProcedure": "**From Console:**1. Login to OCI Console.1. Select `Identity & Security` from the Services menu.1. Select Domains from the Identity menu.1. For each domain listed, click on the name and select `Users`.1. Click on an individual user under the `Username` heading.1. Click on `Auth Tokens` in the lower left-hand corner of the page.1. Delete any auth token with a date older than 90 days under the `Created` column of the Customer Secret Keys.", + "AuditProcedure": "**From Console:**1. Login to OCI Console.1. Select `Identity & Security` from the Services menu.1. Select Domains from the Identity menu.1. For each domain listed, click on the name and select `Users`.1. Click on an individual user under the `Username` heading.5. Click on `Auth Tokens` in the lower left-hand corner of the page.1. Ensure the date of the Auth Token under the `Created` column of the Customer Secret Key is no more than 90 days old.", + "AdditionalInformation": "The Audit Procedure and Remediation Procedure for OCI IAM without Identity Domains can be found in the CIS OCI Foundation Benchmark 2.0.0 under the respective recommendations.", + "References": "" + } + ] + }, + { + "Id": "1.11", + "Description": "Ensure user IAM Database Passwords rotate within 90 days", + "Checks": [ + "identity_user_db_passwords_rotated_90_days" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Users can create and manage their database password in their IAM user profile and use that password to authenticate to databases in their tenancy. An IAM database password is a different password than an OCI Console password. Setting an IAM database password allows an authorized IAM user to sign in to one or more Autonomous Databases in their tenancy.An IAM database password is a different password than an OCI Console password. Setting an IAM database password allows an authorized IAM user to sign in to one or more Autonomous Databases in their tenancy.", + "RationaleStatement": "It is important to secure and rotate an IAM Database password 90 days or less as it provides the same access the user would have a using a local database user.", + "ImpactStatement": "", + "RemediationProcedure": "#### OCI IAM with Identity Domains**From Console:**1. Login to OCI Console.1. Select `Identity & Security` from the Services menu.1. Select Domains from the Identity menu.1. For each domain listed, click on the name and select `Users`.1. Click on an individual user under the `Username` heading.1. Click on `IAM Database Passwords` in the lower left-hand corner of the page.1. Delete any Database Passwords with a date older than 90 days under the `Created` column of the Database Passwords.", + "AuditProcedure": "**From Console:**1. Login to OCI Console.2. Select `Identity & Security` from the Services menu.3. Select `Users` from the Identity menu.4. Click on an individual user under the Name heading.5. Click on `Database Passwords` in the lower left-hand corner of the page.6. Ensure the date of the Database Passwords under the `Created` column of the Database Passwords is no more than 90 days **From Console:**1. Login to OCI Console.1. Select `Identity & Security` from the Services menu.1. Select Domains from the Identity menu.1. For each domain listed, click on the name and select `Users`.1. Click on an individual user under the `Username` heading.1. Click on `Database Passwords` in the lower left-hand corner of the page.1. Ensure the date of the Database Passwords under the `Created` column of the Database Password is no more than 90 days old.", + "AdditionalInformation": "The Audit Procedure and Remediation Procedure for OCI IAM without Identity Domains can be found in the CIS OCI Foundation Benchmark 2.0.0 under the respective recommendations.", + "References": "https://docs.oracle.com/en-us/iaas/Content/Identity/Concepts/usercredentials.htm#usercredentials_iam_db_pwd" + } + ] + }, + { + "Id": "1.12", + "Description": "Ensure API keys are not created for tenancy administrator users", + "Checks": [ + "identity_tenancy_admin_users_no_api_keys" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Tenancy administrator users have full access to the organization's OCI tenancy. API keys associated with user accounts are used for invoking the OCI APIs via custom programs or clients like CLI/SDKs. The clients are typically used for performing day-to-day operations and should never require full tenancy access. Service-level administrative users with API keys should be used instead.", + "RationaleStatement": "For performing day-to-day operations tenancy administrator access is not needed.Service-level administrative users with API keys should be used to apply privileged security principle.", + "ImpactStatement": "", + "RemediationProcedure": "**From Console:**1. Login to OCI console.2. Select `Identity` from Services menu.3. Select `Users` from Identity menu, or select `Domains`, select a domain, and select `Users`.4. Select the username of a tenancy administrator user with an API key.5. Select `API Keys` from the menu in the lower left-hand corner.6. Delete any associated keys from the `API Keys` table.7. Repeat steps 3-6 for all tenancy administrator users with an API key.**From CLI:**1. For each tenancy administrator user with an API key, execute the following command to retrieve API key details:```oci iam user api-key list --user-id ```2. For each API key, execute the following command to delete the key:```oci iam user api-key delete --user-id --fingerprint ```3. The following message will be displayed:```Are you sure you want to delete this resource? [y/N]:```4. Type 'y' and press 'Enter'.", + "AuditProcedure": "**From Console:**1. Login to OCI Console. 1. Select `Identity & Security` from the Services menu.1. Select `Domains` from the Identity menu.1. Click on the 'Default' Domain in the (root).1. Click on 'Groups'.1. Select the 'Administrators' group by clicking on the Name1. Click on each local or synchronized `Administrators` member profile4. Click on API Keys to verify if a user has an API key associated.", + "AdditionalInformation": "The Audit Procedure and Remediation Procedure for OCI IAM without Identity Domains can be found in the CIS OCI Foundation Benchmark 2.0.0 under the respective recommendations.", + "References": "" + } + ] + }, + { + "Id": "1.13", + "Description": "Ensure all OCI IAM local user accounts have a valid and current email address", + "Checks": [ + "identity_user_valid_email_address" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "All OCI IAM local user accounts have an email address field associated with the account. It is recommended to specify an email address that is valid and current.If you have an email address in your user profile, you can use the Forgot Password link on the sign on page to have a temporary password sent to you.", + "RationaleStatement": "Having a valid and current email address associated with an OCI IAM local user account allows you to tie the account to identity in your organization. It also allows that user to reset their password if it is forgotten or lost.", + "ImpactStatement": "", + "RemediationProcedure": "**From Console:**1. Login to OCI Console.1. Select `Identity & Security` from the Services menu.1. Select Domains from the Identity menu.1. For each domain listed, click on the name and select `Users`.1. Click on each non-complaint user.1. Click on `Edit User`.1. Enter a valid and current email address in the Email and Recovery Email text boxes.1. Click `Save Changes`", + "AuditProcedure": "**From Console:**1. Login to OCI Console.1. Select `Identity & Security` from the Services menu.1. Select Domains from the Identity menu.1. For each domain listed, click on the name and select `Users`.1. Click on an individual user under the `Username` heading.1. Ensure a valid and current email address is next to Email and Recovery email.", + "AdditionalInformation": "The Audit Procedure and Remediation Procedure for OCI IAM without Identity Domains can be found in the CIS OCI Foundation Benchmark 2.0.0 under the respective recommendations.", + "References": "" + } + ] + }, + { + "Id": "1.14", + "Description": "Ensure Instance Principal authentication is used for OCI instances, OCI Cloud Databases and OCI Functions to access OCI resources", + "Checks": [ + "identity_instance_principal_used" + ], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "OCI instances, OCI database and OCI functions can access other OCI resources either via an OCI API key associated to a user or via Instance Principal. Instance Principal authentication can be achieved by inclusion in a Dynamic Group that has an IAM policy granting it the required access or using an OCI IAM policy that has `request.principal` added to the `where` clause. Access to OCI Resources refers to making API calls to another OCI resource like Object Storage, OCI Vaults, etc.", + "RationaleStatement": "Instance Principal reduces the risks related to hard-coded credentials. Hard-coded API keys can be shared and require rotation, which can open them up to being compromised. Compromised credentials could allow access to OCI services outside of the expected radius.", + "ImpactStatement": "For an OCI instance that contains embedded credential audit the scripts and environment variables to ensure that none of them contain OCI API Keys or credentials.", + "RemediationProcedure": "**From Console (Dynamic Groups):**1. Go to [https://cloud.oracle.com/identity/domains/](https://cloud.oracle.com/identity/domains/)1. Select a Compartment1. Click on the Domain1. Click on `Dynamic groups`1. Click Create Dynamic Group.1. Enter a Name1. Enter a Description1. Enter Matching Rules to that includes the instances accessing your OCI resources.1. Click Create.", + "AuditProcedure": "**From Console (Dynamic Groups):**1. Go to [https://cloud.oracle.com/identity/domains/](https://cloud.oracle.com/identity/domains/)1. Select a Compartment1. Click on a Domain1. Click on `Dynamic groups`1. Click on the Dynamic Group1. Check if the Matching Rules includes the instances accessing your OCI resources.**From Console (request.principal):**1. Go to [https://cloud.oracle.com/identity/policies](https://cloud.oracle.com/identity/policies)1. Select a Compartment1. Click on an individual policy under the Name heading.1. Ensure Policy statements look like this :```allow any-user to in compartment where ALL {request.principal.type='', request.principal.id=''}```or```allow any-user to in compartment where ALL {request.principal.type='', request.principal.compartment.id=''}```**From CLI (request.principal):**1. Execute the following for each compartment_OCID: ```oci iam policy list --compartment-id | grep request.principal```1. Ensure that the condition includes the instances accessing your OCI resources", + "AdditionalInformation": "The Audit Procedure and Remediation Procedure for OCI IAM without Identity Domains can be found in the CIS OCI Foundation Benchmark 2.0.0 under the respective recommendations.", + "References": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingdynamicgroups.htm" + } + ] + }, + { + "Id": "1.15", + "Description": "Ensure storage service-level admins cannot delete resources they manage", + "Checks": [], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "To apply the separation of duties security principle, one can restrict service-level administrators from being able to delete resources they are managing. It means service-level administrators can only manage resources of a specific service but not delete resources for that specific service.Example policies for global/tenant level for block volume service-administrators:```Allow group VolumeUsers to manage volumes in tenancy where request.permission!='VOLUME_DELETE' Allow group VolumeUsers to manage volume-backups in tenancy where request.permission!='VOLUME_BACKUP_DELETE'```Example policies for global/tenant level for file storage system service-administrators:```Allow group FileUsers to manage file-systems in tenancy where request.permission!='FILE_SYSTEM_DELETE'Allow group FileUsers to manage mount-targets in tenancy where request.permission!='MOUNT_TARGET_DELETE'Allow group FileUsers to manage export-sets in tenancy where request.permission!='EXPORT_SET_DELETE'```Example policies for global/tenant level for object storage system service-administrators:```Allow group BucketUsers to manage objects in tenancy where request.permission!='OBJECT_DELETE' Allow group BucketUsers to manage buckets in tenancy where request.permission!='BUCKET_DELETE'```", + "RationaleStatement": "Creating service-level administrators without the ability to delete the resource they are managing helps in tightly controlling access to Oracle Cloud Infrastructure (OCI) services by implementing the separation of duties security principle.", + "ImpactStatement": "", + "RemediationProcedure": "**From Console:**1. Login to OCI console.2. Go to Identity -> Policies, In the compartment dropdown, choose the compartment. Open each policy to view the policy statements.3. Add the appropriate `where` condition to any policy statement that allows the storage service-level to manage the storage service.", + "AuditProcedure": "**From Console:**1. Login to OCI console.2. Go to Identity -> Policies, In the compartment dropdown, choose the compartment. 3. Open each policy to view the policy statements.4. Verify the policies to ensure that the policy statements that grant access to storage service-level administrators have a condition that excludes access to delete the service they are the administrator for.**From CLI:**1. Execute the following command:```for compid in `oci iam compartment list --compartment-id-in-subtree TRUE 2>/dev/null | jq -r '.data[] | .id'` do for policy in `oci iam policy list --compartment-id $compid 2>/dev/null | jq -r '.data[] | .id'` do output=`oci iam policy list --compartment-id $compid 2>/dev/null | jq -r '.data[] | .id, .name, .statements'` if [ ! -z $output ]; then echo $output; fi done done```2. Verify the policies to ensure that the policy statements that grant access to storage service-level administrators have a condition that excludes access to delete the service they are the administrator for.", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en/solutions/oci-best-practices/protect-data-rest1.html#GUID-939A5EA1-3057-48E0-9E02-ADAFCB82BA3E:https://docs.oracle.com/en-us/iaas/Content/Identity/policyreference/policyreference.htm:https://docs.oracle.com/en-us/iaas/Content/Block/home.htm:https://docs.oracle.com/en-us/iaas/Content/File/home.htm:https://docs.oracle.com/en-us/iaas/Content/Object/home.htm" + } + ] + }, + { + "Id": "1.16", + "Description": "Ensure OCI IAM credentials unused for 45 days or more are disabled", + "Checks": [], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "OCI IAM Local users can access OCI resources using different credentials, such as passwords or API keys. It is recommended that credentials that have been unused for 45 days or more be deactivated or removed.", + "RationaleStatement": "Disabling or removing unnecessary OCI IAM local users will reduce the window of opportunity for credentials associated with a compromised or abandoned account to be used.", + "ImpactStatement": "", + "RemediationProcedure": "**From Console:**1. Login to OCI Console.2. Select `Identity & Security` from the Services menu.3. Select Domains from the Identity menu.4. For each domain listed, click on the name and select `Users`.5. Click on an individual user under the `Username` heading.6. Click `More action`7. Select `Deactivate`**From CLI:**1. Create a input.json:```{ operations: [ { op: replace, path: active,value: false} ], schemas: [urn:ietf:params:scim:api:messages:2.0:PatchOp], userId: }```2. Execute the below:```oci identity-domains user patch --from-json file://file.json --endpoint ```", + "AuditProcedure": "Perform the following to determine if unused credentials exist:**From Console:**For Passwords:1. Login to OCI Console.2. Select `Identity & Security` from the Services menu.3. Select `Domains` from the `Identity` menu.4. For each domain listed, click on the name 5. Click `Reports`6. Under Dormant users report click `View report`7. Enter a date 45 days from today’s date in Last Successful Login Date8. Check and ensure that `Last Successful Login Date` is greater than 45 days or emptyFor API Keys:1. Login to OCI Console.2. Select `Observability & Management` from the Services menu.3. Select `Search` from `Logging` menu4. Click `Show Advanced Mode` in the right corner5. Select `Custom` from `Filter by time`6. Under `Select regions to search` add regions7. Under `Query` enter the following query in the text box:```search /_Audit_Include_Subcompartment | data.identity.credentials='//' | summarize count() by data.identity.principalId```8. Enter a day range - Note each query can only be 14 days multiple queries will be required to go 45 days9. Click `Search`10. Expand the results11. If results the count is not zero the user has used their API key during that period12. Repeat steps 8 – 11 for the 45-day period**From CLI:**For Passwords:1. Execute the below:```oci identity-domains users list --all --endpoint --attributes urn:ietf:params:scim:schemas:oracle:idcs:extension:userState:User:lastSuccessfulLoginDate --profile Oracle --query '.data.resources[]|.user-name + + .urn-ietf-params-scim-schemas-oracle-idcs-extension-user-state-user.last-successful-login-date'```2. Review the output the that the date is under 45 days, or no date means they have not logged inFor API Keys: 1. Create the search query text:```export query=search \\/_Audit_Include_Subcompartment\\ | data.identity.credentials='*' | summarize count() by data.identity.principalId```2. Select a day range. Date format is `2024-12-01`- Note each query can only be 14 days multiple queries will be required to go 45 days3. Execute the below:```oci logging-search search-logs --search-query $query --time-start --time-end --query 'data.results[0].data.count' export query=search \\/_Audit_Include_Subcompartment\\ | data.identity.credentials='*' | summarize count() by data.identity.principalId```4. If results the count is not zero, the user has used their API key during that period5. Repeat steps 2 – 4 for the 45-day period", + "AdditionalInformation": "This audit should exclude the OCI Administrator, break-glass accounts, and service accounts as these accounts should only be used for day-to-day business and would likely be unused for up to 45 days.", + "References": "" + } + ] + }, + { + "Id": "1.17", + "Description": "Ensure there is only one active API Key for any single OCI IAM user", + "Checks": [], + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "API Keys are long-term credentials for an OCI IAM user. They can be used to make programmatic requests to the OCI APIs directly or via, OCI SDKs or the OCI CLI.", + "RationaleStatement": "Having a single API Key for an OCI IAM reduces attack surface area and makes it easier to manage.", + "ImpactStatement": "Deletion of an OCI API Key will remove programmatic access to OCI APIs", + "RemediationProcedure": "**From Console:**1. Login to OCI Console.2. Select `Identity & Security` from the Services menu.3. Select `Domains` from the Identity menu.4. For each domain listed, click on the name and select Users.5. Click on an individual user under the Name heading.6. Click on `API Keys` in the lower left-hand corner of the page.7. Delete one of the API Keys **From CLI:**1. Follow the audit procedure above.2. For API Key ID to be removed execute the following command:```oci identity-domains api-key delete –api-key-id --endpoint ```", + "AuditProcedure": "**From Console:**1. Login to OCI Console.2. Select `Identity & Security` from the Services menu.3. Select `Users` from the Identity menu.4. Click on an individual user under the Name heading.5. Click on `API Keys` in the lower left-hand corner of the page.6. Ensure the has only has a one API Key**From CLI:**1. Each user and in each Identity Domain```oci raw-request --http-method GET --target-uri https:///admin/v1/ApiKeys?filter=user.ocid+eq+%%22 | jq '.data.Resources[] | \\(.fingerprint) \\(.id)'```2. Ensure only one key is returned", + "AdditionalInformation": "", + "References": "https://docs.public.oneportal.content.oci.oraclecloud.com/en-us/iaas/Content/Security/Reference/iam_security_topic-IAM_Credentials.htm#IAM_Credentials" + } + ] + }, + { + "Id": "2.1", + "Description": "Ensure no security lists allow ingress from 0.0.0.0/0 to port 22", + "Checks": [ + "network_security_list_ingress_from_internet_to_ssh_port" + ], + "Attributes": [ + { + "Section": "2. Networking", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Security lists provide stateful and stateless filtering of ingress and egress network traffic to OCI resources on a subnet level. It is recommended that no security list allows unrestricted ingress access to port 22.", + "RationaleStatement": "Removing unfettered connectivity to remote console services, such as Secure Shell (SSH), reduces a server's exposure to risk.", + "ImpactStatement": "For updating an existing environment, care should be taken to ensure that administrators currently relying on an existing ingress from 0.0.0.0/0 have access to ports 22 and/or 3389 through another network security group or security list.", + "RemediationProcedure": "**From Console:**1. Follow the audit procedure above.2. For each security list in the returned results, click the security list name3. Either edit the `ingress rule` to be more restrictive, delete the `ingress rule` or click on the `VCN` and terminate the `security list` as appropriate.**From CLI:**1. Follow the audit procedure.2. For each of the `security lists` identified, execute the following command:```oci network security-list get --security-list-id ```3. Then either: - Update the `security list` by copying the `ingress-security-rules` element from the JSON returned by the above command, edit it appropriately and use it in the following command:```oci network security-list update --security-list-id --ingress-security-rules ''``` or - Delete the security list with the following command:```oci network security-list delete --security-list-id ```", + "AuditProcedure": "**From Console:**1. Login to the OCI Console.2. Click the search bar at the top of the screen.3. Type `Advanced Resource Query` and hit `enter`.4. Click the `Advanced Resource Query` button in the upper right corner of the screen.5. Enter the following query in the query box:```query SecurityList resources where (IngressSecurityRules.source = '0.0.0.0/0' && IngressSecurityRules.protocol = 6 && IngressSecurityRules.tcpOptions.destinationPortRange.max >= 22 && IngressSecurityRules.tcpOptions.destinationPortRange.min =<= 22) ```6. Ensure the query returns no results.**From CLI:**1. Execute the following command:```oci search resource structured-search --query-text query SecurityList resources where (IngressSecurityRules.source = '0.0.0.0/0' && IngressSecurityRules.protocol = 6 && IngressSecurityRules.tcpOptions.destinationPortRange.max >= 22 && IngressSecurityRules.tcpOptions.destinationPortRange.min <= 22) ```2. Ensure the query returns no results.**Cloud Guard**Ensure Cloud Guard is enabled in the root compartment of the tenancy. For more information about enabling Cloud Guard, please look at the instructions included in Recommendation 3.15.**From Console:**1. Type `Cloud Guard` into the Search box at the top of the Console.2. Click `Cloud Guard` from the “Services” submenu.3. Click `Detector Recipes` in the Cloud Guard menu.4. Click `OCI Configuration Detector Recipe (Oracle Managed)` under the Recipe Name column.5. Find VCN Security list allows traffic to non-public port from all sources (0.0.0.0/0) in the Detector Rules column.6. Select the vertical ellipsis icon and chose Edit on the VCN Security list allows traffic to non-public port from all sources (0.0.0.0/0) row.7. In the Edit Detector Rule window find the Input Setting box and verify/add to the Restricted Protocol: Ports List setting to TCP:[22], UDP:[22].8. Click the `Save` button.**From CLI:**1. Update the VCN Security list allows traffic to non-public port from all sources (0.0.0.0/0) Detector Rule in Cloud Guard to generate Problems if a VCN security list allows public access via port 22 with the following command:```oci cloud-guard detector-recipe-detector-rule update --detector-recipe-id --detector-rule-id SECURITY_LISTS_OPEN_SOURCE --details '{configurations:[{ configKey : securityListsOpenSourceConfig, name : Restricted Protocol:Ports List, value : TCP:[22], UDP:[22], dataType : null, values : null }]}'```", + "AdditionalInformation": "", + "References": "" + } + ] + }, + { + "Id": "2.2", + "Description": "Ensure no security lists allow ingress from 0.0.0.0/0 to port 3389", + "Checks": [ + "network_security_list_ingress_from_internet_to_rdp_port" + ], + "Attributes": [ + { + "Section": "2. Networking", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Security lists provide stateful and stateless filtering of ingress and egress network traffic to OCI resources on a subnet level. It is recommended that no security group allows unrestricted ingress access to port 3389.", + "RationaleStatement": "Removing unfettered connectivity to remote console services, such as Remote Desktop Protocol (RDP), reduces a server's exposure to risk.", + "ImpactStatement": "For updating an existing environment, care should be taken to ensure that administrators currently relying on an existing ingress from 0.0.0.0/0 have access to ports 22 and/or 3389 through another network security group or security list.", + "RemediationProcedure": "**From Console:**1. Follow the audit procedure above.2. For each security list in the returned results, click the security list name3. Either edit the `ingress rule` to be more restrictive, delete the `ingress rule` or click on the `VCN` and terminate the `security list` as appropriate.**From CLI:**1. Follow the audit procedure.2. For each of the `security lists` identified, execute the following command:```oci network security-list get --security-list-id ```3. Then either: - Update the `security list` by copying the `ingress-security-rules` element from the JSON returned by the above command, edit it appropriately, and use it in the following command```oci network security-list update --security-list-id --ingress-security-rules ''``` or - Delete the security list with the following command:```oci network security-list delete --security-list-id ```", + "AuditProcedure": "**From Console:**1. Login into the OCI Console2. Click in the search bar at the top of the screen.3. Type `Advanced Resource Query` and hit `enter`.4. Click the `Advanced Resource Query` button in the upper right corner of the screen.5. Enter the following query in the query box:```query SecurityList resources where (IngressSecurityRules.source = '0.0.0.0/0' && IngressSecurityRules.protocol = 6 && IngressSecurityRules.tcpOptions.destinationPortRange.max >= 3389 && IngressSecurityRules.tcpOptions.destinationPortRange.min <= 3389) ```6. Ensure query returns no results.**From CLI:**1. Execute the following command:```oci search resource structured-search --query-text query SecurityList resources where (IngressSecurityRules.source = '0.0.0.0/0' && IngressSecurityRules.protocol = 6 && IngressSecurityRules.tcpOptions.destinationPortRange.max >= 3389 && IngressSecurityRules.tcpOptions.destinationPortRange.min <= 3389) ```2. Ensure query returns no results.**Cloud Guard**To Enable Cloud Guard Auditing:Ensure Cloud Guard is enabled in the root compartment of the tenancy. For more information about enabling Cloud Guard, please look at the instructions included in Recommendation 3.15. **From Console:**1. Type `Cloud Guard` into the Search box at the top of the Console .2. Click `Cloud Guard` from the “Services” submenu.3. Click `Detector Recipes` in the Cloud Guard menu.4. Click `OCI Configuration Detector Recipe (Oracle Managed)` under the Recipe Name column.5. Find VCN Security list allows traffic to non-public port from all sources (0.0.0.0/0) in the Detector Rules column.6. Select the vertical ellipsis icon and choose Edit on the VCN Security list allows traffic to non-public port from all sources (0.0.0.0/0) row.7. In the Edit Detector Rule window find the Input Setting box and verify/add to the Restricted Protocol: Ports List setting to TCP:[3389], UDP:[3389].8. Click the `Save` button.**From CLI:**1. Update the VCN Security list allows traffic to non-public port from all sources (0.0.0.0/0) Detector Rule in Cloud Guard to generate Problems if a VCN security list allows public access via port 3389 with the following command:```oci cloud-guard detector-recipe-detector-rule update --detector-recipe-id --detector-rule-id SECURITY_LISTS_OPEN_SOURCE --details '{configurations:[{ configKey : securityListsOpenSourceConfig, name : Restricted Protocol:Ports List, value : TCP:[3389], UDP:[3389], dataType : null, values : null }]}'```", + "AdditionalInformation": "This recommendation can also be audited programmatically using REST API https://docs.oracle.com/en-us/iaas/api/#/en/iaas/20160918/SecurityList/ListSecurityLists", + "References": "" + } + ] + }, + { + "Id": "2.3", + "Description": "Ensure no network security groups allow ingress from 0.0.0.0/0 to port 22", + "Checks": [ + "network_security_group_ingress_from_internet_to_ssh_port" + ], + "Attributes": [ + { + "Section": "2. Networking", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Network security groups provide stateful filtering of ingress/egress network traffic to OCI resources. It is recommended that no security group allows unrestricted ingress to port 22.", + "RationaleStatement": "Removing unfettered connectivity to remote console services, such as Secure Shell (SSH), reduces a server's exposure to risk.", + "ImpactStatement": "For updating an existing environment, care should be taken to ensure that administrators currently relying on an existing ingress from 0.0.0.0/0 have access to ports 22 and/or 3389 through another network security group or security list.", + "RemediationProcedure": "**From Console:** 1. Login into the OCI Console. 2. Click the search bar at the top of the screen. 3. Type Advanced Resource Query and hit enter. 4. Click the Advanced Resource Query button in the upper right corner of the screen. 5. Enter the following query in the query box: query networksecuritygroup resources where lifeCycleState = 'AVAILABLE' 6. For each of the network security groups in the returned results, click the name and inspect each of the security rules. 7. Remove all security rules with direction: Ingress, Source: 0.0.0.0/0, and Destination Port Range: 22.**From CLI:**Issue the following command and identify the security rule to remove.``` for region in `oci iam region list | jq -r '.data[] | .name'`; do for compid in `oci iam compartment list 2>/dev/null | jq -r '.data[] | .id'`; do for nsgid in `oci network nsg list --compartment-id $compid --region $region --all 2>/dev/null | jq -r '.data[] | .id'` do output=`oci network nsg rules list --nsg-id=$nsgid --all 2>/dev/null | jq -r '.data[] | select(.source == 0.0.0.0/0 and .direction == INGRESS and ((.tcp-options.destination-port-range.max >= 22 and .tcp-options.destination-port-range.min <= 22) or .tcp-options.destination-port-range == null))'` if [ ! -z $output ]; then echo NSGID=, $nsgid, Security Rules=, $output; fi done done done```- Remove the security rules```oci network nsg rules remove --nsg-id=```or- Update the security rules```oci network nsg rules update --nsg-id= --security-rules='[]'eg: oci network nsg rules update --nsg-id=ocid1.networksecuritygroup.oc1.iad.xxxxxxxxxxxxxxxxxxxxxx --security-rules='[{ description: null, destination: null, destination-type: null, direction: INGRESS, icmp-options: null, id: 709001, is-stateless: null, protocol: 6, source: 140.238.154.0/24, source-type: CIDR_BLOCK, tcp-options: { destination-port-range: { max: 22, min: 22 }, source-port-range: null }, udp-options: null }]'```", + "AuditProcedure": "**From Console:** 1. Login into the OCI Console. 2. Click the search bar at the top of the screen. 3. Type Advanced Resource Query and hit enter. 4. Click the Advanced Resource Query button in the upper right corner of the screen. 5. Enter the following query in the query box:```query networksecuritygroup resources where lifeCycleState = 'AVAILABLE'``` 6. For each of the network security groups in the returned results, click the name and inspect each of the security rules. 7. Ensure that there are no security rules with direction: Ingress, Source: 0.0.0.0/0, and Destination Port Range: 22.**From CLI:**Issue the following command, it should return no values.```for region in $(oci iam region-subscription list | jq -r '.data[] | .region-name') do echo Enumerating region $region for compid in $(oci iam compartment list --include-root --compartment-id-in-subtree TRUE 2>/dev/null | jq -r '.data[] | .id') do echo Enumerating compartment $compid for nsgid in $(oci network nsg list --compartment-id $compid --region $region --all 2>/dev/null | jq -r '.data[] | .id') do output=$(oci network nsg rules list --nsg-id=$nsgid --all 2>/dev/null | jq -r '.data[] | select(.source == 0.0.0.0/0 and .direction == INGRESS and ((.tcp-options.destination-port-range.max >= 22 and .tcp-options.destination-port-range.min <= 22) or .tcp-options.destination-port-range == null))') if [ ! -z $output ]; then echo NSGID: , $nsgid, Security Rules: , $output; fi done done done```**Cloud Guard:**To Enable Cloud Guard Auditing:Ensure Cloud Guard is enabled in the root compartment of the tenancy. For more information about enabling Cloud Guard, please look at the instructions included in Recommendation 3.15. **From Console:**1. Type `Cloud Guard` into the Search box at the top of the Console .2. Click `Cloud Guard` from the “Services” submenu.3. Click `Detector Recipes` in the Cloud Guard menu.4. Click `OCI Configuration Detector Recipe (Oracle Managed)` under the Recipe Name column.5. Find NSG ingress rule contains disallowed IP/port in the Detector Rules column.6. Select the vertical ellipsis icon and chose Edit on the NSG ingress rule contains disallowed IP/port row.7. In the Edit Detector Rule window find the Input Setting box and verify/add to the Restricted Protocol: Ports List setting to TCP:[22], UDP:[22].8. Click the `Save` button.**From CLI:**1. Update the NSG ingress rule contains disallowed IP/port Detector Rule in Cloud Guard to generate Problems if a network security group allows ingress network traffic to port 22 with the following command:```oci cloud-guard detector-recipe-detector-rule update --detector-recipe-id --detector-rule-id VCN_NSG_INGRESS_RULE_PORTS_CHECK --details '{configurations:[ {configKey : nsgIngressRuleDisallowedPortsConfig, name : Default disallowed ports, value : TCP:[22], UDP:[22], dataType : null, values : null }]}'```", + "AdditionalInformation": "", + "References": "" + } + ] + }, + { + "Id": "2.4", + "Description": "Ensure no network security groups allow ingress from 0.0.0.0/0 to port 3389", + "Checks": [ + "network_security_group_ingress_from_internet_to_rdp_port" + ], + "Attributes": [ + { + "Section": "2. Networking", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Network security groups provide stateful filtering of ingress/egress network traffic to OCI resources. It is recommended that no security group allows unrestricted ingress access to port 3389.", + "RationaleStatement": "Removing unfettered connectivity to remote console services, such as Remote Desktop Protocol (RDP), reduces a server's exposure to risk.", + "ImpactStatement": "For updating an existing environment, care should be taken to ensure that administrators currently relying on an existing ingress from 0.0.0.0/0 have access to ports 22 and/or 3389 through another network security group or security list.", + "RemediationProcedure": "**From CLI:**Using the details returned from the audit procedure either:- Remove the security rules```oci network nsg rules remove --nsg-id=```or- Update the security rules```oci network nsg rules update --nsg-id= --security-rules=eg: oci network nsg rules update --nsg-id=ocid1.networksecuritygroup.oc1.iad.xxxxxxxxxxxxxxxxxxxxxx --security-rules='[{ description: null, destination: null, destination-type: null, direction: INGRESS, icmp-options: null, id: 709001, is-stateless: null, protocol: 6, source: 140.238.154.0/24, source-type: CIDR_BLOCK, tcp-options: { destination-port-range: { max: 3389, min: 3389 }, source-port-range: null }, udp-options: null }]'```", + "AuditProcedure": "**From CLI:**Issue the following command, it should not return anything.``` for region in $(oci iam region-subscription list | jq -r '.data[] | .region-name') do echo Enumerating region $region for compid in $(oci iam compartment list --include-root --compartment-id-in-subtree TRUE 2>/dev/null | jq -r '.data[] | .id') do echo Enumerating compartment $compid for nsgid in $(oci network nsg list --compartment-id $compid --region $region --all 2>/dev/null | jq -r '.data[] | .id') do output=$(oci network nsg rules list --nsg-id=$nsgid --all 2>/dev/null | jq -r '.data[] | select(.source == 0.0.0.0/0 and .direction == INGRESS and ((.tcp-options.destination-port-range.max >= 3389 and .tcp-options.destination-port-range.min <= 3389) or .tcp-options.destination-port-range == null))') if [ ! -z $output ]; then echo NSGID: , $nsgid, Security Rules: , $output; fi done done done```**From Cloud Guard:**To Enable Cloud Guard Auditing:Ensure Cloud Guard is enabled in the root compartment of the tenancy. For more information about enabling Cloud Guard, please look at the instructions included in Recommendation 3.15. **From Console:**1. Type `Cloud Guard` into the Search box at the top of the Console.2. Click `Cloud Guard` from the “Services” submenu.3. Click `Detector Recipes` in the Cloud Guard menu.4. Click `OCI Configuration Detector Recipe (Oracle Managed)` under the Recipe Name column.5. Find NSG ingress rule contains disallowed IP/port in the Detector Rules column.6. Select the vertical ellipsis icon and chose Edit on the NSG ingress rule contains disallowed IP/port row.7. In the Edit Detector Rule window find the Input Setting box and verify/add to the Restricted Protocol: Ports List setting to TCP:[3389], UDP:[3389].8. Click the Save button.**From CLI:**1. Update the NSG ingress rule contains disallowed IP/port Detector Rule in Cloud Guard to generate Problems if a network security group allows ingress network traffic to port 3389 with the following command:```oci cloud-guard detector-recipe-detector-rule update --detector-recipe-id --detector-rule-id VCN_NSG_INGRESS_RULE_PORTS_CHECK --details '{configurations:[ {configKey : nsgIngressRuleDisallowedPortsConfig, name : Default disallowed ports, value : TCP:[3389], UDP:[3389], dataType : null, values : null }]}'```", + "AdditionalInformation": "", + "References": "" + } + ] + }, + { + "Id": "2.5", + "Description": "Ensure the default security list of every VCN restricts all traffic except ICMP", + "Checks": [ + "network_default_security_list_restricts_traffic" + ], + "Attributes": [ + { + "Section": "2. Networking", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "A default security list is created when a Virtual Cloud Network (VCN) is created and attached to the public subnets in the VCN. Security lists provide stateful or stateless filtering of ingress and egress network traffic to OCI resources in the VCN. It is recommended that the default security list does not allow unrestricted ingress and egress access to resources in the VCN.", + "RationaleStatement": "Removing unfettered connectivity to OCI resource, reduces a server's exposure to unauthorized access or data exfiltration.", + "ImpactStatement": "For updating existing environments Ingress rules with a source of 0.0.0.0/0, ensure that the necessary access is available through another Network Security Group or Security List.For updating existing environments Egress rules with a destination of 0.0.0.0/0 for an existing environment, ensure egress is covered via another Network Security Group, Security List, or through the stateful nature of the ingress rule.", + "RemediationProcedure": "**From Console:**1. Login into the OCI Console2. Click on `Networking -> Virtual Cloud Networks` from the services menu3. For each VCN listed `Click on Security Lists`4. Click on `Default Security List for `5. Identify the Ingress Rule with 'Source 0.0.0.0/0'6. Either Edit the Security rule to restrict the source and/or port range or delete the rule.7. Identify the Egress Rule with 'Destination 0.0.0.0/0, All Protocols'8. Either Edit the Security rule to restrict the source and/or port range or delete the rule.", + "AuditProcedure": "**From Console:**1. Login into the OCI Console2. Click on `Networking -> Virtual Cloud Networks` from the services menu3. For each VCN listed `Click on Security Lists`4. Click on `Default Security List for `5. Verify that there is no Ingress rule with 'Source 0.0.0.0/0'6. Verify that there is no Egress rule with 'Destination 0.0.0.0/0, All Protocols'", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en-us/iaas/Content/Security/Reference/networking_security.htm#Securing_Networking_VCN_Load_Balancers_and_DNS" + } + ] + }, + { + "Id": "2.6", + "Description": "Ensure Oracle Integration Cloud (OIC) access is restricted to allowed sources", + "Checks": [ + "integration_instance_access_restricted" + ], + "Attributes": [ + { + "Section": "2. Networking", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Oracle Integration (OIC) is a complete, secure, but lightweight integration solution that enables you to connect your applications in the cloud. It simplifies connectivity between your applications and connects both your applications that live in the cloud and your applications that still live on premises. Oracle Integration provides secure, enterprise-grade connectivity regardless of the applications you are connecting or where they reside. OIC instances are created within an Oracle managed secure private network with each having a public endpoint. The capability to configure ingress filtering of network traffic to protect your OIC instances from unauthorized network access is included. It is recommended that network access to your OIC instances be restricted to your approved corporate IP Addresses or Virtual Cloud Networks (VCN)s.", + "RationaleStatement": "Restricting connectivity to OIC Instances reduces an OIC instance’s exposure to risk.", + "ImpactStatement": "When updating ingress filters for an existing environment, care should be taken to ensure that IP addresses and VCNs currently used by administrators, users, and services to access your OIC instances are included in the updated filters.", + "RemediationProcedure": "**From Console:**1. Follow the audit procedure above.2. For each OIC instance in the returned results, click the OIC Instance name3. Click `Network Access`4. Either edit the `Network Access` to be more restrictive **From CLI**1. Follow the audit procedure.2. Get the json input format using the below command:```oci integration integration-instance change-network-endpoint --generate-param-json-input```3.For each of the OIC Instances identified get its details.4.Update the `Network Access`, copy the `network-endpoint-details` element from the JSON returned by the above get call, edit it appropriately and use it in the following command```Oci integration integration-instance change-network-endpoint --id --from-json ''```", + "AuditProcedure": "**From Console:**1. Login into the OCI Console2. Click in the search bar, top of the screen.3. Type Advanced Resource Query and hit enter.4. Click the Advanced Resource Query button in the upper right of the screen.5. Enter the following query in the query box:```query integrationinstance resources```6. For each OIC Instance returned click on the link under `Display name`7. Click on `Network Access`8 .Ensure `Restrict Network Access` is selected and the IP Address/CIDR Block as well as Virtual Cloud Networks are correct9. Repeat for other subscribed regions**From CLI:**1. Execute the following command:```for region in `oci iam region list | jq -r '.data[] | .name'`; do for compid in `oci iam compartment list --compartment-id-in-subtree TRUE 2>/dev/null | jq -r '.data[] | .id'` do output=`oci integration integration-instance list --compartment-id $compid --region $region --all 2>/dev/null | jq -r '.data[] | select(.network-endpoint-details.network-endpoint-type == null)'` if [ ! -z $output ]; then echo $output; fi done done```2. Ensure `allowlisted-http-ips` and `allowed-http-vcns` are correct", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en/cloud/paas/integration-cloud/integrations-user/get-started-integration-cloud-service.html" + } + ] + }, + { + "Id": "2.7", + "Description": "Ensure Oracle Analytics Cloud (OAC) access is restricted to allowed sources or deployed within a Virtual Cloud Network", + "Checks": [ + "analytics_instance_access_restricted" + ], + "Attributes": [ + { + "Section": "2. Networking", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Oracle Analytics Cloud (OAC) is a scalable and secure public cloud service that provides a full set of capabilities to explore and perform collaborative analytics for you, your workgroup, and your enterprise. OAC instances provide ingress filtering of network traffic or can be deployed with in an existing Virtual Cloud Network VCN. It is recommended that all new OAC instances be deployed within a VCN and that the Access Control Rules are restricted to your corporate IP Addresses or VCNs for existing OAC instances.", + "RationaleStatement": "Restricting connectivity to Oracle Analytics Cloud instances reduces an OAC instance’s exposure to risk.", + "ImpactStatement": "When updating ingress filters for an existing environment, care should be taken to ensure that IP addresses and VCNs currently used by administrators, users, and services to access your OAC instances are included in the updated filters. Also, these changes will temporarily bring the OAC instance offline.", + "RemediationProcedure": "**From Console:**1. Follow the audit procedure above.2. For each OAC instance in the returned results, click the OAC Instance name3. Click `Edit` next to `Access Control Rules`4. Click `+Another Rule` and add rules as required**From CLI:**1. Follow the audit procedure.2. Get the json input format by executing the below command:```oci analytics analytics-instance change-network-endpoint --generate-full-command-json-input```3. For each of the OAC Instances identified get its details.4. Update the `Access Control Rules`, copy the `network-endpoint-details` element from the JSON returned by the above get call, edit it appropriately and use it in the following command:```oci integration analytics-instance change-network-endpoint --from-json ''```", + "AuditProcedure": "**From Console:**1 Login into the OCI Console2. Click in the search bar, top of the screen.3. Type Advanced Resource Query and hit enter.4. Click the Advanced Resource Query button in the upper right of the screen.5. Enter the following query in the query box:```query analyticsinstance resources```6. For each OAC Instance returned click on the link under `Display name`.7. Ensure `Access Control Rules` IP Address/CIDR Block as well as Virtual Cloud Networks are correct.8. Repeat for other subscribed regions.**From CLI:**1. Execute the following command:```for region in `oci iam region list | jq -r '.data[] | .name'`; do for compid in `oci iam compartment list --compartment-id-in-subtree TRUE 2>/dev/null | jq -r '.data[] | .id'` do output=`oci analytics analytics-instance list --compartment-id $compid --region $region --all 2>/dev/null | jq -r '.data[] | select(.network-endpoint-details.network-endpoint-type == PUBLIC)'` if [ ! -z $output ]; then echo $output; fi done done```2. Ensure `network-endpoint-type` are correct.", + "AdditionalInformation": "https://docs.oracle.com/en/cloud/paas/analytics-cloud/acoci/manage-service-access-and-security.html#GUID-3DB25824-4417-4981-9EEC-29C0C6FD3883", + "References": "" + } + ] + }, + { + "Id": "2.8", + "Description": "Ensure Oracle Autonomous Shared Databases (ADB) access is restricted to allowed sources or deployed within a Virtual Cloud Network", + "Checks": [ + "database_autonomous_database_access_restricted" + ], + "Attributes": [ + { + "Section": "2. Networking", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Oracle Autonomous Database Shared (ADB-S) automates database tuning, security, backups, updates, and other routine management tasks traditionally performed by DBAs. ADB-S provide ingress filtering of network traffic or can be deployed within an existing Virtual Cloud Network (VCN). It is recommended that all new ADB-S databases be deployed within a VCN and that the Access Control Rules are restricted to your corporate IP Addresses or VCNs for existing ADB-S databases.", + "RationaleStatement": "Restricting connectivity to ADB-S Databases reduces an ADB-S database’s exposure to risk.", + "ImpactStatement": "When updating ingress filters for an existing environment, care should be taken to ensure that IP addresses and VCNs currently used by administrators, users, and services to access your ADB-S instances are included in the updated filters.", + "RemediationProcedure": "**From Console:**1. Follow the audit procedure above.2. For each ADB-S database in the returned results, click the ADB-S database name3. Click `Edit` next to `Access Control Rules`4. Click `+Another Rule` and add rules as required5. Click `Save Changes`**From CLI:**1. Follow the audit procedure.2. Get the json input format by executing the following command:```oci db autonomous-database update --generate-full-command-json-input```3. For each of the ADB-S Database identified get its details.4. Update the `whitelistIps`, copy the `WhiteListIPs` element from the JSON returned by the above get call, edit it appropriately and use it in the following command:```oci db autonomous-database update –-autonomous-database-id --from-json ''```", + "AuditProcedure": "**From Console:**1. Login into the OCI Console2. Click in the search bar, top of the screen.3. Type Advanced Resource Query and hit enter.4. Click the `Advanced Resource Query` button in the upper right of the screen.5. Enter the following query in the query box:```query autonomousdatabase resources```6. For each ABD-S database returned click on the link under `Display name`7. Click `Edit` next to `Access Control List`8. Ensure `Access Control Rules’ IP Address/CIDR Block as well as VCNs are correct9. Repeat for other subscribed regions**From CLI:**1. Execute the following command:```for region in `oci iam region list | jq -r '.data[] | .name'`; do for compid in `oci iam compartment list --compartment-id-in-subtree TRUE 2>/dev/null | jq -r '.data[] | .id'` do for adbid in `oci db autonomous-database list --compartment-id $compid --region $region --all 2>/dev/null | jq -r '.data[] | select(.nsg-ids == null).id'` do output=`oci db autonomous-database get --autonomous-database-id $adbid --region $region --query=data.{WhiteListIPs:\\whitelisted-ips\\,id:id} --output table 2>/dev/null` if [ ! -z $output ]; then echo $output; fi done done done```2. Ensure `WhiteListIPs` are correct.", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en/cloud/paas/autonomous-database/adbsa/network-access-options.html#GUID-29D62917-0F18-4F3E-8081-B3BD5C0C79F5" + } + ] + }, + { + "Id": "3.1", + "Description": "Ensure Compute Instance Legacy Metadata service endpoint is disabled", + "Checks": [ + "compute_instance_legacy_metadata_endpoint_disabled" + ], + "Attributes": [ + { + "Section": "3. Compute", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Compute Instances that utilize Legacy MetaData service endpoints (IMDSv1) are susceptible to potential SSRF attacks. To bolster security measures, it is strongly advised to reconfigure Compute Instances to adopt Instance Metadata Service v2, aligning with the industry's best security practices.", + "RationaleStatement": "Enabling Instance Metadata Service v2 enhances security and grants precise control over metadata access. Transitioning from IMDSv1 reduces the risk of SSRF attacks, bolstering system protection.IMDv1 poses security risks due to its inferior security measures and limited auditing capabilities. Transitioning to IMDv2 ensures a more secure environment with robust security features and improved monitoring capabilities.", + "ImpactStatement": "If you disable IMDSv1 on an instance that does not support IMDSv2, you might not be able to connect to the instance when you launch it.IMDSv2 is supported on the following platform images:- Oracle Autonomous Linux 8.x images- Oracle Autonomous Linux 7.x images released in June 2020 or later- Oracle Linux 8.x, Oracle Linux 7.x, and Oracle Linux 6.x images released in July 2020 or laterOther platform images, most custom images, and most Marketplace images do not support IMDSv2. Custom Linux images might support IMDSv2 if cloud-init is updated to version 20.3 or later and Oracle Cloud Agent is updated to version 0.0.19 or later. Custom Windows images might support IMDSv2 if Oracle Cloud Agent is updated to version 1.0.0.0 or later; cloudbase-init does not support IMDSv2.", + "RemediationProcedure": "**From Console:**1. Login to the OCI Console2. Click on the search box at the top of the console and search for compute instance name.3. Click on the instance name, In the `Instance Details` section, next to Instance Metadata Service, click `Edit`.4. For the `Instance metadata service`, select the `Version 2 only` option.5. Click `Save Changes`.Note : Disabling IMDSv1 on an incompatible instance may result in connectivity issues upon launch.To re-enable IMDSv1, follow these steps: 1. On the Instance Details page in the Console, click `Edit` next to Instance Metadata Service.2. Choose the `Version 1 and version 2` option, and save your changes.**From CLI:**Run Below Command,```oci compute instance update --instance-id [instance-ocid] --instance-options '{areLegacyImdsEndpointsDisabled :true}'```This will set Instance Metadata Service to use Version 2 Only.", + "AuditProcedure": "**From Console:**1. Login to the OCI Console2. Select compute instance in your compartment.3. Click on each instance name.4. In the `Instance Details` section, next to `Instance metadata service` make sure `Version 2 only` is selected.**From CLI:**1. Run command:```for region in `oci iam region-subscription list | jq -r '.data[] | .region-name'`; do for compid in `oci iam compartment list --compartment-id-in-subtree TRUE 2>/dev/null | jq -r '.data[] | .id'` do output=`oci compute instance list --compartment-id $compid --region $region --all 2>/dev/null | jq -r '.data[] | select(.instance-options.are-legacy-imds-endpoints-disabled == false )'` if [ ! -z $output ]; then echo $output; fi done done```2. No results should be returned", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/gettingmetadata.htm" + } + ] + }, + { + "Id": "3.2", + "Description": "Ensure Secure Boot is enabled on Compute Instance", + "Checks": [ + "compute_instance_secure_boot_enabled" + ], + "Attributes": [ + { + "Section": "3. Compute", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Shielded Instances with Secure Boot enabled prevents unauthorized boot loaders and operating systems from booting. This prevent rootkits, bootkits, and unauthorized software from running before the operating system loads.Secure Boot verifies the digital signature of the system's boot software to check its authenticity. The digital signature ensures the operating system has not been tampered with and is from a trusted source.When the system boots and attempts to execute the software, it will first check the digital signature to ensure validity. If the digital signature is not valid, the system will not allow the software to run.Secure Boot is a feature of UEFI(Unified Extensible Firmware Interface) that only allows approved operating systems to boot up.", + "RationaleStatement": "A Threat Actor with access to the operating system may seek to alter boot components to persist malware or rootkits during system initialization. Secure Boot helps ensure that the system only runs authentic software by verifying the digital signature of all boot components.", + "ImpactStatement": "An existing instance cannot be changed to a Shielded instance with Secure boot enabled. Shielded Secure Boot not available on all instance shapes and Operating systems. Additionally the following limitations exist:Thus to enable you have to terminate the instance and create a new one. Also, Shielded instances do not support live migration. During an infrastructure maintenance event, Oracle Cloud Infrastructure live migrates supported VM instances from the physical VM host that needs maintenance to a healthy VM host with minimal disruption to running instances. If you enable Secure Boot on an instance, the instance cannot be migrated, because the hardware TPM is not migratable. This may result in an outage because the TPM can't be migrate from a unhealthy host to healthy host.", + "RemediationProcedure": "Note: Secure Boot facility is available on selected VM images and Shapes in OCI. User have to configure Secured Boot at time of instance creation only.**From Console:**1. Navigate to https://cloud.oracle.com/compute/instances1. Select the instance from the Audit Procedure1. Click `Terminate`.1. Determine whether or not to permanently delete instance's attached boot volume.1. Click `Terminate instance`.1. Click on `Create Instance`.1. Select Image and Shape which supports Shielded Instance configuration. Icon for Shield in front of Image/Shape row indicates support of Shielded Instance.1. Click on `edit` of Security Blade.1. Turn On Shielded Instance, then Turn on the Secure Boot Toggle.1. Fill in the rest of the details as per requirements.1. Click `Create`.", + "AuditProcedure": "**From Console:**1. Login to the OCI Console2. Select compute instance in your compartment.3. Click on each instance name.4. In the `Launch Options` section,5. Check if `Secure Boot` is `Enabled`.**From CLI:**Run command:```for region in `oci iam region-subscription list | jq -r '.data[] | .region-name'`; do for compid in `oci iam compartment list --compartment-id-in-subtree TRUE 2>/dev/null | jq -r '.data[] | .id'` do output=`oci compute instance list --compartment-id $compid --region $region --all 2>/dev/null | jq -r '.data[] | select(.platform-config == null or platform-config.is-secure-boot-enabled == false )'` if [ ! -z $output ]; then echo $output; fi done done```In response, check if `platform-config` are not null and `is-secure-boot-enabled` is set to `true`", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en-us/iaas/Content/Compute/References/shielded-instances.htm:https://uefi.org/sites/default/files/resources/UEFI_Secure_Boot_in_Modern_Computer_Security_Solutions_2013.pdf" + } + ] + }, + { + "Id": "3.3", + "Description": "Ensure In-transit Encryption is enabled on Compute Instance", + "Checks": [ + "compute_instance_in_transit_encryption_enabled" + ], + "Attributes": [ + { + "Section": "3. Compute", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "The Block Volume service provides the option to enable in-transit encryption for paravirtualized volume attachments on virtual machine (VM) instances.", + "RationaleStatement": "All the data moving between the instance and the block volume is transferred over an internal and highly secure network. If you have specific compliance requirements related to the encryption of the data while it is moving between the instance and the block volume, you should enable the in-transit encryption option.", + "ImpactStatement": "In-transit encryption for boot and block volumes is only available for virtual machine (VM) instances launched from platform images, along with bare metal instances that use the following shapes: BM.Standard.E3.128, BM.Standard.E4.128, BM.DenseIO.E4.128. It is not supported on other bare metal instances.", + "RemediationProcedure": "**From Console:**1. Navigate to https://cloud.oracle.com/compute/instances1. Select the instance from the Audit Procedure1. Click `Terminate`.1. Determine whether or not to permanently delete instance's attached boot volume.1. Click `Terminate instance`.1. Click on `Create Instance`.1. Fill in the details as per requirements.1. In the `Boot volume` section ensure `Use in-transit encryption` is checked.1. Fill in the rest of the details as per requirements.1. Click `Create`.", + "AuditProcedure": "**From Console:**1. Go to [https://cloud.oracle.com/compute/instances](https://cloud.oracle.com/compute/instances)2. Select compute instance in your compartment.3. Click on each instance name.4. Click on `Boot volume` on the bottom left.5. Under the `In-transit encryption` column make sure it is `Enabled`**From CLI:**1. Execute the following:```for region in `oci iam region-subscription list | jq -r '.data[] | .region-name'`; do for compid in `oci iam compartment list --compartment-id-in-subtree TRUE 2>/dev/null | jq -r '.data[] | .id'` do output=`oci compute instance list --compartment-id $compid --region $region --all 2>/dev/null | jq -r '.data[] | select(.launch-options.is-pv-encryption-in-transit-enabled == false )'` if [ ! -z $output ]; then echo $output; fi done done```2. Ensure no results are returned", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en-us/iaas/Content/Block/Concepts/overview.htm#BlockVolumeEncryption__intransit" + } + ] + }, + { + "Id": "4.1", + "Description": "Ensure default tags are used on resources", + "Checks": [], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Using default tags is a way to ensure all resources that support tags are tagged during creation. Tags can be based on static or computed values. It is recommended to set up default tags early after root compartment creation to ensure all created resources will get tagged.Tags are scoped to Compartments and are inherited by Child Compartments. The recommendation is to create default tags like “CreatedBy” at the Root Compartment level to ensure all resources get tagged.When using Tags it is important to ensure that Tag Namespaces are protected by IAM Policies otherwise this will allow users to change tags or tag values.Depending on the age of the OCI Tenancy there may already be Tag defaults setup at the Root Level and no need for further action to implement this action.", + "RationaleStatement": "In the case of an incident having default tags like “CreatedBy” applied will provide info on who created the resource without having to search the Audit logs.", + "ImpactStatement": "There is no performance impact when enabling the above described features.", + "RemediationProcedure": "**From Console:**1. Login to OCI Console.2. From the navigation menu, select `Governance & Administration`.3. Under `Tenancy Management`, select `Tag Namespaces`.4. Under `Compartment`, select the root compartment.5. If no tag namespace exists, click `Create Tag Namespace`, enter a name and description and click `Create Tag Namespace`.6. Click the name of a tag namespace.7. Click `Create Tag Key Definition`.8. Enter a tag key (e.g. CreatedBy) and description, and click `Create Tag Key Definition`.9. From the navigation menu, select `Identity & Security`.10. Under `Identity`, select `Compartments`.11. Click the name of the root compartment.12. Under `Resources`, select `Tag Defaults`.13. Click `Create Tag Default`.14. Select a tag namespace, tag key, and enter `${iam.principal.name}` as the tag value.15. Click `Create`.**From CLI:**1. Create a Tag Namespace in the Root Compartment```oci iam tag-namespace create --compartment-id= --name= --description= --query data.{\\Tag Namespace OCID\\:id} --output table```2. Note the Tag Namespace OCID and use it when creating the Tag Key Definition```oci iam tag create --tag-namespace-id= --name= --description= --query data.{\\Tag Key Definition OCID\\:id} --output table```3. Note the Tag Key Definition OCID and use it when creating the Tag Default in the Root compartment```oci iam tag-default create --compartment-id= --tag-definition-id= --value=\\${iam.principal.name}```", + "AuditProcedure": "**From Console:**1. Login to OCI Console.2. From the navigation menu, select `Identity & Security`.3. Under `Identity`, select `Compartments`.4. Click the name of the root compartment.5. Under `Resources`, select `Tag Defaults`.6. In the `Tag Defaults` table, verify that there is a Tag with a value of `${iam.principal.name}` and a Tag Key Status of `Active`.Note: The name of the tag may be different then “CreatedBy” if the Tenancy Administrator has decided to use another tag.**From CLI:**1. List the active tag defaults defined at the Root compartment level by using the Tenancy OCID as compartment id.Note: The Tenancy OCID can be found in the `~/.oci/config` file used by the OCI Command Line Tool```oci iam tag-default list --compartment-id= --query=data [?\\lifecycle-state\\=='ACTIVE'].{name:\\tag-definition-name\\,value:value} --output table```2. Verify in the table returned that there is at least one row that contains the value of `${iam.principal.name}`.", + "AdditionalInformation": "'- There is no requirement to use the “Oracle-Tags” namespace to implement this control. A Tag Namespace Administrator can create any namespace and use it for this control.", + "References": "" + } + ] + }, + { + "Id": "4.2", + "Description": "Create at least one notification topic and subscription to receive monitoring alerts", + "Checks": [ + "events_notification_topic_and_subscription_exists" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Notifications provide a multi-channel messaging service that allow users and applications to be notified of events of interest occurring within OCI. Messages can be sent via eMail, HTTPs, PagerDuty, Slack or the OCI Function service. Some channels, such as eMail require confirmation of the subscription before it becomes active.", + "RationaleStatement": "Creating one or more notification topics allow administrators to be notified of relevant changes made to OCI infrastructure.", + "ImpactStatement": "There is no performance impact when enabling the above described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "**From Console:**1. Go to the Notifications Service page: [https://console.us-ashburn-1.oraclecloud.com/notification/topics](https://console.us-ashburn-1.oraclecloud.com/notification/topics)2. Select the `Compartment` that hosts the notifications3. Click `Create Topic`4. Set the `name` to something relevant5. Set the `description` to describe the purpose of the topic6. Click `Create`7. Click the newly created topic8. Click `Create Subscription`9. Choose the correct `protocol`10. Complete the correct parameter, for instance `email` address11. Click `Create`**From CLI:**1. Create a topic in a compartment```oci ons topic create --name --description --compartment-id ```2. Note the `OCID` of the `topic` using the `topic-id` field of the returned JSON and use it to create a new subscription```oci ons subscription create --compartment-id --topic-id --protocol --subscription-endpoint ```3. The returned JSON includes the id of the `subscription`.", + "AuditProcedure": "**From Console:**1. Go to the Notifications Service page: [https://console.us-ashburn-1.oraclecloud.com/notification/topics](https://console.us-ashburn-1.oraclecloud.com/notification/topics)2. Select the `Compartment` that hosts the notifications3. Find and click the `Topic` relevant to your monitoring alerts.4. Ensure a valid active subscription is shown.**From CLI:** 1. List the topics in the `Compartment` that hosts the notifications```oci ons topic list --compartment-id --all```2. Note the `OCID` of the monitoring topic(s) using the `topic-id` field of the returned JSON and use it to list the subscriptions```oci ons subscription list --compartment-id --topic-id --all```3. Ensure at least one active subscription is returned", + "AdditionalInformation": "'- The console URL shown is for the Ashburn region. Your tenancy might have a different home region and thus console URL.- The same Notification topic can be reused by many Events. A single topic can have multiple subscriptions allowing the same topic to be published to multiple locations.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "" + } + ] + }, + { + "Id": "4.3", + "Description": "Ensure a notification is configured for Identity Provider changes", + "Checks": [ + "events_rule_identity_provider_changes" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "It is recommended to setup an Event Rule and Notification that gets triggered when Identity Providers are created, updated or deleted. Event Rules are compartment scoped and will detect events in child compartments. It is recommended to create the Event rule at the root compartment level.", + "RationaleStatement": "OCI Identity Providers allow management of User ID / passwords in external systems and use of those credentials to access OCI resources. Identity Providers allow users to single sign-on to OCI console and have other OCI credentials like API Keys.Monitoring and alerting on changes to Identity Providers will help in identifying changes to the security posture.", + "ImpactStatement": "There is no performance impact when enabling the above described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "**From Console:**1. Go to the `Events Service` page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `compartment` that should host the rule3. Click `Create Rule`4. Provide a `Display Name` and `Description`5. Create a Rule Condition by selecting `Identity` in the Service Name Drop-down and selecting `Identity Provider – Create`, `Identity Provider - Delete and Identity Provider – Update`6. In the `Actions` section select `Notifications` as Action Type7. Select the `Compartment` that hosts the Topic to be used.8. Select the `Topic` to be used9. Optionally add Tags to the Rule10. Click `Create Rule`**From CLI:**1. Find the `topic-id` of the topic the Event Rule should use for sending notifications by using the topic `name` and `Compartment OCID````oci ons topic list --compartment-id --all --query data [?name==''].{name:name,topic_id:\\topic-id\\} --output table```2. Create a JSON file to be used when creating the Event Rule. Replace topic id, display name, description and compartment OCID.```{ actions: { actions: [ { actionType: ONS, isEnabled: true, topicId: }] }, condition:{\\eventType\\:[\\com.oraclecloud.identitycontrolplane.createidentityprovider\\,\\ com.oraclecloud.identitycontrolplane.deleteidentityprovider\\,\\ com.oraclecloud.identitycontrolplane.updateidentityprovider\\],\\data\\:{}}, displayName: , description: , isEnabled: true, compartmentId: }```3. Create the actual event rule```oci events rule create --from-json file://event_rule.json```4. Note in the JSON returned that it lists the parameters specified in the JSON file provided and that there is an OCID provided for the Event Rule", + "AuditProcedure": "**From Console:**1. Go to the Events Service page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `Compartment` that hosts the rules3. Find and click the `Rule` that handles `Identity Provider` Changes (if any)4. Click the `Edit Rule` button and verify that the `RuleConditions` section contains a condition for the Service `Identity` and Event Types: `Identity Provider – Create`, `Identity Provider - Delete` and `Identity Provider – Update`5. Verify that in the `Actions` section the Action Type contains: `Notifications` and that a valid `Topic` is referenced.**From CLI:** 1. Find the OCID of the specific Event Rule based on Display Name and Compartment OCID```oci events rule list --compartment-id --query data [?\\display-name\\==''].{id:id} --output table```2. List the details of a specific Event Rule based on the OCID of the rule.```oci events rule get --rule-id ```3. In the JSON output locate the Conditions key value pair and verify that the following Conditions are present:```com.oraclecloud.identitycontrolplane.createidentityprovidercom.oraclecloud.identitycontrolplane.deleteidentityprovidercom.oraclecloud.identitycontrolplane.updateidentityprovider```4. Verify the value of the `is-enabled` attribute is `true`5. In the JSON output verify that `actionType` is `ONS` and locate the `topic-id`6. Verify the correct topic is used by checking the topic name```oci ons topic get --topic-id --query data.{name:name} --output table```", + "AdditionalInformation": "'- The same Notification topic can be reused by many Event Rules.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "" + } + ] + }, + { + "Id": "4.4", + "Description": "Ensure a notification is configured for IdP group mapping changes", + "Checks": [ + "events_rule_idp_group_mapping_changes" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "It is recommended to setup an Event Rule and Notification that gets triggered when Identity Provider Group Mappings are created, updated or deleted. Event Rules are compartment scoped and will detect events in child compartments. It is recommended to create the Event rule at the root compartment level.", + "RationaleStatement": "IAM Policies govern access to all resources within an OCI Tenancy. IAM Policies use OCI Groups for assigning the privileges. Identity Provider Groups could be mapped to OCI Groups to assign privileges to federated users in OCI. Monitoring and alerting on changes to Identity Provider Group mappings will help in identifying changes to the security posture.", + "ImpactStatement": "There is no performance impact when enabling the above described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "**From Console:**1. Go to the `Events Service` page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `compartment` that should host the rule3. Click `Create Rule`4. Provide a `Display Name` and `Description`5. Create a Rule Condition by selecting `Identity` in the Service Name Drop-down and selecting `Idp Group Mapping – Create`, `Idp Group Mapping – Delete` and `Idp Group Mapping – Update`6. In the `Actions` section select `Notifications` as Action Type7. Select the `Compartment` that hosts the Topic to be used.8. Select the `Topic` to be used9. Optionally add Tags to the Rule10. Click `Create Rule`**From CLI:**1. Find the `topic-id` of the topic the Event Rule should use for sending notifications by using the topic `name` and `Compartment OCID````oci ons topic list --compartment-id --all --query data [?name==''].{name:name,topic_id:\\topic-id\\} --output table```2. Create a JSON file to be used when creating the Event Rule. Replace topic id, display name, description and compartment OCID.```{ actions: { actions: [ { actionType: ONS, isEnabled: true, topicId: }] }, condition:{\\eventType\\:[\\com.oraclecloud.identitycontrolplane.addidpgroupmapping\\,\\com.oraclecloud.identitycontrolplane.removeidpgroupmapping\\,\\com.oraclecloud.identitycontrolplane.updateidpgroupmapping\\],\\data\\:{}}, displayName: , description: , isEnabled: true, compartmentId: }```3. Create the actual event rule```oci events rule create --from-json file://event_rule.json```4. Note in the JSON returned that it lists the parameters specified in the JSON file provided and that there is an OCID provided for the Event Rule", + "AuditProcedure": "**From Console:**1. Go to the Events Service page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `Compartment` that hosts the rules3. Find and click the `Rule` that handles `Idp Group Mapping` Changes (if any)4. Ensure the `Rule` is `ACTIVE`5. Click the `Edit Rule` button and verify that the `RuleConditions` section contains a condition for the Service `Identity` and Event Types: `Idp Group Mapping – Create`, `Idp Group Mapping – Delete` and `Idp Group Mapping – Update`6. Verify that in the `Actions` section the Action Type contains: `Notifications` and that a valid `Topic` is referenced.**From CLI:** 1. Find the OCID of the specific Event Rule based on Display Name and Compartment OCID```oci events rule list --compartment-id --query data [?\\display-name\\==''].{id:id} --output table```2. List the details of a specific Event Rule based on the OCID of the rule.```oci events rule get --rule-id ```3. In the JSON output locate the Conditions key value pair and verify that the following Conditions are present:```com.oraclecloud.identitycontrolplane.addidpgroupmappingcom.oraclecloud.identitycontrolplane.removeidpgroupmappingcom.oraclecloud.identitycontrolplane.updateidpgroupmapping```4. Verify the value of the `is-enabled` attribute is `true`5. In the JSON output verify that `actionType` is `ONS` and locate the `topic-id`6. Verify the correct topic is used by checking the topic name```oci ons topic get --topic-id --query data.{name:name} --output table```", + "AdditionalInformation": "'- The same Notification topic can be reused by many Event Rules.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "" + } + ] + }, + { + "Id": "4.5", + "Description": "Ensure a notification is configured for IAM group changes", + "Checks": [ + "events_rule_iam_group_changes" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "It is recommended to setup an Event Rule and Notification that gets triggered when IAM Groups are created, updated or deleted. Event Rules are compartment scoped and will detect events in child compartments, it is recommended to create the Event rule at the root compartment level.", + "RationaleStatement": "IAM Groups control access to all resources within an OCI Tenancy. Monitoring and alerting on changes to IAM Groups will help in identifying changes to satisfy least privilege principle.", + "ImpactStatement": "There is no performance impact when enabling the above described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "**From Console:**1. Go to the `Events Service` page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `compartment` that should host the rule3. Click `Create Rule`4. Provide a `Display Name` and `Description`5. Create a Rule Condition by selecting `Identity` in the Service Name Drop-down and selecting `Group – Create`, `Group – Delete` and `Group – Update`6. In the `Actions` section select `Notifications` as Action Type7. Select the `Compartment` that hosts the Topic to be used.8. Select the `Topic` to be used9. Optionally add Tags to the Rule10. Click `Create Rule`**From CLI:**1. Find the `topic-id` of the topic the Event Rule should use for sending Notifications by using the topic `name` and `Compartment OCID````oci ons topic list --compartment-id --all --query data [?name==''].{name:name,topic_id:\\topic-id\\} --output table```2. Create a JSON file to be used when creating the Event Rule. Replace topic id, display name, description and compartment OCID.```{ actions: { actions: [ { actionType: ONS, isEnabled: true, topicId: }] }, condition: {\\eventType\\:[\\com.oraclecloud.identitycontrolplane.creategroup\\,\\com.oraclecloud.identitycontrolplane.deletegroup\\,\\com.oraclecloud.identitycontrolplane.updategroup\\],\\data\\:{}}, displayName: , description: , isEnabled: true, compartmentId: }```3. Create the actual event rule```oci events rule create --from-json file://event_rule.json```4. Note in the JSON returned that it lists the parameters specified in the JSON file provided and that there is an OCID provided for the Event Rule", + "AuditProcedure": "**From Console:**1. Go to the `Events Service` page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `Compartment` that hosts the rules3. Find and click the `Rule` that handles IAM `Group` Changes4. Click the `Edit Rule` button and verify that the `Rule Conditions` section contains a condition for the Service `Identity` and Event Types: `Group – Create`, `Group – Delete` and `Group – Update`5. Verify that in the `Actions` section the Action Type contains: `Notifications` and that a valid `Topic` is referenced.**From CLI:**1. Find the OCID of the specific Event Rule based on `Display Name` and `Compartment OCID````oci events rule list --compartment-id --query data [?\\display-name\\==''].{id:id} --output table```2. List the details of a specific Event Rule based on the OCID of the rule.```oci events rule get --rule-id ```3. In the JSON output locate the Conditions key value pair and verify that the following Conditions are present:```com.oraclecloud.identitycontrolplane.creategroupcom.oraclecloud.identitycontrolplane.deletegroupcom.oraclecloud.identitycontrolplane.updategroup```4. Verify the value of the `is-enabled` attribute is `true`5. In the JSON output verify that `actionType` is ONS and locate the `topic-id`6. Verify the correct topic is used by checking the topic name```oci ons topic get --topic-id --query data.{name:name} --output table```", + "AdditionalInformation": "'- The same Notification topic can be reused by many Event Rules.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "" + } + ] + }, + { + "Id": "4.6", + "Description": "Ensure a notification is configured for IAM policy changes", + "Checks": [ + "events_rule_iam_policy_changes" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "It is recommended to setup an Event Rule and Notification that gets triggered when IAM Policies are created, updated or deleted. Event Rules are compartment scoped and will detect events in child compartments, it is recommended to create the Event rule at the root compartment level.", + "RationaleStatement": "IAM Policies govern access to all resources within an OCI Tenancy. Monitoring and alerting on changes to IAM policies will help in identifying changes to the security posture.", + "ImpactStatement": "There is no performance impact when enabling the above described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "**From Console:**1. Go to the `Events Service` page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `compartment` that should host the rule3. Click `Create Rule`4. Provide a `Display Name` and `Description`5. Create a Rule Condition by selecting `Identity` in the Service Name Drop-down and selecting `Policy – Change Compartment`, `Policy – Create`, `Policy - Delete` and `Policy – Update`6. In the `Actions` section select `Notifications` as Action Type7. Select the `Compartment` that hosts the Topic to be used.8. Select the `Topic` to be used9. Optionally add Tags to the Rule10. Click `Create Rule`**From CLI:**1. Find the `topic-id` of the topic the Event Rule should use for sending Notifications by using the topic `name` and `Compartment OCID````oci ons topic list --compartment-id --all --query data [?name==''].{name:name,topic_id:\\topic-id\\} --output table```2. Create a JSON file to be used when creating the Event Rule. Replace topic id, display name, description and compartment OCID.```{ actions: { actions: [ { actionType: ONS, isEnabled: true, topicId: }] }, condition:{\\eventType\\:[\\com.oraclecloud.identitycontrolplane.createpolicy\\,\\com.oraclecloud.identitycontrolplane.deletepolicy\\,\\com.oraclecloud.identitycontrolplane.updatepolicy\\],\\data\\:{}}, displayName: , description: , isEnabled: true, compartmentId: }```3. Create the actual event rule```oci events rule create --from-json file://event_rule.json```4. Note in the JSON returned that it lists the parameters specified in the JSON file provided and that there is an OCID provided for the Event Rule", + "AuditProcedure": "**From Console:**1. Go to the Events Service page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `Compartment` that hosts the rules3. Find and click the `Rule` that handles `IAM Policy` Changes (if any)4. Click the `Edit Rule` button and verify that the `RuleConditions` section contains a condition for the Service `Identity` and Event Types: `Policy – Create`, ` Policy - Delete` and `Policy – Update`5. Verify that in the `Actions` section the Action Type contains: `Notifications` and that a valid `Topic` is referenced.**From CLI:** 1. Find the OCID of the specific Event Rule based on Display Name and Compartment OCID```oci events rule list --compartment-id --query data [?\\display-name\\==''].{id:id} --output table```2. List the details of a specific Event Rule based on the OCID of the rule.```oci events rule get --rule-id ```3. In the JSON output locate the Conditions key value pair and verify that the following Conditions are present:```com.oraclecloud.identitycontrolplane.createpolicycom.oraclecloud.identitycontrolplane.deletepolicycom.oraclecloud.identitycontrolplane.updatepolicy```4. Verify the value of the `is-enabled` attribute is `true`5. In the JSON output verify that `actionType` is `ONS` and locate the `topic-id`6. Verify the correct topic is used by checking the topic name```oci ons topic get --topic-id --query data.{name:name} --output table```", + "AdditionalInformation": "'- The same Notification topic can be reused by many Event Rules.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "" + } + ] + }, + { + "Id": "4.7", + "Description": "Ensure a notification is configured for user changes", + "Checks": [ + "events_rule_user_changes" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "It is recommended to setup an Event Rule and Notification that gets triggered when IAM Users are created, updated, deleted, capabilities updated, or state updated. Event Rules are compartment scoped and will detect events in child compartments, it is recommended to create the Event rule at the root compartment level.", + "RationaleStatement": "Users use or manage Oracle Cloud Infrastructure resources. Monitoring and alerting on changes to Users will help in identifying changes to the security posture.", + "ImpactStatement": "There is no performance impact when enabling the above described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "**From Console:**1. Using the search box to navigate to `events`2. Navigate to the `rules` page3. Select the `compartment` that should host the rule4. Click `Create Rule`5. Provide a `Display Name` and `Description`6. Create a Rule Condition by selecting `Identity` in the Service Name Drop-down and selecting:`User – Create`, `User – Delete`, `User – Update`, `User Capabilities – Update`,`User State – Update` 7. In the `Actions` section select `Notifications` as Action Type8. Select the `Compartment` that hosts the Topic to be used.9. Select the `Topic` to be used10. Optionally add Tags to the Rule11. Click `Create Rule`**From CLI:**1. Find the `topic-id` of the topic the Event Rule should use for sending Notifications by using the topic `name` and `Compartment OCID````oci ons topic list --compartment-id --all --query data [?name==''].{name:name,topic_id:\\topic-id\\} --output table```2. Create a JSON file to be used when creating the Event Rule. Replace topic id, display name, description and compartment OCID.```{ actions: { actions: [ { actionType: ONS, isEnabled: true, topicId: }] }, condition: {\\eventType\\:[\\com.oraclecloud.identitycontrolplane.createuser\\,\\com.oraclecloud.identitycontrolplane.deleteuser\\,\\com.oraclecloud.identitycontrolplane.updateuser\\,\\com.oraclecloud.identitycontrolplane.updateusercapabilities\\,\\com.oraclecloud.identitycontrolplane.updateuserstate\\],\\data\\:{}}, displayName: , description: , isEnabled: true, compartmentId: }```3. Create the actual event rule```oci events rule create --from-json file://event_rule.json```4. Note in the JSON returned that it lists the parameters specified in the JSON file provided and that there is an OCID provided for the Event Rule", + "AuditProcedure": "**From Console:**1. Using the search box to navigate to `events`2. Navigate to the `rules` page3. Select the `Compartment` that hosts the rules4. Find and click the `Rule` that handles `IAM User` Changes5. Click the `Edit Rule` button and verify that the `Rule Conditions` section contains a condition for the Service `Identity` and Event Types: `User – Create`, `User – Delete`, `User – Update`, `User Capabilities – Update`,`User State – Update` 6. Verify that in the `Actions` section the Action Type contains: `Notifications` and that a valid `Topic` is referenced.**From CLI:**1. Find the OCID of the specific Event Rule based on `Display Name` and `Compartment OCID````oci events rule list --compartment-id --query data [?\\display-name\\==''].{id:id} --output table```2. List the details of a specific Event Rule based on the OCID of the rule.```oci events rule get --rule-id ```3. In the JSON output locate the Conditions key value pair and verify that the following Conditions are present:```com.oraclecloud.identitycontrolplane.createusercom.oraclecloud.identitycontrolplane.deleteusercom.oraclecloud.identitycontrolplane.updateusercom.oraclecloud.identitycontrolplane.updateusercapabilitiescom.oraclecloud.identitycontrolplane.updateuserstate```4. Verify the value of the `is-enabled` attribute is `true`5. In the JSON output verify that `actionType` is ONS and locate the `topic-id`6. Verify the correct topic is used by checking the topic name```oci ons topic get --topic-id --query data.{name:name} --output table```", + "AdditionalInformation": "'- The same Notification topic can be reused by many Event Rules.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "" + } + ] + }, + { + "Id": "4.8", + "Description": "Ensure a notification is configured for VCN changes", + "Checks": [ + "events_rule_vcn_changes" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "It is recommended to setup an Event Rule and Notification that gets triggered when Virtual Cloud Networks are created, updated or deleted. Event Rules are compartment scoped and will detect events in child compartments, it is recommended to create the Event rule at the root compartment level.", + "RationaleStatement": "Virtual Cloud Networks (VCNs) closely resembles a traditional network. Monitoring and alerting on changes to VCNs will help in identifying changes to the security posture.", + "ImpactStatement": "There is no performance impact when enabling the above described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "**From Console:**1. Go to the `Events Service` page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `compartment` that should host the rule3. Click `Create Rule`4. Provide a `Display Name` and `Description`5. Create a Rule Condition by selecting `Networking` in the Service Name Drop-down and selecting `VCN – Create`, ` VCN - Delete and VCN – Update`6. In the `Actions` section select `Notifications` as Action Type7. Select the `Compartment` that hosts the Topic to be used.8. Select the `Topic` to be used9. Optionally add Tags to the Rule10. Click `Create Rule`**From CLI:**1. Find the `topic-id` of the topic the Event Rule should use for sending Notifications by using the topic `name` and `Compartment OCID````oci ons topic list --compartment-id --all --query data [?name==''].{name:name,topic_id:\\topic-id\\} --output table```2. Create a JSON file to be used when creating the Event Rule. Replace topic id, display name, description and compartment OCID.```{ actions: { actions: [ { actionType: ONS, isEnabled: true, topicId: }] }, condition:{\\eventType\\:[\\com.oraclecloud.virtualnetwork.createvcn\\,\\com.oraclecloud.virtualnetwork.deletevcn\\,\\com.oraclecloud.virtualnetwork.updatevcn\\],\\data\\:{}}, displayName: , description: , isEnabled: true, compartmentId: }```3. Create the actual event rule```oci events rule create --from-json file://event_rule.json```4. Note in the JSON returned that it lists the parameters specified in the JSON file provided and that there is an OCID provided for the Event Rule", + "AuditProcedure": "**From Console:**1. Go to the Events Service page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `Compartment` that hosts the rules3. Find and click the `Rule` that handles `VCN` Changes (if any)4. Click the `Edit Rule` button and verify that the `RuleConditions` section contains a condition for the Service `Networking` and Event Types: `VCN – Create`, ` VCN - Delete and VCN – Update`5. Verify that in the `Actions` section the Action Type contains: `Notifications` and that a valid `Topic` is referenced.**From CLI:**1. Find the OCID of the specific Event Rule based on Display Name and Compartment OCID```oci events rule list --compartment-id --query data [?\\display-name\\==''].{id:id} --output table```2. List the details of a specific Event Rule based on the OCID of the rule.```oci events rule get --rule-id ```3. In the JSON output locate the Conditions key value pair and verify that the following Conditions are present:```com.oraclecloud.virtualnetwork.createvcncom.oraclecloud.virtualnetwork.deletevcncom.oraclecloud.virtualnetwork.updatevcn```4. Verify the value of the `is-enabled` attribute is `true`5. In the JSON output verify that `actionType` is `ONS` and locate the `topic-id`6. Verify the correct topic is used by checking the topic name```oci ons topic get --topic-id --query data.{name:name} --output table```", + "AdditionalInformation": "'- The same Notification topic can be reused by many Event Rules.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "" + } + ] + }, + { + "Id": "4.9", + "Description": "Ensure a notification is configured for changes to route tables", + "Checks": [ + "events_rule_route_table_changes" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "It is recommended to setup an Event Rule and Notification that gets triggered when route tables are created, updated or deleted. Event Rules are compartment scoped and will detect events in child compartments, it is recommended to create the Event rule at the root compartment level.", + "RationaleStatement": "Route tables control traffic flowing to or from Virtual Cloud Networks and Subnets. Monitoring and alerting on changes to route tables will help in identifying changes these traffic flows.", + "ImpactStatement": "There is no performance impact when enabling the above described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "**From Console:**1. Go to the `Events Service` page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `compartment` that should host the rule3. Click `Create Rule`4. Provide a `Display Name` and `Description`5. Create a Rule Condition by selecting `Networking` in the Service Name Drop-down and selecting `Route Table – Change Compartment`, `Route Table – Create`, `Route Table - Delete` and `Route Table – Update`6. In the `Actions` section select `Notifications` as Action Type7. Select the `Compartment` that hosts the Topic to be used.8. Select the `Topic` to be used9. Optionally add Tags to the Rule10. Click `Create Rule`**From CLI:**1. Find the `topic-id` of the topic the Event Rule should use for sending Notifications by using the topic `name` and `Compartment OCID````oci ons topic list --compartment-id --all --query data [?name==''].{name:name,topic_id:\\topic-id\\} --output table```2. Create a JSON file to be used when creating the Event Rule. Replace topic id, display name, description and compartment OCID.```{ actions: { actions: [ { actionType: ONS, isEnabled: true, topicId: }] }, condition:{\\eventType\\:[\\com.oraclecloud.virtualnetwork.changeroutetablecompartment\\,\\com.oraclecloud.virtualnetwork.createroutetable\\,\\com.oraclecloud.virtualnetwork.deleteroutetable\\,\\com.oraclecloud.virtualnetwork.updateroutetable\\],\\data\\:{}}, displayName: , description: , isEnabled: true, compartmentId: }```3. Create the actual event rule```oci events rule create --from-json file://event_rule.json```4. Note in the JSON returned that it lists the parameters specified in the JSON file provided and that there is an OCID provided for the Event Rule", + "AuditProcedure": "**From Console:**1. Go to the Events Service page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `Compartment` that hosts the rules3. Find and click the `Rule` that handles `Route Table` Changes (if any)4. Click the `Edit Rule` button and verify that the `RuleConditions` section contains a condition for the Service `Networking` and Event Types: `Route Table – Change Compartment`, `Route Table – Create`, ` Route Table - Delete` and `Route Table - Update`5. Verify that in the `Actions` section the Action Type contains: `Notifications` and that a valid `Topic` is referenced.**From CLI:**1. Find the OCID of the specific Event Rule based on Display Name and Compartment OCID```oci events rule list --compartment-id --query data [?\\display-name\\==''].{id:id} --output table```2. List the details of a specific Event Rule based on the OCID of the rule.```oci events rule get --rule-id ```3. In the JSON output locate the Conditions key value pair and verify that the following Conditions are present:```com.oraclecloud.virtualnetwork.changeroutetablecompartmentcom.oraclecloud.virtualnetwork.createroutetablecom.oraclecloud.virtualnetwork.deleteroutetablecom.oraclecloud.virtualnetwork.updateroutetable```4. Verify the value of the `is-enabled` attribute is `true`5. In the JSON output verify that `actionType` is `ONS` and locate the `topic-id`6. Verify the correct topic is used by checking the topic name```oci ons topic get --topic-id --query data.{name:name} --output table```", + "AdditionalInformation": "'- The same Notification topic can be reused by many Event Rules.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "" + } + ] + }, + { + "Id": "4.10", + "Description": "Ensure a notification is configured for security list changes", + "Checks": [ + "events_rule_security_list_changes" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "It is recommended to setup an Event Rule and Notification that gets triggered when security lists are created, updated or deleted. Event Rules are compartment scoped and will detect events in child compartments, it is recommended to create the Event rule at the root compartment level.", + "RationaleStatement": "Security Lists control traffic flowing into and out of Subnets within a Virtual Cloud Network. Monitoring and alerting on changes to Security Lists will help in identifying changes to these security controls.", + "ImpactStatement": "There is no performance impact when enabling the above described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "**From Console:**1. Go to the `Events Service` page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `compartment` that should host the rule3. Click `Create Rule`4. Provide a `Display Name` and `Description`5. Create a Rule Condition by selecting `Networking` in the Service Name Drop-down and selecting `Security List – Change Compartment`, `Security List – Create`, `Security List - Delete` and `Security List – Update`6. In the `Actions` section select `Notifications` as Action Type7. Select the `Compartment` that hosts the Topic to be used.8. Select the `Topic` to be used9. Optionally add Tags to the Rule10. Click `Create Rule`**From CLI:**1. Find the `topic-id` of the topic the Event Rule should use for sending Notifications by using the topic `name` and `Compartment OCID````oci ons topic list --compartment-id --all --query data [?name==''].{name:name,topic_id:\\topic-id\\} --output table```2. Create a JSON file to be used when creating the Event Rule. Replace topic-id, display name, description and compartment OCID.```{ actions: { actions: [ { actionType: ONS, isEnabled: true, topicId: }] }, condition:{\\eventType\\:[\\com.oraclecloud.virtualnetwork.changesecuritylistcompartment\\,\\com.oraclecloud.virtualnetwork.createsecuritylist\\,\\com.oraclecloud.virtualnetwork.deletesecuritylist\\,\\com.oraclecloud.virtualnetwork.updatesecuritylist\\],\\data\\:{}}, displayName: , description: , isEnabled: true, compartmentId: }```3. Create the actual event rule```oci events rule create --from-json file://event_rule.json```4. Note in the JSON returned that it lists the parameters specified in the JSON file provided and that there is an OCID provided for the Event Rule", + "AuditProcedure": "**From Console:**1. Go to the Events Service page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `Compartment` that hosts the rules3. Find and click the `Rule` that handles `Security List` Changes (if any)4. Click the `Edit Rule` button and verify that the `RuleConditions` section contains a condition for the Service `Networking` and Event Types: `Security List – Change Compartment`, `Security List – Create`, `Security List - Delete` and `Security List – Update`5. Verify that in the `Actions` section the Action Type contains: `Notifications` and that a valid `Topic` is referenced.**From CLI:**1. Find the OCID of the specific Event Rule based on Display Name and Compartment OCID```oci events rule list --compartment-id --query data [?\\display-name\\==''].{id:id} --output table```2. List the details of a specific Event Rule based on the OCID of the rule.```oci events rule get --rule-id ```3. In the JSON output locate the Conditions key value pair and verify that the following Conditions are present:```com.oraclecloud.virtualnetwork.changesecuritylistcompartmentcom.oraclecloud.virtualnetwork.createsecuritylistcom.oraclecloud.virtualnetwork.deletesecuritylistcom.oraclecloud.virtualnetwork.updatesecuritylist```4. Verify the value of the `is-enabled` attribute is `true`5. In the JSON output verify that `actionType` is `ONS` and locate the `topic-id`6. Verify the correct topic is used by checking the topic name```oci ons topic get --topic-id --query data.{name:name} --output table```", + "AdditionalInformation": "'- The same Notification topic can be reused by many Event Rules.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "" + } + ] + }, + { + "Id": "4.11", + "Description": "Ensure a notification is configured for network security group changes", + "Checks": [ + "events_rule_network_security_group_changes" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "It is recommended to setup an Event Rule and Notification that gets triggered when network security groups are created, updated or deleted. Event Rules are compartment scoped and will detect events in child compartments, it is recommended to create the Event rule at the root compartment level.", + "RationaleStatement": "Network Security Groups control traffic flowing between Virtual Network Cards attached to Compute instances. Monitoring and alerting on changes to Network Security Groups will help in identifying changes these security controls.", + "ImpactStatement": "There is no performance impact when enabling the above described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "**From Console:**1. Go to the `Events Service` page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `compartment` that should host the rule3. Click `Create Rule`4. Provide a `Display Name` and `Description`5. Create a Rule Condition by selecting `Networking` in the Service Name Drop-down and selecting `Network Security Group – Change Compartment`, `Network Security Group – Create`, `Network Security Group - Delete` and `Network Security Group – Update`6. In the `Actions` section select `Notifications` as Action Type7. Select the `Compartment` that hosts the Topic to be used.8. Select the `Topic` to be used9. Optionally add Tags to the Rule10. Click `Create Rule`**From CLI:**1. Find the `topic-id` of the topic the Event Rule should use for sending Notifications by using the topic `name` and `Compartment OCID````oci ons topic list --compartment-id --all --query data [?name==''].{name:name,topic_id:\\topic-id\\} --output table```2. Create a JSON file to be used when creating the Event Rule. Replace topic id, display name, description and compartment OCID.```{ actions: { actions: [ { actionType: ONS, isEnabled: true, topicId: } ] }, condition:{\\eventType\\:[\\com.oraclecloud.virtualnetwork.changenetworksecuritygroupcompartment\\,\\com.oraclecloud.virtualnetwork.createnetworksecuritygroup\\,\\com.oraclecloud.virtualnetwork.deletenetworksecuritygroup\\,\\com.oraclecloud.virtualnetwork.updatenetworksecuritygroup\\],\\data\\:{}}, displayName: , description: , isEnabled: true, compartmentId: }```3. Create the actual event rule```oci events rule create --from-json file://event_rule.json```4. Note in the JSON returned that it lists the parameters specified in the JSON file provided and that there is an OCID provided for the Event Rule", + "AuditProcedure": "**From Console:**1. Go to the Events Service page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `Compartment` that hosts the rules3. Find and click the `Rule` that handles `Network Security Group` Changes (if any)4. Click the `Edit Rule` button and verify that the `RuleConditions` section contains a condition for the Service `Networking` and Event Types: `Network Security Group – Change Compartment`, `Network Security Group – Create`, `Network Security Group - Delete` and `Network Security Group – Update`5. Verify that in the `Actions` section the Action Type contains: `Notifications` and that a valid `Topic` is referenced.**From CLI:**1. Find the OCID of the specific Event Rule based on Display Name and Compartment OCID```oci events rule list --compartment-id --query data [?\\display-name\\==''].{id:id} --output table```2. List the details of a specific Event Rule based on the OCID of the rule.```oci events rule get --rule-id ```3. In the JSON output locate the Conditions key value pair and verify that the following conditions are present:```com.oraclecloud.virtualnetwork.changenetworksecuritygroupcompartmentcom.oraclecloud.virtualnetwork.createnetworksecuritygroupcom.oraclecloud.virtualnetwork.deletenetworksecuritygroupcom.oraclecloud.virtualnetwork.updatenetworksecuritygroup```4. Verify the value of the `is-enabled` attribute is `true`5. In the JSON output verify that `actionType` is `ONS` and locate the `topic-id`6. Verify the correct topic is used by checking the topic name```oci ons topic get --topic-id --query data.{name:name} --output table```", + "AdditionalInformation": "'- The same Notification topic can be reused by many Event Rules.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "" + } + ] + }, + { + "Id": "4.12", + "Description": "Ensure a notification is configured for changes to network gateways", + "Checks": [ + "events_rule_network_gateway_changes" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "It is recommended to setup an Event Rule and Notification that gets triggered when Network Gateways are created, updated, deleted, attached, detached, or moved. This recommendation includes Internet Gateways, Dynamic Routing Gateways, Service Gateways, Local Peering Gateways, and NAT Gateways. Event Rules are compartment scoped and will detect events in child compartments, it is recommended to create the Event rule at the root compartment level.", + "RationaleStatement": "Network Gateways act as routers between VCNs and the Internet, Oracle Services Networks, other VCNS, and on-premise networks.Monitoring and alerting on changes to Network Gateways will help in identifying changes to the security posture.", + "ImpactStatement": "There is no performance impact when enabling the above described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "**From Console:**1. Go to the `Events Service` page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `compartment` that should host the rule3. Click `Create Rule`4. Provide a `Display Name` and `Description`5. Create a Rule Condition by selecting `Networking` in the Service Name Drop-down and selecting:```DRG – CreateDRG – DeleteDRG – UpdateDRG Attachment – CreateDRG Attachment – DeleteDRG Attachment – UpdateInternet Gateway – CreateInternet Gateway – DeleteInternet Gateway – UpdateInternet Gateway – Change CompartmentLocal Peering Gateway – CreateLocal Peering Gateway – Delete EndLocal Peering Gateway – UpdateLocal Peering Gateway – Change CompartmentNAT Gateway – CreateNAT Gateway – DeleteNAT Gateway – UpdateNAT Gateway – Change CompartmentService Gateway – CreateService Gateway – Delete EndService Gateway – UpdateService Gateway – Attach ServiceService Gateway – Detach ServiceService Gateway – Change Compartment```6. In the `Actions` section select `Notifications` as Action Type7. Select the `Compartment` that hosts the Topic to be used.8. Select the `Topic` to be used9. Optionally add Tags to the Rule10. Click `Create Rule`**From CLI:**1. Find the `topic-id` of the topic the Event Rule should use for sending Notifications by using the topic `name` and `Compartment OCID````oci ons topic list --compartment-id --all --query data [?name==''].{name:name,topic_id:\\topic-id\\} --output table```2. Create a JSON file to be used when creating the Event Rule. Replace topic id, display name, description and compartment OCID.```{ actions: { actions: [ { actionType: ONS, isEnabled: true, topicId: } ] }, condition:{\\eventType\\:[\\com.oraclecloud.virtualnetwork.createdrg\\,\\com.oraclecloud.virtualnetwork.deletedrg\\,\\com.oraclecloud.virtualnetwork.updatedrg\\,\\com.oraclecloud.virtualnetwork.createdrgattachment\\,\\com.oraclecloud.virtualnetwork.deletedrgattachment\\,\\com.oraclecloud.virtualnetwork.updatedrgattachment\\,\\com.oraclecloud.virtualnetwork.changeinternetgatewaycompartment\\,\\com.oraclecloud.virtualnetwork.createinternetgateway\\,\\com.oraclecloud.virtualnetwork.deleteinternetgateway\\,\\com.oraclecloud.virtualnetwork.updateinternetgateway\\,\\com.oraclecloud.virtualnetwork.changelocalpeeringgatewaycompartment\\,\\com.oraclecloud.virtualnetwork.createlocalpeeringgateway\\,\\com.oraclecloud.virtualnetwork.deletelocalpeeringgateway.end\\,\\com.oraclecloud.virtualnetwork.updatelocalpeeringgateway\\,\\com.oraclecloud.natgateway.changenatgatewaycompartment\\,\\com.oraclecloud.natgateway.createnatgateway\\,\\com.oraclecloud.natgateway.deletenatgateway\\,\\com.oraclecloud.natgateway.updatenatgateway\\,\\com.oraclecloud.servicegateway.attachserviceid\\,\\com.oraclecloud.servicegateway.changeservicegatewaycompartment\\,\\com.oraclecloud.servicegateway.createservicegateway\\,\\com.oraclecloud.servicegateway.deleteservicegateway.end\\,\\com.oraclecloud.servicegateway.detachserviceid\\,\\com.oraclecloud.servicegateway.updateservicegateway\\],\\data\\:{}}, displayName: , description: , isEnabled: true, compartmentId: }```3. Create the actual event rule```oci events rule create --from-json file://event_rule.json```4. Note in the JSON returned that it lists the parameters specified in the JSON file provided and that there is an OCID provided for the Event Rule", + "AuditProcedure": "**From Console:**1. Go to the Events Service page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `Compartment` that hosts the rules3. Find and click the `Rule` that handles `Network Gateways` Changes (if any)4. Click the `Edit Rule` button and verify that the `RuleConditions` section contains a condition for the Service `Networking` and Event Types: ```DRG – CreateDRG – DeleteDRG – UpdateDRG Attachment – CreateDRG Attachment – DeleteDRG Attachment – UpdateInternet Gateway – CreateInternet Gateway – DeleteInternet Gateway – UpdateInternet Gateway – Change CompartmentLocal Peering Gateway – CreateLocal Peering Gateway – Delete EndLocal Peering Gateway – UpdateLocal Peering Gateway – Change CompartmentNAT Gateway – CreateNAT Gateway – DeleteNAT Gateway – UpdateNAT Gateway – Change CompartmentService Gateway – CreateService Gateway – Delete EndService Gateway – UpdateService Gateway – Attach ServiceService Gateway – Detach ServiceService Gateway – Change Compartment```5. Verify that in the `Actions` section the Action Type contains: `Notifications` and that a valid `Topic` is referenced.**From CLI:**1. Find the OCID of the specific Event Rule based on Display Name and Compartment OCID```oci events rule list --compartment-id --query data [?\\display-name\\==''].{id:id} --output table```2. List the details of a specific Event Rule based on the OCID of the rule.```oci events rule get --rule-id ```3. In the JSON output locate the Conditions key value pair and verify that the following Conditions are present:```com.oraclecloud.virtualnetwork.createdrgcom.oraclecloud.virtualnetwork.deletedrgcom.oraclecloud.virtualnetwork.updatedrgcom.oraclecloud.virtualnetwork.createdrgattachmentcom.oraclecloud.virtualnetwork.deletedrgattachmentcom.oraclecloud.virtualnetwork.updatedrgattachmentcom.oraclecloud.virtualnetwork.changeinternetgatewaycompartmentcom.oraclecloud.virtualnetwork.createinternetgatewaycom.oraclecloud.virtualnetwork.deleteinternetgatewaycom.oraclecloud.virtualnetwork.updateinternetgatewaycom.oraclecloud.virtualnetwork.changelocalpeeringgatewaycompartmentcom.oraclecloud.virtualnetwork.createlocalpeeringgatewaycom.oraclecloud.virtualnetwork.deletelocalpeeringgateway.endcom.oraclecloud.virtualnetwork.updatelocalpeeringgatewaycom.oraclecloud.natgateway.changenatgatewaycompartmentcom.oraclecloud.natgateway.createnatgatewaycom.oraclecloud.natgateway.deletenatgatewaycom.oraclecloud.natgateway.updatenatgatewaycom.oraclecloud.servicegateway.attachserviceidcom.oraclecloud.servicegateway.changeservicegatewaycompartmentcom.oraclecloud.servicegateway.createservicegatewaycom.oraclecloud.servicegateway.deleteservicegateway.endcom.oraclecloud.servicegateway.detachserviceidcom.oraclecloud.servicegateway.updateservicegateway```4. Verify the value of the `is-enabled` attribute is `true`5. In the JSON output verify that `actionType` is `ONS` and locate the `topic-id`6. Verify the correct topic is used by checking the topic name```oci ons topic get --topic-id --query data.{name:name} --output table```", + "AdditionalInformation": "'- The same Notification topic can be reused by many Event Rules.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "" + } + ] + }, + { + "Id": "4.13", + "Description": "Ensure VCN flow logging is enabled for all subnets", + "Checks": [ + "network_vcn_subnet_flow_logs_enabled" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "VCN flow logs record details about traffic that has been accepted or rejected based on the security list rule.", + "RationaleStatement": "Enabling VCN flow logs enables you to monitor traffic flowing within your virtual network and can be used to detect anomalous traffic.", + "ImpactStatement": "Enabling VCN flow logs will not affect the performance of your virtual network but it will generate additional use of object storage that should be controlled via object lifecycle management.By default, VCN flow logs are stored for 30 days in object storage. Users can specify a longer retention period.", + "RemediationProcedure": "**From Console:**First, if a Capture filter has not already been created, create a Capture Filter by the following steps:1. Go to the Network Command Center page (https://cloud.oracle.com/networking/network-command-center)2. Click 'Capture filters'3. Click 'Create Capture filter'4. Type a name for the Capture filter in the Name box.5. Select 'Flow log capture filter'6. For `Sample rating` select `100%`7. Scroll to `Rules`8. For `Traffic disposition` select `All`9. For `Include/Exclude` select `Include`10. Level `Source IPv4 CIDR or IPv6 prefix` and `Destination IPv4 CIDR or IPv6 prefix` empty11. For `IP protocol` select `Include`12. Click `Create Capture filter`Second, enable VCN flow logging for your VCN or subnet(s) by the following steps:1. Go to the Logs page (https://cloud.oracle.com/logging/logs)2. Click the `Enable Service Log` button in the middle of the screen.3. Select the relevant resource compartment.4. Select `Virtual Cloud Networks - Flow logs` from the Service drop down menu.5. Select the relevant resource level from the resource drop down menu either `VCN` or `subnet`.5. Select the relevant resource from the resource drop down menu.6. Select the from the Log Category drop down menu that either `Flow Logs - subnet records` or `Flow Logs - vcn records`.7. Select the Capture filter from above7. Type a name for your flow logs in the Log Name text box.7. Select the Compartment for the Log Location8. Select the Log Group for the Log Location or Click `Create New Group` to create a new log group8. Click the Enable Log button in the lower left-hand corner.", + "AuditProcedure": "**From Console (For Logging enabled Flow logs):**1. Go to the Virtual Cloud Network (VCN) page (https://cloud.oracle.com/networking/vcns)2. Select the Compartment 3. Click on the name of each VCN4. Click on each subnet within the VCN5. Under Resources click on Logs or the Monitoring tab6. Verify that there is a log enabled for the subnet7. Click the `Log Name`8. Verify `Flowlogs Capture Filter` is set to `No filter (collecting all logs)`9. If there is a Capture filter click the 'Capture Filter Name'10. Click `Edit`11. Verify Sampling rate is `100%`12. Click `Cancel`13. Verify there is a in the Rules list that is: `Enabled, Traffic disposition: All, Include/Exclude: Include, Source CIDR: Any, Destination CIDR: Any, IP Protocol: All`**From Console (For Network Command Center Enabled Flow logs):**1. Go to the Network Command Center page (https://cloud.oracle.com/networking/network-command-center)2. Click on Flow Logs3. Click on the Flow log `Name`4. Click `Edit`5. Verify Sampling rate is `100%` 6. Click `Cancel`7. Verify there is a in the Rules list that is: `Enabled, Traffic disposition: All, Include/Exclude: Include, Source CIDR: Any, Destination CIDR: Any, IP Protocol: All`", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en/solutions/oci-aggregate-logs-siem/index.html#GUID-601E052A-8A8E-466B-A8A8-2BBBD3B80B6D" + } + ] + }, + { + "Id": "4.14", + "Description": "Ensure Cloud Guard is enabled in the root compartment of the tenancy", + "Checks": [ + "cloudguard_enabled" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Cloud Guard detects misconfigured resources and insecure activity within a tenancy and provides security administrators with the visibility to resolve these issues. Upon detection, Cloud Guard can suggest, assist, or take corrective actions to mitigate these issues. Cloud Guard should be enabled in the root compartment of your tenancy with the default configuration, activity detectors and responders.", + "RationaleStatement": "Cloud Guard provides an automated means to monitor a tenancy for resources that are configured in an insecure manner as well as risky network activity from these resources.", + "ImpactStatement": "There is no performance impact when enabling the above described features, but additional IAM policies will be required.", + "RemediationProcedure": "**From Console:**1. Type `Cloud Guard` into the Search box at the top of the Console.2. Click `Cloud Guard` from the Services submenu.3. Click `Enable Cloud Guard`.4. Click `Create Policy`.5. Click `Next`.6. Under `Reporting Region`, select a region.7. Under `Compartments To Monitor`, choose `Select Compartment`.8. Under `Select Compartments`, select the `root` compartment.9. Under `Configuration Detector Recipe`, select `OCI Configuration Detector Recipe (Oracle Managed)`.10. Under `Activity Detector Recipe`, select `OCI Activity Detector Recipe (Oracle Managed)`.11. Click `Enable`.**From CLI:**1. Create OCI IAM Policy for Cloud Guard```oci iam policy create --compartment-id '' --name 'CloudGuardPolicies' --description 'Cloud Guard Access Policy' --statements '[ allow service cloudguard to read vaults in tenancy, allow service cloudguard to read keys in tenancy, allow service cloudguard to read compartments in tenancy, allow service cloudguard to read tenancies in tenancy, allow service cloudguard to read audit-events in tenancy, allow service cloudguard to read compute-management-family in tenancy, allow service cloudguard to read instance-family in tenancy, allow service cloudguard to read virtual-network-family in tenancy, allow service cloudguard to read volume-family in tenancy, allow service cloudguard to read database-family in tenancy, allow service cloudguard to read object-family in tenancy, allow service cloudguard to read load-balancers in tenancy, allow service cloudguard to read users in tenancy, allow service cloudguard to read groups in tenancy, allow service cloudguard to read policies in tenancy, allow service cloudguard to read dynamic-groups in tenancy, allow service cloudguard to read authentication-policies in tenancy ]'```2. Enable Cloud Guard in root compartment```oci cloud-guard configuration update --reporting-region '' --compartment-id '' --status 'ENABLED'```", + "AuditProcedure": "**From Console:**1. Type `Cloud Guard` into the Search box at the top of the Console.2. Click `Cloud Guard` from the Services submenu.3. View if `Cloud Guard` is enabled**From CLI:**1. Retrieve the `Cloud Guard` status from the console```oci cloud-guard configuration get --compartment-id --query 'data.status'```2. Ensure the returned value is ENABLED`", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en-us/iaas/Content/General/Concepts/regions.htm" + } + ] + }, + { + "Id": "4.15", + "Description": "Ensure a notification is configured for Oracle Cloud Guard problems detected", + "Checks": [ + "events_rule_cloudguard_problems" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Cloud Guard detects misconfigured resources and insecure activity within a tenancy and provides security administrators with the visibility to resolve these issues. Upon detection, Cloud Guard generates a Problem. It is recommended to setup an Event Rule and Notification that gets triggered when Oracle Cloud Guard Problems are created, dismissed or remediated. Event Rules are compartment scoped and will detect events in child compartments. It is recommended to create the Event rule at the root compartment level.", + "RationaleStatement": "Cloud Guard provides an automated means to monitor a tenancy for resources that are configured in an insecure manner as well as risky network activity from these resources. Monitoring and alerting on Problems detected by Cloud Guard will help in identifying changes to the security posture.", + "ImpactStatement": "There is no performance impact when enabling the above described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "**From Console:**1. Go to the Events Service page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)1. Select the compartment that should host the rule1. Click Create Rule1. Provide a Display Name and Description1. Create a Rule Condition by selecting Cloud Guard in the Service Name Drop-down and selecting: `Detected – Problem`, `Remediated – Problem`, and `Dismissed - Problem`1. In the Actions section select Notifications as Action Type1. Select the Compartment that hosts the Topic to be used.1. Select the Topic to be used1. Optionally add Tags to the Rule1. Click Create Rule**From CLI:**1. Find the topic-id of the topic the Event Rule should use for sending Notifications by using the topic name and Compartment OCID```oci ons topic list --compartment-id= --all --query data [?name==''].{name:name,topic_id:\\topic-id\\} --output table```1. Create a JSON file to be used when creating the Event Rule. Replace topic id, display name, description and compartment OCID.```{ actions: { actions: [ { actionType: ONS, isEnabled: true, topicId: }] }, condition:{\\eventType\\:[\\ com.oraclecloud.cloudguard.problemdetected\\,\\ com.oraclecloud.cloudguard.problemdismissed\\,\\ com.oraclecloud.cloudguard.problemremediated\\],\\data\\:{}}, displayName: , description: , isEnabled: true, compartmentId: compartment OCID}```1. Create the actual event rule```oci events rule create --from-json file://event_rule.json```1. Note in the JSON returned that it lists the parameters specified in the JSON file provided and that there is an OCID provided for the Event Rule", + "AuditProcedure": "**From Console:**1. Go to the Events Service page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)1. Select the Compartment that hosts the rules1. Find and click the Rule that handles Cloud Guard Changes (if any)1. Click the Edit Rule button and verify that the RuleConditions section contains a condition for the Service Cloud Guard and Event Types: Detected – Problem, Remediated – Problem, and Dismissed - Problem1. Verify that in the Actions section the Action Type contains: Notifications and that a valid Topic is referenced.**From CLI:**1. Find the OCID of the specific Event Rule based on Display Name and Compartment OCID```oci events rule list --compartment-id= --query data [?\\display-name\\==''].{id:id} --output table```1. List the details of a specific Event Rule based on the OCID of the rule.1. In the JSON output locate the Conditions key-value pair and verify that the following Conditions are present: ```com.oraclecloud.cloudguard.problemdetected,com.oraclecloud.cloudguard.problemdismissed,com.oraclecloud.cloudguard.problemremediated```1. Verify the value of the is-enabled attribute is true1. In the JSON output verify that actionType is ONS and locate the topic-id1. Verify the correct topic is used by checking the topic name```oci ons topic get --topic-id= --query data.{name:name} --output table```", + "AdditionalInformation": "'- Your tenancy might have a different Cloud Reporting region than your home region.- The same Notification topic can be reused by many Event Rules.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "https://docs.oracle.com/en-us/iaas/cloud-guard/using/export-notifs-config.htm" + } + ] + }, + { + "Id": "4.16", + "Description": "Ensure customer created Customer Managed Key (CMK) is rotated at least annually", + "Checks": [ + "kms_key_rotation_enabled" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Oracle Cloud Infrastructure Vault securely stores master encryption keys that protect your encrypted data. You can use the Vault service to rotate keys to generate new cryptographic material. Periodically rotating keys limits the amount of data encrypted by one key version.", + "RationaleStatement": "Rotating keys annually limits the data encrypted under one key version. Key rotation thereby reduces the risk in case a key is ever compromised.", + "ImpactStatement": "", + "RemediationProcedure": "**From Console:**1. Login into OCI Console.2. Select `Identity & Security` from the Services menu.3. Select `Vault`.4. Click on the individual Vault under the Name heading.5. Click on the menu next to the time created.6. Click `Rotate Key`**From CLI:**1. Execute the following:```oci kms management key rotate --key-id --endpoint ```", + "AuditProcedure": "**From Console:**1. Login into OCI Console.2. Select `Identity & Security` from the Services menu.3. Select `Vault`.4. Click on the individual Vault under the Name heading.5. Ensure the date of each Master Encryption key under the `Created` column of the Master Encryption key is no more than 365 days old, and that the key is in the `ENABLED` state6. Repeat for all Vaults in all compartments**From CLI:**1. Execute the following for each Vault in each compartment```oci kms management key list --compartment-id '' --endpoint '' --all --query data[*].[\\time-created\\,\\display-name\\,\\lifecycle-state\\]```2. Ensure the date of the Master Encryption key is no more than 365 days old and is also in the `ENABLED` state.", + "AdditionalInformation": "", + "References": "" + } + ] + }, + { + "Id": "4.17", + "Description": "Ensure write level Object Storage logging is enabled for all buckets", + "Checks": [ + "objectstorage_bucket_logging_enabled" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Object Storage write logs will log all write requests made to objects in a bucket.", + "RationaleStatement": "Enabling Object Storage write logging ensures the `requestAction` property will show values like `PUT`, `POST`, or `DELETE`, providing increased visibility into changes made to objects within buckets.", + "ImpactStatement": "Enabling object storage write logging does not impact object storage performance, but will consume additional storage for the logs themselves. By default, logs are retained for 30 days, but users may configure longer retention periods. To manage costs, implement object lifecycle policies to remove unneeded logs as appropriate.", + "RemediationProcedure": "**From Console:**1. Log in to the OCI Console.2. Go to [Object Storage Buckets](https://cloud.oracle.com/object-storage/buckets).3. Click the name of the bucket to configure.4. In the Resource menu, click `Monitoring`.5. Scroll to the `Logs` section.6. Find `Write Access Events` and click the three dots `...` at the end of the row.7. Click `Enable Log`.8. Choose an existing log group or select `Create new group`.9. Configure the log name.10. Set a desired log retention period (in months).11. Click `Enable log`.**From CLI:***If a log group does not exist:*1. Create a log group:```shoci logging log-group create --compartment-id --display-name --description ```2. Check work request status:```shoci logging work-request get --work-request-id ```Wait until status is `SUCCEEDED`.*To enable write logging for your bucket(s):*3. Get the Log Group OCID:```shoci logging log-group list --compartment-id --query \"data[?\\display-name==''].id\" --raw-output```4. Create `config.json` with the following content (update all placeholders):```json{ \"compartment-id\": \"\", \"source\": { \"resource\": \"\", \"service\": \"ObjectStorage\", \"source-type\": \"OCISERVICE\", \"category\": \"write\" }}```5. Create the service log:```shoci logging log create --log-group-id --display-name --log-type SERVICE --is-enabled TRUE --configuration file://config.json```6. Confirm creation with work request id:```shoci logging work-request get --work-request-id ```Look for status `SUCCEEDED`.", + "AuditProcedure": "**From Console:**1. Log in to the OCI Console.2. Go to [Object Storage Buckets](https://cloud.oracle.com/object-storage/buckets).3. Click on a bucket's name.4. Select `Monitoring` from the Resource menu.5. Scroll to `Logs` and ensure the `Status` for `Write Access Events` is `Active`.**From CLI:**1. List all buckets in the compartment:```shoci os bucket list --compartment-id ```2. Find the Log Group OCID:```shoci logging log-group list --compartment-id --query \"data[?\\display-name=='']\"```3. List logs associated with the specific bucket:```shoci logging log list --log-group-id --query \"data[?configuration.source.resource=='']\"```4. Ensure a log entry exists for the target bucket's name.", + "AdditionalInformation": "", + "References": "" + } + ] + }, + { + "Id": "4.18", + "Description": "Ensure a notification is configured for Local OCI User Authentication", + "Checks": [ + "events_rule_local_user_authentication" + ], + "Attributes": [ + { + "Section": "4. Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "It is recommended that an Event Rule and Notification be set up when a user in the via OCI local authentication. Event Rules are compartment-scoped and will detect events in child compartments. This Event rule is required to be created at the root compartment level.", + "RationaleStatement": "Users should rarely use OCI local authenticated and be authenticated via organizational standard Identity providers, not local credentials. Access in this matter would represent a break glass activity and should be monitored to see if changes made impact the security posture.", + "ImpactStatement": "There is no performance impact when enabling the above-described features but depending on the amount of notifications sent per month there may be a cost associated.", + "RemediationProcedure": "From Console:1. Go to the Events Service page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `Root compartment` that should host the rule3. Click `Create Rule`4. Provide a `Display Name` and `Description`5. Create a Rule Condition by selecting `Identity SignOn` in the Service Name Drop-down and selecting `Interactive Login`6. In the `Actions` section select `Notifications` as Action Type7. Select the `Compartment` that hosts the Topic to be used.8. Select the `Topic` to be used9. Optionally add Tags to the Rule10. Click `Create Rule`From CLI:1. Find the `topic-id` of the topic the Event Rule should use for sending notifications by using the topic `name` and `Tenancy OCID````oci ons topic list --compartment-id --all --query data [?name==''].{name:name,topic_id:\\topic-id\\} --output table```2. Create a JSON file to be used when creating the Event Rule. Replace topic id, display name, description and compartment OCID.```{ actions: { actions: [ { actionType: ONS, isEnabled: true, topicId: }] }, condition:{\\eventType\\:[\\com.oraclecloud.identitysignon.interactivelogin\\,data\\:{}}, displayName: , description: , isEnabled: true, compartmentId: }```3. Create the actual event rule```oci events rule create --from-json file://event_rule.json```4. Note in the JSON returned that it lists the parameters specified in the JSON file provided and that there is an OCID provided for the Event Rule", + "AuditProcedure": "From Console:1. Go to the Events Service page: [https://cloud.oracle.com/events/rules](https://cloud.oracle.com/events/rules)2. Select the `Root Compartment `that hosts the rules3. Click the `Rule` that handles `Identity SignOn` Changes (if any)4. Click the `Edit Rule` button and verify that the `RuleCondition`s section contains a condition `Event Type` for the Service `Identity SignOn` and Event Types: `Interactive Login `5. On the Action Type contains: `Notifications` and that a valid Topic is referenced.From CLI:1. Find the OCID of the specific Event Rule based on Display Name and Tenancy OCID```oci events rule list --compartment-id --query data [?\\display-name\\==''].{id:id} --output table```2. List the details of a specific Event Rule based on the OCID of the rule.```oci events rule get --rule-id ```3. In the JSON output locate the Conditions key value pair and verify that the following Conditions are present:```com.oraclecloud.identitysignon.interactivelogin```4. Verify the value of the `is-enabled` attribute is `true`5. In the JSON output verify that `actionType` is `ONS` and locate the `topic-id`6. Verify the correct topic is used by checking the topic name```oci ons topic get --topic-id --query data.{name:name} --output table```", + "AdditionalInformation": "'- The same Notification topic can be reused by many Event Rules.- The generated notification will include an eventID that can be used when querying the Audit Logs in case further investigation is required.", + "References": "https://docs.oracle.com/en-us/iaas/Content/Security/Reference/iam_security_topic-IAM_Federation.htm#IAM_Federation" + } + ] + }, + { + "Id": "5.1.1", + "Description": "Ensure no Object Storage buckets are publicly visible", + "Checks": [ + "objectstorage_bucket_not_publicly_accessible" + ], + "Attributes": [ + { + "Section": "5. Storage", + "SubSection": "5.1 Object Storage", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "A bucket is a logical container for storing objects. It is associated with a single compartment that has policies that determine what action a user can perform on a bucket and on all the objects in the bucket. By Default a newly created bucket is private. It is recommended that no bucket be publicly accessible.", + "RationaleStatement": "Removing unfettered reading of objects in a bucket reduces an organization's exposure to data loss.", + "ImpactStatement": "For updating an existing bucket, care should be taken to ensure objects in the bucket can be accessed through either IAM policies or pre-authenticated requests.", + "RemediationProcedure": "**From Console:**1. Follow the audit procedure above. 2. For each `bucket` in the returned results, click the Bucket `Display Name`3. Click `Edit Visibility`3. Select `Private`4. Click `Save Changes`**From CLI:**1. Follow the audit procedure2. For each of the `buckets` identified, execute the following command:```oci os bucket update --bucket-name --public-access-type NoPublicAccess```", + "AuditProcedure": "**From Console:**1. Login into the OCI Console2. Click in the search bar at the top of the screen.3. Type `Advanced Resource Query` and click `enter`.4. Click the `Advanced Resource Query` button in the upper right of the screen.5. Enter the following query in the query box:```querybucket resourceswhere (publicAccessType == 'ObjectRead') || (publicAccessType == 'ObjectReadWithoutList')```6. Ensure query returns no results**From CLI:**1. Execute the following command:```oci search resource structured-search --query-text query bucket resourceswhere (publicAccessType == 'ObjectRead') || (publicAccessType == 'ObjectReadWithoutList')```2. Ensure query returns no results**Cloud Guard**To Enable Cloud Guard Auditing:Ensure Cloud Guard is enabled in the root compartment of the tenancy. For more information about enabling Cloud Guard, please look at the instructions included in Recommendation 3.15. **From Console:**1. Type `Cloud Guard` into the Search box at the top of the Console. 2. Click `Cloud Guard` from the “Services” submenu.3. Click `Detector Recipes` in the Cloud Guard menu.4. Click `OCI Configuration Detector Recipe (Oracle Managed)` under the Recipe Name column.5. Find Bucket is public in the Detector Rules column.6. Verify that the Bucket is public Detector Rule is Enabled.**From CLI:**1. Verify the Bucket is public Detector Rule in Cloud Guard is enabled to generate Problems if Object Storage Buckets are configured to be accessible over the public Internet with the following command:```oci cloud-guard detector-recipe-detector-rule get --detector-recipe-id --detector-rule-id BUCKET_IS_PUBLIC```", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/managingbuckets.htm" + } + ] + }, + { + "Id": "5.1.2", + "Description": "Ensure Object Storage Buckets are encrypted with a Customer Managed Key (CMK)", + "Checks": [ + "objectstorage_bucket_encrypted_with_cmk" + ], + "Attributes": [ + { + "Section": "5. Storage", + "SubSection": "5.1 Object Storage", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Oracle Object Storage buckets support encryption with a Customer Managed Key (CMK). By default, Object Storage buckets are encrypted with an Oracle managed key.", + "RationaleStatement": "Encryption of Object Storage buckets with a Customer Managed Key (CMK) provides an additional level of security on your data by allowing you to manage your own encryption key lifecycle management for the bucket.", + "ImpactStatement": "Encrypting with a Customer Managed Keys requires a Vault and a Customer Master Key. In addition, you must authorize Object Storage service to use keys on your behalf.Required Policy:```Allow service objectstorage-, to use keys in compartment where target.key.id = ''```", + "RemediationProcedure": "**From Console:**1. Go to [https://cloud.oracle.com/object-storage/buckets](https://cloud.oracle.com/object-storage/buckets)1. Click on an individual bucket under the Name heading.1. Click `Assign` next to `Encryption Key: Oracle managed key`.1. Select a `Vault`1. Select a `Master Encryption Key`1. Click `Assign`**From CLI:**1. Execute the following command```oci os bucket update --bucket-name --kms-key-id ```", + "AuditProcedure": "**From Console:**1. Go to [https://cloud.oracle.com/object-storage/buckets](https://cloud.oracle.com/object-storage/buckets)1. Click on an individual bucket under the Name heading.1. Ensure that the `Encryption Key` is not set to `Oracle managed key`.1. Repeat for each compartment**From CLI:**1. Execute the following command```oci os bucket get --bucket-name ```2. Ensure `kms-key-id` is not `null`**Cloud Guard**To Enable Cloud Guard Auditing:Ensure Cloud Guard is enabled in the root compartment of the tenancy. For more information about enabling Cloud Guard, please look at the instructions included in Recommendation 3.15. **From Console:**1. Type `Cloud Guard` into the Search box at the top of the Console. 2. Click `Cloud Guard` from the “Services” submenu.3. Click `Detector Recipes` in the Cloud Guard menu.4. Click `OCI Configuration Detector Recipe (Oracle Managed)` under the Recipe Name column.5. Find Object Storage bucket is encrypted with Oracle-managed key in the Detector Rules column.6. Verify that the Object Storage bucket is encrypted with Oracle-managed key Detector Rule is Enabled.**From CLI:**1. Verify the Object Storage bucket is encrypted with Oracle-managed key Detector Rule in Cloud Guard is enabled to generate Problems if Object Storage Buckets are configured without a customer managed key with the following command:```oci cloud-guard detector-recipe-detector-rule get --detector-recipe-id --detector-rule-id BUCKET_ENCRYPTED_WITH_ORACLE_MANAGED_KEY```", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en/solutions/oci-best-practices/protect-data-rest1.html#GUID-9C0F713E-4C67-43C6-80CA-525A6AB221F1:https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/encryption.htm" + } + ] + }, + { + "Id": "5.1.3", + "Description": "Ensure Versioning is Enabled for Object Storage Buckets", + "Checks": [ + "objectstorage_bucket_versioning_enabled" + ], + "Attributes": [ + { + "Section": "5. Storage", + "SubSection": "5.1 Object Storage", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "A bucket is a logical container for storing objects. Object versioning is enabled at the bucket level and is disabled by default upon creation. Versioning directs Object Storage to automatically create an object version each time a new object is uploaded, an existing object is overwritten, or when an object is deleted. You can enable object versioning at bucket creation time or later.", + "RationaleStatement": "Versioning object storage buckets provides for additional integrity of your data. Management of data integrity is critical to protecting and accessing protected data. Some customers want to identify object storage buckets without versioning in order to apply their own data lifecycle protection and management policy.", + "ImpactStatement": "", + "RemediationProcedure": "**From Console:**1. Follow the audit procedure above.2. For each bucket in the returned results, click the Bucket Display Name3. Click `Edit` next to `Object Versioning: Disabled`4. Click `Enable Versioning`**From CLI:**1. Follow the audit procedure2. For each of the buckets identified, execute the following command:```oci os bucket update --bucket-name --versioning Enabled```", + "AuditProcedure": "**From Console:**1. Login to OCI Console.2. Select `Storage` from the Services menu.3. Select `Buckets` from under the `Object Storage & Archive Storage` section.4. Click on an individual bucket under the Name heading.5. Ensure that the `Object Versioning` is set to Enabled.6. Repeat for each compartment**From CLI:**1. Execute the following command:```for region in $(oci iam region-subscription list --all | jq -r '.data[] | .region-name')do echo Enumerating region $region for compid in $(oci iam compartment list --include-root --compartment-id-in-subtree TRUE 2>/dev/null | jq -r '.data[] | .id') do echo Enumerating compartment $compid for bkt in $(oci os bucket list --compartment-id $compid --region $region 2>/dev/null | jq -r '.data[] | .name') do output=$(oci os bucket get --bucket-name $bkt --region $region 2>/dev/null | jq -r '.data | select(.versioning == Disabled).name') if [ ! -z $output ]; then echo $output; fi done donedone```2. Ensure no results are returned.", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/usingversioning.htm:https://docs.oracle.com/en-us/iaas/api/#/en/objectstorage/20160918/Bucket/GetBucket" + } + ] + }, + { + "Id": "5.2.1", + "Description": "Ensure Block Volumes are encrypted with Customer Managed Keys (CMK)", + "Checks": [ + "blockstorage_block_volume_encrypted_with_cmk" + ], + "Attributes": [ + { + "Section": "5. Storage", + "SubSection": "5.2 Block Volumes", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Oracle Cloud Infrastructure Block Volume service lets you dynamically provision and manage block storage volumes. By default, the Oracle service manages the keys that encrypt block volumes. Block Volumes can also be encrypted using a customer managed key.Terminated Block Volumes cannot be recovered and any data on a terminated volume is permanently lost. However, Block Volumes can exist in a terminated state within the OCI Portal and CLI for some time after deleting. As such, any Block Volumes in this state should not be considered when assessing this policy.", + "RationaleStatement": "Encryption of block volumes provides an additional level of security for your data. Management of encryption keys is critical to protecting and accessing protected data. Customers should identify block volumes encrypted with Oracle service managed keys in order to determine if they want to manage the keys for certain volumes and then apply their own key lifecycle management to the selected block volumes.", + "ImpactStatement": "Encrypting with a Customer Managed Key requires a Vault and a Customer Master Key. In addition, you must authorize the Block Volume service to use the keys you create.Required IAM Policy:```Allow service blockstorage to use keys in compartment where target.key.id = ''```", + "RemediationProcedure": "**From Console:**1. Follow the audit procedure above.2. For each block volume returned, click the link under Display name.3. If the value for `Encryption Key` is `Oracle-managed key`, click `Assign` next to `Oracle-managed key`.4. Select a `Vault Compartment` and `Vault`.5. Select a `Master Encryption Key Compartment` and `Master Encryption key`.6. Click `Assign`.**From CLI:**1. Follow the audit procedure.2. For each `boot volume` identified, get the OCID.3. Execute the following command:```oci bv volume-kms-key update –volume-id --kms-key-id ```", + "AuditProcedure": "**From Console:**1. Login to the OCI Console.2. Click the search bar at the top of the screen.3. Type 'Advanced Resource Query' and press return.4. Click `Advanced resource query`.5. Enter the following query in the query box:```query volume resources```6. For each block volume returned, click the link under `Display name`.7. Ensure the value for `Encryption Key` is not `Oracle-managed key`.8. Repeat for other subscribed regions.**From CLI:**1. Execute the following command:```for region in $(oci iam region-subscription list --all| jq -r '.data[] | .region-name')do echo Enumerating region: $region for compid in `oci iam compartment list --compartment-id-in-subtree TRUE 2>/dev/null | jq -r '.data[] | .id'` do echo Enumerating compartment: $compid for bvid in `oci bv volume list --compartment-id $compid --region $region 2>/dev/null | jq -r '.data[] | select(.kms-key-id == null).id'` do output=`oci bv volume get --volume-id $bvid --region $region --query=data.{name:\\display-name\\,id:id} --output table 2>/dev/null` if [ ! -z $output ]; then echo $output; fi done done done```2. Ensure the query returns no results.", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en/solutions/oci-best-practices/protect-data-rest1.html#GUID-BA1F5A20-8C78-49E3-8183-927F0CC6F6CC:https://docs.oracle.com/en-us/iaas/Content/Block/Concepts/overview.htm" + } + ] + }, + { + "Id": "5.2.2", + "Description": "Ensure boot volumes are encrypted with Customer Managed Key (CMK)", + "Checks": [ + "blockstorage_boot_volume_encrypted_with_cmk" + ], + "Attributes": [ + { + "Section": "5. Storage", + "SubSection": "5.2 Block Volumes", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "When you launch a virtual machine (VM) or bare metal instance based on a platform image or custom image, a new boot volume for the instance is created in the same compartment. That boot volume is associated with that instance until you terminate the instance. By default, the Oracle service manages the keys that encrypt this boot volume. Boot Volumes can also be encrypted using a customer managed key.", + "RationaleStatement": "Encryption of boot volumes provides an additional level of security for your data. Management of encryption keys is critical to protecting and accessing protected data. Customers should identify boot volumes encrypted with Oracle service managed keys in order to determine if they want to manage the keys for certain boot volumes and then apply their own key lifecycle management to the selected boot volumes.", + "ImpactStatement": "Encrypting with a Customer Managed Keys requires a Vault and a Customer Master Key. In addition, you must authorize the Boot Volume service to use the keys you create.Required IAM Policy:```Allow service Bootstorage to use keys in compartment where target.key.id = ''```", + "RemediationProcedure": "**From Console:**1. Follow the audit procedure above.2. For each Boot Volume in the returned results, click the Boot Volume name3. Click `Assign` next to `Encryption Key`4. Select the `Vault Compartment` and `Vault`5. Select the `Master Encryption Key Compartment` and `Master Encryption key`6. Click `Assign`**From CLI:**1. Follow the audit procedure.2. For each `boot volume` identified get its OCID. Execute the following command:```oci bv boot-volume-kms-key update --boot-volume-id --kms-key-id ```", + "AuditProcedure": "**From Console:**1. Login into the OCI Console2. Click in the search bar, top of the screen.3. Type Advanced Resource Query and click enter.4. Click the `Advanced Resource Query` button in the upper right of the screen.5. Enter the following query in the query box:```query bootvolume resources```6. For each boot volume returned click on the link under `Display name`7. Ensure `Encryption Key` does not say `Oracle managed key`8. Repeat for other subscribed regions**From CLI:**1. Execute the following command:```for region in `oci iam region list | jq -r '.data[] | .name'`; do for bvid in `oci search resource structured-search --region $region --query-text query bootvolume resources 2>/dev/null | jq -r '.data.items[] | .identifier'` do output=`oci bv boot-volume get --boot-volume-id $bvid 2>/dev/null | jq -r '.data | select(.kms-key-id == null).id'` if [ ! -z $output ]; then echo $output; fi done done```2. Ensure query returns no results.", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en/solutions/oci-best-practices/protect-data-rest1.html#GUID-BA1F5A20-8C78-49E3-8183-927F0CC6F6CC" + } + ] + }, + { + "Id": "5.3.1", + "Description": "Ensure File Storage Systems are encrypted with Customer Managed Keys (CMK)", + "Checks": [ + "filestorage_file_system_encrypted_with_cmk" + ], + "Attributes": [ + { + "Section": "5. Storage", + "SubSection": "5.3 File Storage Service", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Oracle Cloud Infrastructure File Storage service (FSS) provides a durable, scalable, secure, enterprise-grade network file system. By default, the Oracle service manages the keys that encrypt FSS file systems. FSS file systems can also be encrypted using a customer managed key.", + "RationaleStatement": "Encryption of FSS systems provides an additional level of security for your data. Management of encryption keys is critical to protecting and accessing protected data. Customers should identify FSS file systems that are encrypted with Oracle service managed keys in order to determine if they want to manage the keys for certain FSS file systems and then apply their own key lifecycle management to the selected FSS file systems.", + "ImpactStatement": "Encrypting with a Customer Managed Keys requires a Vault and a Customer Master Key. In addition, you must authorize the File Storage service to use the keys you create.Required IAM Policy:```Allow service FssOc1Prod to use keys in compartment where target.key.id = ''```", + "RemediationProcedure": "From Console:1. Follow the audit procedure above.2. For each File Storage System in the returned results, click the File System Storage3. Click `Edit` next to `Encryption Key`4. Select `Encrypt using customer-managed keys`5. Select the `Vault Compartment` and `Vault`6. Select the `Master Encryption Key Compartment` and `Master Encryption key`7. Click `Save Changes`**From CLI:**1. Follow the audit procedure.2. For each `File Storage System` identified get its OCID. Execute the following command:```oci bv volume-kms-key update –volume-id --kms-key-id ```", + "AuditProcedure": "**From Console:**1. Login into the OCI Console2. Click in the search bar, top of the screen.3. Type Advanced Resource Query and click enter.4. Click the `Advanced Resource Query` button in the upper right of the screen.5. Enter the following query in the query box:```query filesystem resources```6. For each file storage system returned click on the link under `Display name`7. Ensure `Encryption Key` does not say `Oracle-managed key`8. Repeat for other subscribed regions**From CLI:**1. Execute the following command:```for region in `oci iam region list | jq -r '.data[] | .name'`; do for fssid in `oci search resource structured-search --region $region --query-text query filesystem resources 2>/dev/null | jq -r '.data.items[] | .identifier'` do output=`oci fs file-system get --file-system-id $fssid --region $region 2>/dev/null | jq -r '.data | select(.kms-key-id == ).id'` if [ ! -z $output ]; then echo $output; fi done done```2. Ensure query returns no results", + "AdditionalInformation": "", + "References": "https://docs.oracle.com/en/solutions/oci-best-practices/protect-data-rest1.html#GUID-BA1F5A20-8C78-49E3-8183-927F0CC6F6CC:https://docs.oracle.com/en-us/iaas/Content/File/Concepts/filestorageoverview.htm" + } + ] + }, + { + "Id": "6.1", + "Description": "Create at least one compartment in your tenancy to store cloud resources", + "Checks": [ + "identity_non_root_compartment_exists" + ], + "Attributes": [ + { + "Section": "6. Asset Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "When you sign up for Oracle Cloud Infrastructure, Oracle creates your tenancy, which is the root compartment that holds all your cloud resources. You then create additional compartments within the tenancy (root compartment) and corresponding policies to control access to the resources in each compartment. Compartments allow you to organize and control access to your cloud resources. A compartment is a collection of related resources (such as instances, databases, virtual cloud networks, block volumes) that can be accessed only by certain groups that have been given permission by an administrator.", + "RationaleStatement": "Compartments are a logical group that adds an extra layer of isolation, organization and authorization making it harder for unauthorized users to gain access to OCI resources.", + "ImpactStatement": "Once the compartment is created an OCI IAM policy must be created to allow a group to resources in the compartment otherwise only group with tenancy access will have access.", + "RemediationProcedure": "**From Console:**1. Login to OCI Console.1. Select `Identity` from the Services menu.1. Select `Compartments` from the Identity menu.1. Click `Create Compartment`1. Enter a `Name`1. Enter a `Description`1. Select the root compartment as the `Parent Compartment`1. Click `Create Compartment`**From CLI:**1. Execute the following command```oci iam compartment create --compartment-id '' --name '' --description ''```", + "AuditProcedure": "**From Console:**1. Login into the OCI Console.1. Click in the search bar, top of the screen.1. Type `Advanced Resource Query` and hit `enter`.1. Click the `Advanced Resource Query` button in the upper right of the screen.1. Enter the following query in the query box:```query compartment resourceswhere (compartmentId='' && lifecycleState='ACTIVE')```6. Ensure query returns at least one compartment in addition to the `ManagedCompartmentForPaaS` compartment**From CLI:**1. Execute the following command```oci search resource structured-search --query-text query compartment resourceswhere (compartmentId='' && lifecycleState='ACTIVE')```2. Ensure `items` are returned.", + "AdditionalInformation": "", + "References": "" + } + ] + }, + { + "Id": "6.2", + "Description": "Ensure no resources are created in the root compartment", + "Checks": [ + "identity_no_resources_in_root_compartment" + ], + "Attributes": [ + { + "Section": "6. Asset Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "When you create a cloud resource such as an instance, block volume, or cloud network, you must specify to which compartment you want the resource to belong. Placing resources in the root compartment makes it difficult to organize and isolate those resources.", + "RationaleStatement": "Placing resources into a compartment will allow you to organize and have more granular access controls to your cloud resources.", + "ImpactStatement": "Placing a resource in a compartment will impact how you write policies to manage access and organize that resource.", + "RemediationProcedure": "**From Console:**1. Follow audit procedure above.2. For each item in the returned results, click the item name.3. Then select `Move Resource` or `More Actions` then `Move Resource`.4. Select a compartment that is not the root compartment in `CHOOSE NEW COMPARTMENT`.5. Click `Move Resource`.**From CLI:**1. Follow the audit procedure above.2. For each bucket item execute the below command: ```oci os bucket update --bucket-name --compartment-id ```3. For other resources use the `change-compartment` command for the resource type:``` oci change-compartment -- --compartment-id ``` i. Example for an Autonomous Database:```oci db autonomous-database change-compartment --autonomous-database-id --compartment-id ```", + "AuditProcedure": "**From Console:**1. Login into the OCI Console.2. Click in the search bar, top of the screen.3. Type `Advance Resource Query` and hit `enter`.4. Click the `Advanced Resource Query` button in the upper right of the screen.5. Enter the following query into the query box:```query VCN, instance, bootvolume, volume, filesystem, bucket, autonomousdatabase, database, dbsystem resources where compartmentId = ''```6. Ensure query returns no results.**From CLI:**1. Execute the following command:```oci search resource structured-search --query-text query VCN, instance, volume, bootvolume, filesystem, bucket, autonomousdatabase, database, dbsystem resources where compartmentId = ''```2. Ensure query return no results.", + "AdditionalInformation": "https://docs.cloud.oracle.com/en-us/iaas/Content/GSG/Concepts/settinguptenancy.htm#Understa", + "References": "" + } + ] + } + ] +} diff --git a/prowler/compliance/oraclecloud/csa_ccm_4.0_oraclecloud.json b/prowler/compliance/oraclecloud/csa_ccm_4.0_oraclecloud.json new file mode 100644 index 0000000000..300e32788d --- /dev/null +++ b/prowler/compliance/oraclecloud/csa_ccm_4.0_oraclecloud.json @@ -0,0 +1,7307 @@ +{ + "Framework": "CSA-CCM", + "Name": "CSA Cloud Controls Matrix (CCM) v4.0.13", + "Version": "4.0", + "Provider": "OracleCloud", + "Description": "The Cloud Security Alliance (CSA) Cloud Controls Matrix (CCM) is a cybersecurity control framework for cloud computing, composed of 197 control objectives structured in 17 domains covering all key aspects of cloud technology. The CCM can be used as a tool for the systematic assessment of a cloud implementation, and provides guidance on which security controls should be implemented by which actor within the cloud supply chain.", + "Requirements": [ + { + "Id": "A&A-02", + "Description": "Conduct independent audit and assurance assessments according to relevant standards at least annually.", + "Name": "Independent Assessments", + "Attributes": [ + { + "Section": "Audit & Assurance", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC4.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AAC-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.5.2", + "5.2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "AS1.1", + "AS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.2.1", + "27002: 18.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.35", + "27001: A.5.36" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-2", + "CA-2(1)", + "CA-2(2)", + "CA-7", + "CA-7(1)" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-01" + ] + } + ] + } + ], + "Checks": [ + "cloudguard_enabled" + ] + }, + { + "Id": "A&A-04", + "Description": "Verify compliance with all relevant standards, regulations, legal/contractual, and statutory requirements applicable to the audit.", + "Name": "Requirements Compliance", + "Attributes": [ + { + "Section": "Audit & Assurance", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC3.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-01", + "GRM-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "7.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "AS1.1", + "AS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.3.2", + "27001: A.18.2.2", + "27002: 18.2.2", + "27001: A.18.2.3", + "27002: 18.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.3.2", + "27001: A.5.31", + "27001: A.5.32", + "27001: A.5.33", + "27001: A.5.34", + "27001: A.5.36" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-1" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-3", + "DE.DP-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-01" + ] + } + ] + } + ], + "Checks": [ + "cloudguard_enabled" + ] + }, + { + "Id": "AIS-04", + "Description": "Define and implement a SDLC process for application design, development, deployment, and operation in accordance with security requirements defined by the organization.", + "Name": "Secure Application Design and Development", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AIS-01", + "AIS-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.4", + "5.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.1.1", + "27002: 14.1.1", + "27017: 14.1.1", + "27001: A.14.1.2", + "27002: 14.1.2", + "27017: 14.1.2", + "27001: A.14.2.1", + "27002: 14.2.1", + "27017: 14.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.8", + "27001: A.8.25", + "27001: A.8.26", + "27001: A.8.28" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-2", + "PL-8", + "PL-8(1)", + "SA-3", + "SA-3(1)", + "SA-4", + "SA-4(2)", + "SA-4(3)", + "SA-4(8)", + "SA-4(9)", + "SA-5", + "SA-8", + "SA-8(1)-(7)", + "SA-8(9)-(13)", + "SA-8(15)-(20)", + "SA-8(22)", + "SA-8(24)-(28)", + "SA-8(30)-(33)", + "SA-17", + "SA-17(1)-(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-6", + "PR.DS-7", + "PR.IP-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "PR.IR-01", + "PR.PS-01", + "PR.PS-02", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.1", + "6.2.3", + "6.5.2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "AIS-05", + "Description": "Implement a testing strategy, including criteria for acceptance of new information systems, upgrades and new versions, which provides application security assurance and maintains compliance while enabling organizational speed of delivery goals. Automate when applicable and possible.", + "Name": "Automated Application Security Testing", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "AIS-01", + "AIS-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.12", + "16.13" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD2.3", + "SD2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.2.8", + "27001: A.14.2.9", + "27001: A.12.1.2", + "27002: 12.1.2", + "27001: A.14.1.1", + "27002: 14.1.1", + "27001: A.14.2.2", + "27002: 14.2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.25", + "27001: A.8.29", + "27001: A.8.32", + "27002: 8.25 (e)", + "27002: 8.32 (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SA-11", + "SA-11(1)-(9)", + "SI-6", + "SI-6(2)", + "SI-6(3)", + "SI-10", + "SI-10(1)-(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.PT-3", + "PR.IP-12", + "DE.CM-8" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "ID.RA-01", + "PR.PS-01", + "PR.PS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A.3.2.2", + "A.3.2.2.1", + "6.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.4", + "6.4.1", + "6.4.2", + "6.5.1" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "AIS-07", + "Description": "Define and implement a process to remediate application security vulnerabilities, automating remediation when possible.", + "Name": "Application Vulnerability Remediation", + "Attributes": [ + { + "Section": "Application & Interface Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.1", + "CC7.4", + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.2", + "16.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27001: A.12.6.1", + "27002: 12.6.1", + "27017: 12.6.1", + "27018: 12.6.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.26", + "27001: A.8.8", + "27002: 5.26 (j)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SI-2", + "SI-2(2)-(6)", + "SA-11", + "SA-11(2)", + "SA-15", + "SA-15(1)-(3)", + "SA-15(5)-(8)", + "SA-15(10)-(12)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.IP-12", + "DE.CM-8", + "RS.AN-5", + "RS.MI-3", + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "ID.RA-01", + "ID.RA-06", + "ID.RA-08", + "PR.PS-02", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.2", + "6.5", + "6.5.1-10" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "11.3.1", + "11.3.1.1" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "BCR-08", + "Description": "Periodically backup data stored in the cloud. Ensure the confidentiality, integrity and availability of the backup, and verify data restoration from backup for resiliency.", + "Name": "Backup", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "A1.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "11.1", + "11.2", + "11.3", + "11.4", + "11.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8", + "5.2.9" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.3", + "27017: 12.3", + "27018: 12.3.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.13", + "27001: A.5.23", + "27001: A.5.30", + "27002: 8.13", + "27002: 5.23 2nd (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-4", + "CP-4(4)", + "CP-6", + "CP-6(1)-(3)", + "CP-9", + "CP-9(1)", + "CP-9(2)", + "CP-10", + "CP-10(2)", + "CP-10(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-4", + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-11", + "RC.RP-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "9.5.1", + "12.10.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1", + "10.3.3" + ] + } + ] + } + ], + "Checks": [ + "objectstorage_bucket_versioning_enabled" + ] + }, + { + "Id": "BCR-09", + "Description": "Establish, document, approve, communicate, apply, evaluate and maintain a disaster response plan to recover from natural and man-made disasters. Update the plan at least annually or upon significant changes.", + "Name": "Disaster Response Plan", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "CC3.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8", + "5.2.9", + "1.6.1", + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "BC1.4", + "BC2.1", + "BC2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.29", + "27001: A.5.30", + "27002: 5.29", + "27002: 5.30" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2(1)", + "CP-2(2)", + "CP-2(3)", + "CP-2(5)", + "CP-2(6)", + "CP-2(7)", + "CP-2(8)", + "PE-13", + "PE-13(1)", + "PE-13(2)", + "PE-13(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-9", + "PR.IP-10", + "RC.IM-1", + "RC.IM-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.IM-04" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "BCR-11", + "Description": "Supplement business-critical equipment with redundant equipment independently located at a reasonable minimum distance in accordance with applicable industry standards.", + "Name": "Equipment Redundancy", + "Attributes": [ + { + "Section": "Business Continuity Management and Operational Resilience", + "CCMLite": "No", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.2", + "CC3.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-06" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.8" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "BC1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.20", + "27001: A.7.11", + "27001: A.8.14", + "27002: 5.20 (t)", + "27002: 8.14 (c)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2", + "CP-2(2)", + "CP-4(3)", + "CP-6", + "CP-6(1)", + "CP-7", + "CP-8", + "CP-8(1)-(3)", + "CP-9", + "CP-9(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.BE-4", + "ID.BE-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.OC-04", + "GV.OC-05", + "PR.IR-03" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CCC-04", + "Description": "Restrict the unauthorized addition, removal, update, and management of organization assets.", + "Name": "Unauthorized Change Protection", + "Attributes": [ + { + "Section": "Change Control and Configuration Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "CCC-04" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.1", + "1.3.4", + "5.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.4", + "SM2.6" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.1.4", + "27002: 12.1.4", + "27001: A.12.4.2", + "27002: 12.4.2", + "27001: A.14.2.2", + "27017: 14.2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.3", + "27001: A.8.4", + "27001: A.8.15", + "27001: A.8.31", + "27001: A.8.32" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-7", + "CA-7(4)", + "CM-3", + "CM-3(1)", + "CM-3(5)", + "CM-3(7)", + "CM-3(8)", + "CM-5", + "CM-5(1)", + "CM-5(4)", + "CM-5(5)", + "CM-6", + "CM-6(1)", + "CM-6(2)", + "CM-7", + "CM-7(1)", + "CM-7(4)", + "CM-7(5)", + "CM-7(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-1", + "ID.AM-2", + "ID.AM-4", + "PR.MA-1", + "PR.MA-2", + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-01", + "ID.AM-02", + "ID.AM-04", + "ID.AM-08", + "PR.PS-02", + "PR.PS-03", + "PR.PS-05", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.5.1", + "6.5.2" + ] + } + ] + } + ], + "Checks": [ + "events_rule_iam_group_changes", + "events_rule_iam_policy_changes", + "events_rule_user_changes", + "events_rule_vcn_changes" + ] + }, + { + "Id": "CCC-07", + "Description": "Implement detection measures with proactive notification in case of changes deviating from the established baseline.", + "Name": "Detection of Baseline Deviation", + "Attributes": [ + { + "Section": "Change Control and Configuration Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC8.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-01" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.5.1", + "1.5.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.2.2", + "27001: A.14.2.4", + "27001: A.12.4.1", + "27002: 12.4.1 (g)", + "27001: A.5.1.1", + "27017: 5.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.9", + "27001: A.8.15", + "27002: 8.9" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-6", + "CM-6(2)", + "SI-2", + "SI-2(2)-(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.MA-1", + "PR.IP-1", + "DE.DP-4", + "PR.IP-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-01", + "DE.CM-09", + "DE.AE-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4.5.3", + "6.4.5.4", + "11.5", + "11.5.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "11.5.2", + "11.6.1" + ] + } + ] + } + ], + "Checks": [ + "cloudguard_enabled", + "events_rule_cloudguard_problems", + "events_rule_network_gateway_changes", + "events_rule_network_security_group_changes", + "events_rule_route_table_changes", + "events_rule_security_list_changes", + "events_rule_vcn_changes" + ] + }, + { + "Id": "CEK-03", + "Description": "Provide cryptographic protection to data at-rest and in-transit, using cryptographic libraries certified to approved standards.", + "Name": "Data Encryption", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-03", + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.6", + "3.1", + "3.11", + "11.3", + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.1", + "27001: A.18.1.2", + "27001: A.18.1.3", + "27001: A.18.1.4", + "27001: A.18.1.5", + "27001: A.10.1", + "27002: 10.1", + "27001: A.13.2.1", + "27002: 13.2.1", + "27001: A.18", + "27002: 18", + "27001: A.14.1.2", + "27002: 14.1.2", + "27001: A.14.1.3", + "27002 14.1.3 c)", + "27001 - A.10.1.1", + "27017 - 10.1.1", + "27001 - A.10.1.2", + "27017 - 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.8.24", + "27002: 8.24 Other Information (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-19", + "AC-19(5)", + "SC-8", + "SC-8(1)", + "SC-8(3)", + "SC-8(4)", + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-28", + "SC-28(1)-(3)", + "SI-4", + "SI-4(10)", + "SI-7", + "SI-7(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "Requirement 3", + "2.2.3", + "2.3", + "3.4", + "3.5.3", + "4.1", + "8.2.1", + "PCI Glossary - Strong Cryptography" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.7", + "3.5.1", + "4.2.1", + "4.2.1.2", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "blockstorage_block_volume_encrypted_with_cmk", + "blockstorage_boot_volume_encrypted_with_cmk", + "compute_instance_in_transit_encryption_enabled", + "filestorage_file_system_encrypted_with_cmk", + "objectstorage_bucket_encrypted_with_cmk" + ] + }, + { + "Id": "CEK-04", + "Description": "Use encryption algorithms that are appropriate for data protection, considering the classification of data, associated risks, and usability of the encryption technology.", + "Name": "Encryption Algorithm", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.2", + "27001: 6.1.3", + "27001: A.8.2", + "27002: 8.2", + "27001: A.8.3", + "27001: A.10.1.1", + "27002: 10.1.1 (b)", + "27001: A.10.1.2", + "27002: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.2", + "27001: 6.1.3", + "27001: A.8.24", + "27001: A.5.12", + "27001: A.5.13", + "27002: 8.24 General (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2", + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02", + "ID.AM-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A2", + "Requirement 3", + "2.3", + "2.2.3", + "3.4", + "3.5.3", + "4.1", + "8.2.1", + "PCI Glossary - Strong Cryptography" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.7", + "3.5.1", + "4.2.1", + "4.2.1.2", + "4.2.2" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CEK-08", + "Description": "CSPs must provide the capability for CSCs to manage their own data encryption keys.", + "Name": "CSC Key Management Capability", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2", + "SC2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1", + "27017: 10.1", + "27001: A.10.1.1", + "27017: 10.1.1", + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.23", + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-9", + "CP-9(8)", + "SA-9", + "SA-9(6)", + "SC-12", + "SC-12(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.SC-3", + "ID.AM-6", + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.SC-05" + ] + } + ] + } + ], + "Checks": [ + "blockstorage_block_volume_encrypted_with_cmk", + "blockstorage_boot_volume_encrypted_with_cmk", + "filestorage_file_system_encrypted_with_cmk", + "objectstorage_bucket_encrypted_with_cmk" + ] + }, + { + "Id": "CEK-10", + "Description": "Generate Cryptographic keys using industry accepted cryptographic libraries specifying the algorithm strength and the random number generator used.", + "Name": "Key Generation", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2", + "TS2.3", + "SY1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27002: 10.1.1 (e)", + "27017: 10.1.1", + "27001: A.10.1.2", + "27002: 10.1.2", + "27002: 10.1.2 (a)", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24", + "27002: 8.24 (d), Key management (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.2.3", + "3.6.1", + "PCI Glossary - Cryptographic Key Generation" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1", + "3.6.1.1", + "3.7.1" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "CEK-12", + "Description": "Rotate cryptographic keys in accordance with the calculated cryptoperiod, which includes provisions for considering the risk of information disclosure and legal and regulatory requirements.", + "Name": "Key Rotation", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27017: 10.1.1", + "27001: A.10.1.2", + "27002: 10.1.2 e)", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.31", + "27001: A.8.24", + "27002: 5.31 Cryptography", + "27002: 8.24 Key management (e,m)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)", + "SC-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "ID.GV-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05", + "GV.OC-03" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.7.4", + "3.7.5" + ] + } + ] + } + ], + "Checks": [ + "kms_key_rotation_enabled", + "identity_user_api_keys_rotated_90_days", + "identity_user_auth_tokens_rotated_90_days", + "identity_user_customer_secret_keys_rotated_90_days", + "identity_user_db_passwords_rotated_90_days" + ] + }, + { + "Id": "CEK-14", + "Description": "Define, implement and evaluate processes, procedures and technical measures to destroy keys stored outside a secure environment and revoke keys stored in Hardware Security Modules (HSMs) when they are no longer needed, which include provisions for legal and regulatory requirements.", + "Name": "Key Destruction", + "Attributes": [ + { + "Section": "Cryptography, Encryption & Key Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.1", + "27017: 10.1.1", + "27017: 10.1.2", + "27001: A.10.1.2", + "27002: 10.1.2 (j)", + "27001: A.18.1.3", + "27002: 18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.31", + "27001: A.8.24", + "27002: 5.31 Cryptography", + "27002: 8.24 Key management (j,m)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-12", + "SC-12(2)", + "SC-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.IP-6", + "ID.GV-3", + "PR.DS-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05", + "ID.AM-08", + "GV.OC-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.6.4", + "3.6.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.7.4", + "3.7.5" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "DCS-06", + "Description": "Catalogue and track all relevant physical and logical assets located at all of the CSP's sites within a secured system.", + "Name": "Assets Cataloguing and Tracking", + "Attributes": [ + { + "Section": "Datacenter Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DCS - 01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "1.1", + "2.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SM2.6" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.1.1", + "27002: 8.1.1", + "27017: 8.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.9" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-8", + "CM-8(1)", + "CM-8(2)", + "CM-8(4)", + "CM-8(7)", + "CM-8(8)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-1", + "ID.AM-2", + "ID.AM-4", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-01", + "ID.AM-02", + "ID.AM-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.4", + "9.7.1", + "9.9.1", + "9.9.1.a", + "9.9.1.b", + "9.9.1.c", + "12.3.3", + "12.3.4" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1.1", + "6.3.2", + "9.4.2", + "9.4.3", + "12.5.1" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "DSP-02", + "Description": "Apply industry accepted methods for the secure disposal of data from storage media such that data is not recoverable by any forensic means.", + "Name": "Secure Disposal", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3", + "CC6.4", + "CC6.5", + "CC6.7", + "P4.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DSI-07" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.3.3", + "7.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.3.2", + "27002: 8.3.2", + "27001: A.11.2.7", + "27002: 11.2.7" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.7.10", + "27001: A.7.14", + "27001: A.8.10", + "27002: 7.10 (Secure reuse or disposal)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-22", + "SI-12", + "SI-12(3)", + "SI-18", + "SI-18(1)", + "SI-18(4)", + "SI-18(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.SC-10", + "PR.PS-02", + "PR.PS-03", + "ID.AM-08" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.1", + "9.8", + "9.8.1", + "9.8.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1", + "3.7.5", + "9.4.7" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "DSP-03", + "Description": "Create and maintain a data inventory, at least for any sensitive data and personal data.", + "Name": "Data Inventory", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1", + "1.3.2", + "1.3.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.1.1", + "27002: 8.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.9", + "27001: A.8.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-12", + "CM-12(1)", + "PM-5", + "PM-5(1)", + "SI-12", + "SI-12(1)", + "SI-19", + "SI-19(1)", + "SI-19(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1", + "9.4.5" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "DSP-04", + "Description": "Classify data according to its type and sensitivity level.", + "Name": "Data Classification", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "C1.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "DSI-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.7" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.3.1", + "1.3.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.8.2.1", + "27002: 8.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-16", + "AC-16(9)", + "PM-22", + "PM-23", + "PT-2", + "PT-2(1)", + "SI-18", + "SI-18(2)", + "SI-19", + "SI-19(6)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.AM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-05", + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "9.6.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "9.4.2", + "9.4.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "DSP-07", + "Description": "Develop systems, products, and business practices based upon a principle of security by design and industry best practices.", + "Name": "Data Protection by Design and Default", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "PI1.2", + "PI1.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "16.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.3.1", + "5.3.2", + "5.3.3", + "5.3.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SD2.2", + "IM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.14.1.1", + "27002:14.1.1", + "27001: A.14.2.5", + "27002:14.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.27", + "27001: A.8.28", + "27001: A.8.29", + "27002: 5.8 (Information security requirements a-i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-17", + "PM-24", + "PM-25", + "PT-2", + "PT-2(2)", + "SA-3", + "SA-4", + "SA-5", + "SA-8", + "SA-8(9)", + "SA-8(13)", + "SA-8(18)", + "SA-8(20)", + "SA-8(22)", + "SA-8(23)", + "SA-8(33)", + "SA-15", + "SA-15(12)", + "SC-3", + "SC-3(3)", + "SC-7", + "SC-7(24)", + "SC-8", + "SC-8(1)-(4)", + "SC-28", + "SC-28(1)", + "SI-12", + "SI-12(1)-(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-2", + "PR.PT-3", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "PR.PS-06" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.2.1" + ] + } + ] + } + ], + "Checks": [ + "objectstorage_bucket_not_publicly_accessible", + "database_autonomous_database_access_restricted", + "analytics_instance_access_restricted", + "integration_instance_access_restricted" + ] + }, + { + "Id": "DSP-10", + "Description": "Define, implement and evaluate processes, procedures and technical measures that ensure any transfer of personal or sensitive data is protected from unauthorized access and only processed within scope as permitted by the respective laws and regulations.", + "Name": "Sensitive Data Transfer", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-02", + "EKM-03" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.1", + "3.12", + "3.13" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "9.5.1", + "9.5.2", + "9.5.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.4", + "IM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.13.2.1", + "27002: 13.2.1", + "27001: A.8.3.3", + "27002: 8.3.3", + "27001: A.13.2.3", + "27002: 13.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.7.10" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-4", + "AC-4(23)-(25)", + "CA-3", + "CA-3(6)", + "CA-6", + "CA-6(1)", + "CA-6(2)", + "SC-4", + "SC-4(2)", + "SC-7", + "SC-7(10)", + "SC-7(24)", + "SC-8", + "SC-8(1)-(5)", + "SC-16", + "SC-16(1)-(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2", + "PR.DS-5", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02", + "PR.IR-01", + "ID.AM-03", + "GV.OC-03", + "ID.AM-07" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "4.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "4.1.1", + "4.2.1", + "4.2.2" + ] + } + ] + } + ], + "Checks": [ + "compute_instance_in_transit_encryption_enabled" + ] + }, + { + "Id": "DSP-16", + "Description": "Data retention, archiving and deletion is managed in accordance with business requirements, applicable laws and regulations.", + "Name": "Data Retention and Deletion", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "C1.1", + "C1.2", + "CC3.1", + "P4.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-02", + "BCR-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.4", + "3.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.3.1", + "7.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.33", + "27001: A.8.10", + "27002: 5.33 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SI-12", + "SI-12(1)-(3)", + "SI-18", + "SI-18(1)", + "SI-18(4)", + "SI-18(5)", + "SI-19", + "SI-19(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-3", + "PR.IP-6", + "ID.GV-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-08", + "GV.OC-03", + "GV.SC-10", + "PR.DS-11" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.2.1" + ] + } + ] + } + ], + "Checks": [ + "audit_log_retention_period_365_days" + ] + }, + { + "Id": "DSP-17", + "Description": "Define and implement, processes, procedures and technical measures to protect sensitive data throughout it's lifecycle.", + "Name": "Sensitive Data Protection", + "Attributes": [ + { + "Section": "Data Security and Privacy Lifecycle Management", + "CCMLite": "Yes", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSC-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.1", + "CC6.1", + "CC6.3", + "CC6.7", + "CC8.1", + "C1.1", + "P2.0", + "P3.0", + "P4.0", + "P5.0", + "P6.0" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.1", + "3.1", + "3.14" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.3.3", + "9.1.1", + "9.2.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.1", + "IM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3", + "27002: 18.1.3", + "27001:A.18.1.4", + "27002:18.1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.11", + "27001: A.8.12" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-2", + "PM-22", + "PM-24", + "PT-7", + "PT-7(1)", + "PT-7(2)", + "PT-8", + "SC-8", + "SC-8(1)-(5)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1", + "PR.DS-2", + "PR.DS-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01", + "PR.DS-02", + "PR.DS-10" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.0 (including all subsections)", + "4.0 (including all subsections)" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.1.1", + "4.1.1" + ] + } + ] + } + ], + "Checks": [ + "objectstorage_bucket_not_publicly_accessible", + "objectstorage_bucket_encrypted_with_cmk", + "database_autonomous_database_access_restricted", + "blockstorage_block_volume_encrypted_with_cmk", + "blockstorage_boot_volume_encrypted_with_cmk" + ] + }, + { + "Id": "GRC-05", + "Description": "Develop and implement an Information Security Program, which includes programs for all the relevant domains of the CCM.", + "Name": "Information Security Program", + "Attributes": [ + { + "Section": "Governance, Risk and Compliance", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-04" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "14.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SG2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 4.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-1", + "PM-3", + "PM-14", + "PL-2", + "PM-18", + "PM-31" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.4.1", + "A.3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.4.1", + "A3.1.1" + ] + } + ] + } + ], + "Checks": [ + "cloudguard_enabled" + ] + }, + { + "Id": "IAM-02", + "Description": "Establish, document, approve, communicate, implement, apply, evaluate and maintain strong password policies and procedures. Review and update the policies and procedures at least annually.", + "Name": "Strong Password Policy and Procedures", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-12", + "GRM-06", + "GRM-09" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.1.1", + "1.5.1", + "4.1.2", + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1", + "SA1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5", + "27002: 5", + "27001: A.9.4.3", + "27002: 9.4.3", + "27017: 9.4.3", + "27018: 9.4.3", + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27001: A.7.2.2", + "27002: 7.2.2", + "27001: A.9.2.6", + "27002: 9.2.6", + "27001: A.9.2.3", + "27002: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5.1", + "27001: A.5.4", + "27001: A.5.17", + "27001: A.6.3", + "27001: A.8.5", + "27001: A.5.37" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(3)", + "AC-2(11)", + "AC-3", + "AC-3(3)", + "AC-12", + "AC-12(1)", + "IA-2", + "IA-2(10)", + "IA-5", + "IA-5(1)", + "IA-5(18)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "PR.AC-1", + "PR.AC-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.PO-01", + "GV.PO-02", + "ID.IM-03", + "PR.AA-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.4", + "12.1", + "12.1.1", + "12.11" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.1.1", + "8.3.8" + ] + } + ] + } + ], + "Checks": [ + "identity_password_policy_minimum_length_14", + "identity_password_policy_expires_within_365_days", + "identity_password_policy_prevents_reuse" + ] + }, + { + "Id": "IAM-03", + "Description": "Manage, store, and review the information of system identities, and level of access.", + "Name": "Identity Inventory", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-04", + "IAM-08", + "IAM-10" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1", + "5.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.2 (c)", + "27001: A.8.1.1", + "27002: 8.1.1", + "27001: A.9.4.1", + "27002: 9.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.2 (c)", + "27001: A.5.15", + "27001: A.5.16", + "27001: A.5.18", + "27001: A.7.4", + "27001: A.8.15", + "27001: A.8.2", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-10", + "AU-10(1)", + "AU-10(2)", + "AU-16", + "AU-16(1)", + "IA-4", + "IA-4(8)", + "IA-4(9)", + "IA-5", + "IA-5(5)", + "IA-8", + "IA-8(4)", + "PM-5(1)", + "SA-8", + "SA-8(22)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-04", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.4.a" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.5", + "7.2.5.1" + ] + } + ] + } + ], + "Checks": [ + "identity_user_api_keys_rotated_90_days", + "identity_user_auth_tokens_rotated_90_days", + "identity_user_customer_secret_keys_rotated_90_days", + "identity_user_valid_email_address" + ] + }, + { + "Id": "IAM-04", + "Description": "Employ the separation of duties principle when implementing information system access.", + "Name": "Separation of Duties", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC1.3", + "CC5.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.2.2", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.6.1.2", + "27002: 6.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.18", + "27001: A.5.3", + "27001: A.8.2" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(3)", + "AC-2(11)", + "AC-6", + "AC-6(1)-(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.4", + "6.4.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.5.3", + "6.5.4", + "7.2.1", + "7.2.2" + ] + } + ] + } + ], + "Checks": [ + "identity_service_level_admins_exist", + "identity_iam_admins_cannot_update_tenancy_admins", + "identity_tenancy_admin_permissions_limited" + ] + }, + { + "Id": "IAM-05", + "Description": "Employ the least privilege principle when implementing information system access.", + "Name": "Least Privilege", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-06", + "IVS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.1.1", + "27002: 9.1.1", + "27001: A.9.1.2", + "27002: 9.1.2", + "27001: A.9.2.3", + "27002: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.8.2", + "27002: 5.15 (Other information 2nd (a))" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(4)", + "IA-12", + "IA-12(2)", + "IA-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "7.1", + "7.1.1", + "7.1.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "7.2.2", + "7.2.5", + "7.2.6" + ] + } + ] + } + ], + "Checks": [ + "identity_tenancy_admin_permissions_limited", + "identity_service_level_admins_exist", + "identity_no_resources_in_root_compartment", + "identity_non_root_compartment_exists" + ] + }, + { + "Id": "IAM-07", + "Description": "De-provision or respectively modify access of movers / leavers or system identity changes in a timely manner in order to effectively adopt and communicate identity and access management policies.", + "Name": "User Access Changes and Revocation", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.3", + "6.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.18" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(1)", + "AC-2(2)", + "AC-2(6)", + "AC-2(8)", + "AC-3", + "AC-3(8)", + "AC-6", + "AC-6(7)", + "AU-10", + "AU-10(4)", + "AU-16", + "AU-16(1)", + "CM-7", + "CM-7(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4", + "PR.IP-11" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.RR-04", + "GV.SC-10", + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1.2", + "8.1.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.2.5", + "8.2.6" + ] + } + ] + } + ], + "Checks": [ + "identity_user_api_keys_rotated_90_days", + "identity_user_auth_tokens_rotated_90_days", + "identity_user_customer_secret_keys_rotated_90_days" + ] + }, + { + "Id": "IAM-08", + "Description": "Review and revalidate user access for least privilege and separation of duties with a frequency that is commensurate with organizational risk tolerance.", + "Name": "User Access Review", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-10" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.5", + "27001: A.9.2.6", + "27001: A.9.4.1", + "27017: 9.4.1", + "27001: A.6.1.2", + "27001: A 9.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.3", + "27001: A.5.18", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(4)", + "AC-6(8)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.5.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.5.1", + "7.2.5", + "7.2.4" + ] + } + ] + } + ], + "Checks": [ + "identity_user_api_keys_rotated_90_days", + "identity_user_auth_tokens_rotated_90_days", + "identity_user_customer_secret_keys_rotated_90_days", + "identity_user_db_passwords_rotated_90_days" + ] + }, + { + "Id": "IAM-09", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the segregation of privileged access roles such that administrative access to data, encryption and key management capabilities and logging capabilities are distinct and separated.", + "Name": "Segregation of Privileged Access Roles", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.1", + "CC6.1", + "CC6.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.3", + "27002: 9.2.3", + "27017: 9.2.3", + "27018: 9.2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.2", + "27001: A.8.18", + "27002: 8.2 (j)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-3(7)", + "AC-6(4)", + "AC-6(8)", + "IA-5", + "IA-5(6)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.3", + "3.5.2", + "7.1.2", + "7.1.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.6.1", + "3.7.6", + "6.5.3", + "6.5.4", + "7.2.1", + "7.2.2", + "10.3.1" + ] + } + ] + } + ], + "Checks": [ + "identity_tenancy_admin_permissions_limited", + "identity_iam_admins_cannot_update_tenancy_admins", + "identity_tenancy_admin_users_no_api_keys" + ] + }, + { + "Id": "IAM-10", + "Description": "Define and implement an access process to ensure privileged access roles and rights are granted for a time limited period, and implement procedures to prevent the culmination of segregated privileged access.", + "Name": "Management of Privileged Access Roles", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1", + "6.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.3", + "27002: 9.2.3", + "27017: 9.2.3", + "27018: 9.2.3", + "27001: A.9.4.4", + "27002: 9.4.4", + "27017: 9.4.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.2", + "27001: A.8.18", + "27002: 8.2 (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(7)", + "AC-3", + "AC-3(4)", + "AC-3(11)", + "AC-3(13)", + "AC-3(14)", + "AC-6", + "AC-6(4)", + "AC-6(5)", + "AC-6(8)", + "AC-12", + "AC-12(3)", + "AC-17", + "AC-17(4)", + "IA-8", + "IA-8(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "7.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "7.2.2" + ] + } + ] + } + ], + "Checks": [ + "identity_tenancy_admin_permissions_limited", + "identity_tenancy_admin_users_no_api_keys", + "identity_iam_admins_cannot_update_tenancy_admins" + ] + }, + { + "Id": "IAM-12", + "Description": "Define, implement and evaluate processes, procedures and technical measures to ensure the logging infrastructure is read-only for all with write access, including privileged access roles, and that the ability to disable it is controlled through a procedure that ensures the segregation of duties and break glass procedures.", + "Name": "Safeguard Logs Integrity", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.3" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1", + "27018: 12.4.1", + "27001: A.12.4.2", + "27002: 12.4.2", + "27017: 12.4.2", + "27018: 12.4.2", + "27001: A.12.4.3", + "27002: 12.4.3", + "27017: 12.4.3", + "27018: 12.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15", + "27001: A.8.18", + "27002: 8.15 Protection of Logs" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-2", + "AC-2(11)", + "AC-2(12)", + "IA-8", + "IA-8(4)", + "SA-8", + "SA-8(22)", + "SC-34", + "SC-34(1)", + "SC-34(2)", + "SC-36", + "SI-4", + "SI-4(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4" + ] + } + ] + } + ], + "Checks": [ + "audit_log_retention_period_365_days" + ] + }, + { + "Id": "IAM-13", + "Description": "Define, implement and evaluate processes, procedures and technical measures that ensure users are identifiable through unique IDs or which can associate individuals to the usage of user IDs.", + "Name": "Uniquely Identifiable Users", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.1", + "27002: 9.2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.16" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-3", + "AC-3(14)", + "AC-24", + "AC-24(2)", + "AU-10", + "AU-10(1)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(12)", + "IA-4", + "IA-4(1)", + "SA-8", + "SA-8(22)", + "SC-23", + "SC-23(3)", + "SC-40(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1", + "8.2", + "8.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "8.2.1", + "8.2.2", + "8.2.4" + ] + } + ] + } + ], + "Checks": [ + "identity_user_mfa_enabled_console_access", + "identity_user_valid_email_address" + ] + }, + { + "Id": "IAM-14", + "Description": "Define, implement and evaluate processes, procedures and technical measures for authenticating access to systems, application and data assets, including multifactor authentication for at least privileged user and sensitive data access. Adopt digital certificates or alternatives which achieve an equivalent level of security for system identities.", + "Name": "Strong Authentication", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02", + "IAM-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "6.3", + "6.5", + "12.5", + "12.7" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3", + "SA1.4", + "SA1.8" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.1.2", + "27002: 9.1.2", + "27017: 9.1.2", + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27001: A.9.4.2", + "27002: 9.4.2", + "27017: 9.4.2", + "27018: 9.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.15", + "27001: A.5.17", + "27001: A.8.5", + "27001: A.8.24", + "27002: 8.5", + "27002: 8.24 other information (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-6", + "AC-6(5)", + "AC-7", + "AC-7(4)", + "AU-10", + "AU-10(2)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(8)", + "IA-2(12)", + "IA-3", + "IA-3(1)", + "IA-5", + "IA-5(2)", + "IA-5(7)", + "IA-5(9)", + "IA-5(10)", + "IA-5(12)", + "IA-5(14)-(16)", + "IA-8", + "IA-8(1)", + "IA-8(6)", + "SC-23", + "SC-23(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-6", + "PR.AC-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.1.2", + "8.1.3", + "8.1.6", + "8.2", + "8.3", + "8.3.2", + "12.3.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.1", + "8.3.1", + "8.3.2", + "8.4.1", + "8.4.2", + "8.4.3" + ] + } + ] + } + ], + "Checks": [ + "identity_user_mfa_enabled_console_access" + ] + }, + { + "Id": "IAM-15", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the secure management of passwords.", + "Name": "Passwords Management", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.4", + "27002: 9.2.4", + "27017: 9.2.4", + "27018: 9.2.4", + "27001: A.9.3.1", + "27002: 9.3.1", + "27017: 9.3.1", + "27018: 9.3.1", + "27001: A.9.4.3", + "27002: 9.4.3", + "27017: 9.4.3", + "27018: 9.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.17" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IA-4", + "IA-4(8)", + "IA-5", + "IA-5(1)", + "IA-5(8)", + "IA-5(18)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "8.2", + "8.2.1-6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.2", + "2.3.1", + "8.3.5", + "8.3.6", + "8.3.7", + "8.3.8", + "8.3.9", + "8.3.10", + "8.3.10.1", + "8.6.2" + ] + } + ] + } + ], + "Checks": [ + "identity_password_policy_minimum_length_14", + "identity_password_policy_expires_within_365_days", + "identity_password_policy_prevents_reuse" + ] + }, + { + "Id": "IAM-16", + "Description": "Define, implement and evaluate processes, procedures and technical measures to verify access to data and system functions is authorized.", + "Name": "Authorization Mechanisms", + "Attributes": [ + { + "Section": "Identity & Access Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.2", + "CC6.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IAM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "5.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SA1.3", + "SA1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.9.2.5", + "27002: 9.2.5", + "27017: 9.2.5", + "27018: 9.2.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.18" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-3", + "AC-3(5)", + "AC-4", + "AC-4(17)", + "AC-4(21)", + "AC-4(22)", + "AC-6", + "AC-6(8)", + "AC-6(9)", + "AC-12", + "AC-12(1)", + "AC-20", + "AC-20(1)", + "AU-10", + "AU-10(1)", + "AU-10(2)", + "IA-2", + "IA-2(1)", + "IA-2(2)", + "IA-2(12)", + "IA-3", + "IA-3(1)", + "IA-5(1)", + "IA-5(2)", + "IA-5(5)", + "IA-5(8)", + "IA-5(10)", + "IA-5(12)", + "IA-8", + "IA-8(1)", + "IA-8(2)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4", + "PR.AC-6", + "PR.AC-7", + "PR.PT-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-01", + "PR.AA-02", + "PR.AA-03", + "PR.AA-04", + "PR.AA-05", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.3", + "7.1.4" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "7.2.4", + "7.2.3", + "7.2.5.1" + ] + } + ] + } + ], + "Checks": [ + "identity_tenancy_admin_permissions_limited", + "identity_service_level_admins_exist", + "database_autonomous_database_access_restricted", + "analytics_instance_access_restricted", + "integration_instance_access_restricted" + ] + }, + { + "Id": "IPY-03", + "Description": "Implement cryptographically secure and standardized network protocols for the management, import and export of data.", + "Name": "Secure Interoperability and Portability Management", + "Attributes": [ + { + "Section": "Interoperability & Portability", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IPY-04" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY1.1", + "SY1.2", + "NC1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1", + "27001: A.15.1.1", + "27002: 15.1.1", + "27017: 15.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.19", + "27001: A.5.23", + "27001: A.5.31", + "27001: A.5.32", + "27001: A.5.33", + "27001: A.5.34" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PT-2", + "PT-2(2)", + "SA-4", + "SC-16", + "SC-16(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.2.1", + "1.2.5", + "1.2.6", + "2.2.4", + "2.2.5", + "2.2.7", + "4.2.1" + ] + } + ] + } + ], + "Checks": [ + "compute_instance_in_transit_encryption_enabled" + ] + }, + { + "Id": "IVS-02", + "Description": "Plan and monitor the availability, quality, and adequate capacity of resources in order to deliver the required system performance as determined by the business.", + "Name": "Capacity and Resource Planning", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "No", + "IaaS": "CSP-Owned", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "A1.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-04" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.3", + "27001: 6.1", + "27001: 9.1", + "27001: A.12.1.3", + "27002: 12.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.3 (b)", + "27001: 6.1", + "27001: 9.1", + "27001: A.8.6", + "27001: A.8.14" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CP-2", + "CP-2(2)", + "SC-5", + "SC-5(2)", + "SC-4", + "SI-4" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-4", + "ID.BE-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.IR-04", + "GV.OC-04" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "IVS-03", + "Description": "Monitor, encrypt and restrict communications between environments to only authenticated and authorized connections, as justified by the business. Review these configurations at least annually, and support them by a documented justification of all allowed services, protocols, ports, and compensating controls.", + "Name": "Network Security", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-06" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.8", + "3.1", + "12.2", + "13.6", + "13.9" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "NC1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.13.1.1", + "27002: 13.1.1", + "27001: A.13.1.2", + "27002: 13.1.2", + "27001: A.13.1.3", + "27002: 13.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.5.15", + "27001: A.5.37", + "27001: A.8.5", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.21", + "27001: A.8.22", + "27001: A.8.24", + "27002: A.5.15 2nd c)", + "27002: 8.20", + "27002: 8.21", + "27002: 8.22", + "27002: 8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-1", + "SC-4", + "SC-7", + "SC-7(4)", + "SC-7(5)", + "SC-7(8)", + "SC-7(9)", + "SC-7(11)", + "SC-8", + "SC-8(1)", + "SC-11", + "SC-12", + "SC-16", + "SC-23", + "SC-29", + "SC-29(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-5", + "PR.AC-7", + "PR.PT-4", + "DE.CM-1", + "DE.CM-7", + "PR.DS-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.IR-01", + "PR.AA-03", + "PR.AA-05", + "DE.CM-01", + "PR.DS-02", + "ID.AM-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "1.1.6", + "1.2", + "1.2.3", + "2.2", + "4.1.1", + "10.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.2.5", + "1.2.6", + "1.2.7", + "1.4.2", + "2.2.4", + "2.2.5", + "2.2.7", + "4.2.1", + "10.1.1" + ] + } + ] + } + ], + "Checks": [ + "network_vcn_subnet_flow_logs_enabled", + "network_default_security_list_restricts_traffic", + "network_security_group_ingress_from_internet_to_ssh_port", + "network_security_group_ingress_from_internet_to_rdp_port", + "network_security_list_ingress_from_internet_to_ssh_port", + "network_security_list_ingress_from_internet_to_rdp_port" + ] + }, + { + "Id": "IVS-04", + "Description": "Harden host and guest OS, hypervisor or infrastructure control plane according to their respective best practices, and supported by technical controls, as part of a security baseline.", + "Name": "OS Hardening and Base Controls", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "CSP-Owned", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.8", + "CC7.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-07", + "IVS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "4.1", + "4.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.1.3", + "5.2.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SY1.1", + "SY1.3", + "SY1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.14.2.2", + "27002: 14.2.2", + "27001: A.14.2.3", + "27001 A.14.2.4", + "27018: 12.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5", + "27001: 9.1", + "27001: A.5.37", + "27001: A.8.5", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.22", + "27001: A.8.24", + "27002: 8.20", + "27002: 8.22", + "27002: 8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-6", + "CM-6(1)", + "SC-29", + "SC-29(1)", + "SC-2", + "SC-7", + "SC-7(12)", + "SC-30", + "SC-34", + "SC-35", + "SC-39", + "SC-44" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.IP-1", + "PR.PT-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "2.2.1" + ] + } + ] + } + ], + "Checks": [ + "compute_instance_legacy_metadata_endpoint_disabled", + "compute_instance_secure_boot_enabled" + ] + }, + { + "Id": "IVS-06", + "Description": "Design, develop, deploy and configure applications and infrastructures such that CSP and CSC (tenant) user access and intra-tenant access is appropriately segmented and segregated, monitored and restricted from other tenants.", + "Name": "Segmentation and Segregation", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-09" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.3.4", + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SC2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 9.1", + "27001: A.13.1.3", + "27002: 13.1.3", + "27017: 13.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 9.1", + "27001: A.5.15", + "27001: A.5.20", + "27001: A.8.3", + "27001: A.8.9", + "27001: A.8.16", + "27001: A.8.22", + "27002: 5.15 (b)", + "27002: 8.3 (b)", + "27002: 8.16 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-3", + "SC-7", + "SC-7(20)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.AC-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.IR-01", + "PR.PS-01", + "PR.PS-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "2.6", + "8.3.1", + "10.8", + "11.3", + "A3.2.1", + "A3.3.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "A1.1.1", + "A1.1.2", + "A1.1.3" + ] + } + ] + } + ], + "Checks": [ + "network_default_security_list_restricts_traffic", + "identity_non_root_compartment_exists", + "identity_no_resources_in_root_compartment" + ] + }, + { + "Id": "IVS-07", + "Description": "Use secure and encrypted communication channels when migrating servers, services, applications, or data to cloud environments. Such channels must include only up-to-date and approved protocols.", + "Name": "Migration to Cloud Environments", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-10" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.4", + "IM1.4", + "NC1.4", + "SC2.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.13.1.1", + "27002: 13.1.1", + "27017: 13.1.1", + "27018: 13.1.1", + "27001: A.13.1.2", + "27002: 13.1.2", + "27017: 13.1.2", + "27018: 13.1.2", + "27001: A.13.1.3", + "27002: 13.1.3", + "27017: 13.1.3", + "27018: 13.1.3", + "27001: A.13.2.1", + "27002: 13.2.1", + "27017: 13.2.1", + "27018: 13.2.1", + "27001: A.13.2.2", + "27002: 13.2.2", + "27017: 13.2.2", + "27018: 13.2.2", + "27001: A.13.2.3", + "27002: 13.2.3", + "27017: 13.2.3", + "27018: 13.2.3", + "27001: A.13.2.4", + "27002: 13.2.4", + "27017: 13.2.4", + "27018: 13.2.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.14", + "27001: A.8.20", + "27001: A.8.24", + "27002: 8.20 (e)", + "27002: 8.24 Guidance (b,f), other information (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-17", + "AC-20", + "SC-7", + "SC-7(28)", + "SC-8", + "SC-8(1)", + "SC-12", + "SC-23", + "SC-29", + "SI-7", + "SI-7(1)-(3)", + "SI-7(5)-(10)", + "SI-7(12)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-2", + "PR.PT-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "4.2.1" + ] + } + ] + } + ], + "Checks": [ + "compute_instance_in_transit_encryption_enabled" + ] + }, + { + "Id": "IVS-09", + "Description": "Define, implement and evaluate processes, procedures and defense-in-depth techniques for protection, detection, and timely response to network-based attacks.", + "Name": "Network Defense", + "Attributes": [ + { + "Section": "Infrastructure & Virtualization Security", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.6", + "CC6.8", + "CC7.1", + "CC7.2", + "CC7.5" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-13" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "13.3", + "13.8" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.3", + "5.2.4", + "5.2.5", + "5.2.7", + "5.3.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "NC1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1", + "27001: 6.2", + "27001: A.14.1.2", + "27002: 14.1.2", + "27017: 14.1.2", + "27001: A.11.1.4", + "27002: 11.1.4", + "27017: 11.1.4", + "27018: 16.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1", + "27001: 6.2", + "27001: A.5.24", + "27001: A.5.26", + "27001: A.8.8", + "27001: A.8.16", + "27001: A.8.20", + "27001: A.8.21", + "27001: A.8.22", + "27001: A.8.26", + "27002: 8.8 (i)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PL-8", + "PL-8(1)", + "SC-5", + "SC-5(1)", + "SC-5(3)", + "SC-7", + "SC-7(13)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.DP-1", + "DE.CM-1", + "DE.CM-7", + "PR.AC-5", + "RS.MI-2", + "PR.DS-2", + "RS.RP-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-03", + "DE.CM-01", + "PR.IR-01", + "RS.MA-01", + "RS.MI-01", + "RS.MI-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.6", + "1.1", + "1.2", + "1.3", + "1.5", + "12.10.5" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "1.1.1", + "1.3.1", + "1.3.2", + "1.3.3", + "1.4.1", + "1.4.2", + "1.4.3", + "1.4.4", + "1.4.5", + "1.5.1", + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "cloudguard_enabled", + "events_rule_cloudguard_problems" + ] + }, + { + "Id": "LOG-02", + "Description": "Define, implement and evaluate processes, procedures and technical measures to ensure the security and retention of audit logs.", + "Name": "Audit Logs Protection", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.1", + "8.9", + "8.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "3.1.3", + "5.1.2", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.18.1.3", + "27002: 18.1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.28", + "27001: A.5.33", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-4", + "AU-11" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.IP-4", + "PR.IP-6", + "PR.PT-1", + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.DS-01", + "PR.DS-02", + "ID.AM-08", + "PR.DS-11", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5", + "10.7" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4", + "10.5.1" + ] + } + ] + } + ], + "Checks": [ + "audit_log_retention_period_365_days" + ] + }, + { + "Id": "LOG-03", + "Description": "Identify and monitor security-related events within applications and the underlying infrastructure. Define and implement a system to generate alerts to responsible stakeholders based on such events and corresponding metrics.", + "Name": "Security Monitoring and Alerting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.8", + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-03", + "SEF-05" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.5" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4", + "5.2.7", + "1.6.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2", + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.28", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-5", + "AU-5(2)", + "AU-13" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.AE-2", + "DE.AE-3", + "DE.AE-5", + "DE.CM-1", + "DE.CM-2", + "DE.CM-3", + "DE.CM-4", + "DE.CM-5", + "DE.CM-6", + "DE.CM-7", + "DE.DP-1", + "DE.DP-4", + "DE.AE-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.AE-02", + "DE.AE-03", + "DE.AE-04", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1", + "10.2.2", + "10.4.1.1", + "10.4.2.1", + "10.4.3" + ] + } + ] + } + ], + "Checks": [ + "cloudguard_enabled", + "events_rule_cloudguard_problems", + "events_notification_topic_and_subscription_exists", + "events_rule_local_user_authentication" + ] + }, + { + "Id": "LOG-04", + "Description": "Restrict audit logs access to authorized personnel and maintain records that provide unique access accountability.", + "Name": "Audit Logs Access and Accountability", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "IVS-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.14" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "3.1.1", + "4.1.2", + "4.1.3", + "4.2.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.2", + "27001: A.12.4.1", + "27002: 12.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.33", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(4)", + "AU-9(6)", + "AU-10" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-1", + "PR.AC-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.1", + "10.2.1", + "10.2.3", + "10.5.1", + "10.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1.3", + "10.3.1" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "LOG-05", + "Description": "Monitor security audit logs to detect activity outside of typical or expected patterns. Establish and follow a defined process to review and take appropriate and timely actions on detected anomalies.", + "Name": "Audit Logs Monitoring and Response", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.8", + "8.11" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.1", + "1.6.2", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.3", + "27002: 12.4.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15", + "27001: A.8.16" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-6", + "AU-6(1)", + "AU-6(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-3", + "PR.PT-1", + "RS.AN-1", + "RS.CO-1.", + "DE.AE-1", + "DE.AE-5", + "DE.DP-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.AM-03", + "PR.PS-04", + "DE.AE-02", + "DE.AE-03", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.6", + "10.6.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.4.1.1", + "10.4.2.1" + ] + } + ] + } + ], + "Checks": [ + "events_rule_iam_group_changes", + "events_rule_iam_policy_changes", + "events_rule_identity_provider_changes", + "events_rule_idp_group_mapping_changes", + "events_rule_local_user_authentication", + "events_rule_network_gateway_changes", + "events_rule_network_security_group_changes", + "events_rule_route_table_changes", + "events_rule_security_list_changes", + "events_rule_user_changes", + "events_rule_vcn_changes", + "events_rule_cloudguard_problems" + ] + }, + { + "Id": "LOG-07", + "Description": "Establish, document and implement which information meta/data system events should be logged. Review and update the scope at least annually or whenever there is a change in the threat environment.", + "Name": "Logging Scope", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 7.5.3", + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 7.5.3", + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-1", + "AU-14", + "AU-16" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.SC-3", + "ID.SC-4", + "PR.PT-1", + "ID.GV-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.1", + "10.2.2" + ] + } + ] + } + ], + "Checks": [ + "audit_log_retention_period_365_days", + "network_vcn_subnet_flow_logs_enabled", + "objectstorage_bucket_logging_enabled" + ] + }, + { + "Id": "LOG-08", + "Description": "Generate audit records containing relevant security information.", + "Name": "Log Records", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "8.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.1", + "27002: 12.4.1", + "27017: 12.4.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-3", + "AU-3(1)", + "AU-3(3)", + "AU-6", + "AU-6(8)", + "AU-12", + "AU-12(1)", + "AU-12(2)", + "AU-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.PT-1", + "DE.AE-3", + "DE.CM-1", + "DE.CM-2", + "DE.CM-3", + "DE.CM-6", + "DE.CM-7" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-01", + "DE.CM-02", + "DE.CM-03", + "DE.CM-06", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.3" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.2.2" + ] + } + ] + } + ], + "Checks": [ + "audit_log_retention_period_365_days", + "network_vcn_subnet_flow_logs_enabled", + "objectstorage_bucket_logging_enabled" + ] + }, + { + "Id": "LOG-09", + "Description": "The information system protects audit records from unauthorized access, modification, and deletion.", + "Name": "Log Protection", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "GRM-04", + "IVS-01" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.4", + "4.2.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.4.2", + "27002: 12.4.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.15" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(2)", + "AU-9(3)", + "AU-9(4)", + "AU-12(3)", + "AU-12(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.AC-4", + "PR.IP-4", + "PR.IP-6", + "PR.PT-1", + "PR.DS-1", + "PR.DS-6" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AA-05", + "PR.DS-01", + "PR.DS-02", + "PR.DS-11" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.5", + "10.5.1", + "10.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.3.1", + "10.3.2", + "10.3.3", + "10.3.4" + ] + } + ] + } + ], + "Checks": [ + "audit_log_retention_period_365_days" + ] + }, + { + "Id": "LOG-10", + "Description": "Establish and maintain a monitoring and internal reporting capability over the operations of cryptographic, encryption and key management policies, processes, procedures, and controls.", + "Name": "Encryption Monitoring and Reporting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-02", + "EKM-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "4.2.1", + "5.1.1", + "5.1.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1", + "27002: 10.1", + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-1", + "AU-9", + "AU-9(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "PR.PT-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.1.1", + "10.2.1", + "10.4.1" + ] + } + ] + } + ], + "Checks": [ + "kms_key_rotation_enabled" + ] + }, + { + "Id": "LOG-11", + "Description": "Log and monitor key lifecycle management events to enable auditing and reporting on usage of cryptographic keys.", + "Name": "Transaction/Activity Logging", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "EKM-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.1" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.10.1.2", + "27017: 10.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.24" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-9", + "AU-9(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.PT-1", + "DE.AE-3" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.CM-09" + ] + } + ] + } + ], + "Checks": [ + "audit_log_retention_period_365_days" + ] + }, + { + "Id": "LOG-13", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the reporting of anomalies and failures of the monitoring system and provide immediate notification to the accountable party.", + "Name": "Failures and Anomalies Reporting", + "Attributes": [ + { + "Section": "Logging and Monitoring", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.3", + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-03" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.1", + "5.2.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.1", + "27002: 16.1.1", + "27001: A.16.1.2", + "27017: 16.1.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.24", + "27001: A.6.8", + "27002: 6.8 (g)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AU-5", + "AU-5(2)", + "AU-6", + "AU-6(3)", + "AU-6(4)", + "AU-6(5)", + "AU-16" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-3", + "DE.DP-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-04", + "DE.AE-06" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "10.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "10.4.3", + "10.7.1", + "10.7.2", + "10.7.3" + ] + } + ] + } + ], + "Checks": [ + "cloudguard_enabled", + "events_rule_cloudguard_problems", + "events_notification_topic_and_subscription_exists" + ] + }, + { + "Id": "SEF-03", + "Description": "'Establish, document, approve, communicate, apply, evaluate and maintain a security incident response plan, which includes but is not limited to: relevant internal departments, impacted CSCs, and other business critical relationships (such as supply-chain) that may be impacted.'", + "Name": "Incident Response Plans", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2", + "CC7.3", + "CC7.4" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "BCR-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "17.2", + "17.4" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27017: CLD.12.1.5", + "27018: 16.1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: A.5.26", + "27002: 5.26 (e,f)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IR-1", + "IR-2", + "IR-2(1)-(3)", + "IR-3", + "IR-3(1)-(3)", + "IR-4", + "IR-4(1)-(15)", + "IR-5", + "IR-5(1)", + "IR-6", + "IR-6(1)-(3)", + "IR-7", + "IR-7(1)", + "IR-7(2)", + "IR-8", + "IR-8(1)", + "IR-9", + "IR-9(1)-(4)", + "PM-12" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "RS.CO-1", + "RS.CO-4", + "ID.AM-6", + "ID.GV-2", + "ID.SC-5", + "PR.IP-9", + "PR.IP10" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.AT-01", + "PR.AT-02", + "RS.MA-01", + "GV.SC-08", + "ID.IM-02", + "ID.IM-04", + "RC.RP-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.1", + "12.10.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1", + "12.10.5" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "SEF-06", + "Description": "Define, implement and evaluate processes, procedures and technical measures supporting business processes to triage security-related events.", + "Name": "Event Triage Processes", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-02" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.16.1.4", + "27002: 16.1.4", + "27017: 16.1.4", + "27018: 16.1.4", + "27001: A.16.1.5", + "27002: 16.1.5", + "27017: 16.1.5", + "27018: 16.1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.25" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CA-7", + "CA-7(3)", + "CA-7(4)", + "CA-7(5)", + "CA-7(6)", + "IR-4", + "IR-4(1)", + "IR-4(3)", + "IR-4(4)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.AE-1", + "DE.AE-2", + "DE.AE-4", + "RS.RP-1", + "RS.AN-2" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "RS.MA-02", + "RS.MA-03", + "RS.AN-03", + "DE.AE-02", + "DE.AE-04", + "DE.AE-06", + "DE.AE-07", + "DE.AE-08", + "RS.MI-02", + "RC.RP-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "12.5.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1" + ] + } + ] + } + ], + "Checks": [ + "cloudguard_enabled" + ] + }, + { + "Id": "SEF-08", + "Description": "Maintain points of contact for applicable regulation authorities, national and local law enforcement, and other legal jurisdictional authorities.", + "Name": "Points of Contact Maintenance", + "Attributes": [ + { + "Section": "Security Incident Management, E-Discovery, & Cloud Forensics", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC2.3" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "SEF-01" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "17.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.6.2", + "1.6.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "SM2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 4.2", + "27001: A.6.1.3", + "27002: 6.1.3", + "27017: 6.1.3", + "27018: 6.1.3", + "27001: A.16.1.1", + "27002: 16.1.1", + "27001: A.18.1.1", + "27002: 18.1.1", + "27017: 18.1.1", + "27018: 18.1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.5", + "27001: A.5.24", + "27002: 5.24 Incident management procedure (d)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "IR-4", + "IR-4(8)", + "IR-6", + "IR-6(3)", + "IR-7", + "IR-7(2)", + "PM-21", + "PM-23", + "PM-26" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-2", + "RS.CO-3", + "RS.CO-4" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.RR-02", + "RS.CO-02", + "RS.CO-03" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.10.1" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "TVM-02", + "Description": "Establish, document, approve, communicate, apply, evaluate and maintain policies and procedures to protect against malware on managed assets. Review and update the policies and procedures at least annually.", + "Name": "Malware Protection Policy and Procedures", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC6.8" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-01", + "GRM-06", + "GRM-09" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "9.7", + "10.1" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "1.1.1", + "1.5.1", + "5.2.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS1.2", + "TS1.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5", + "27002: 5", + "27001: A.12.2.1", + "27001: A.6.2.1", + "27002: 6.2.1 (h)", + "27001: A.6.2.2", + "27002: 6.2.2 (j)", + "27001: A.7.2.2", + "27002: 7.2.2 (d)", + "27001: A.10.1.1", + "27002: 10.1.1 (g)", + "27001: A.13.2.1", + "27002: 13.2.1 (b)", + "27001: A.15.1.2", + "27017: 15.1.2", + "27001: A.12.2.1", + "27002: 12.2.1 (a),(d)", + "27017: CLD.9.5.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 5.1", + "27001: 5.2", + "27001: 7.3", + "27001: 7.4", + "27001: 7.5", + "27001: 9.1", + "27001: 9.3", + "27001: A.5.1", + "27001: A.5.4", + "27001: A.5.7", + "27001: A.5.37", + "27001: A.8.7", + "27002: 5.7 (b)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-3", + "RA-3(3)", + "RA-5", + "RA-5(3)", + "RA-5(5)", + "SI-3", + "SI-3(4)", + "SI-3(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.GV-1", + "DE.CM-4", + "DE.CM-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "GV.PO-01", + "GV.PO-02", + "ID.IM-03", + "DE.CM-01", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.4", + "12.1", + "12.1.1", + "12.3.1", + "12.5.1", + "12.11" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "12.1.1", + "12.1.2", + "5.1.1", + "5.3.2.1" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "TVM-03", + "Description": "Define, implement and evaluate processes, procedures and technical measures to enable both scheduled and emergency responses to vulnerability identifications, based on the identified risk.", + "Name": "Vulnerability Remediation Schedule", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC5.3", + "CC7.1", + "CC7.4" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "7.2", + "7.7", + "17.9" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.5" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1", + "TM2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.12.2.1", + "27001: A.12.6.1", + "27002: 12.6.1(c)(d)(j)", + "27018: 12.6.1(k)(i)" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.8.7", + "27001: A.8.8", + "27001: A.8.32", + "27002: 8.7", + "27002: 8.8", + "27002: 8.32" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "PM-31", + "RA-3", + "RA-3(1)", + "RA-5", + "RA-5(2)-(4)", + "RA-5(6)", + "SI-3", + "SI-3(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "RS.AN-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01", + "ID.RA-06", + "ID.RA-08", + "PR.PS-02", + "PR.PS-03" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "6.1.a", + "6.1.b" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.1.1", + "6.3.1", + "6.3.2", + "6.3.3", + "12.10.1" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "TVM-04", + "Description": "Define, implement and evaluate processes, procedures and technical measures to update detection tools, threat signatures, and indicators of compromise on a weekly, or more frequent basis.", + "Name": "Detection Updates", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "No mapping" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "10.2" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.3" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TS1.3", + "TS1.4", + "TM1.3", + "TM1.4", + "IM1.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.5.1.1", + "27002: 5.1.1 (h)", + "27001: A.12.6.1", + "27002: 12.6.1 (b),(c)" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.5.1", + "27001: A.8.8", + "27001: A.8.15", + "27001: A.8.16", + "27002: 5.1", + "27002: 5.37", + "27002: 8.8", + "27002: 8.15 (d)", + "27002: 8.16 (d,e)", + "27002: 8.31 2nd (a)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "CM-7", + "CM-7(4)", + "RA-3", + "RA-3(3)", + "RA-5(2)", + "SA-10", + "SA-10(5)", + "SA-11", + "SA-11(2)", + "SI-2", + "SI-2(4)", + "SI-3", + "SI-3(4)", + "SI-4", + "SI-4(9)", + "SI-4(24)", + "SI-8", + "SI-8(2)", + "SI-8(3)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.PS-02", + "ID.RA-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "5.2", + "5.2a", + "5.2b", + "5.2c" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "5.3.1" + ] + } + ] + } + ], + "Checks": [ + "cloudguard_enabled" + ] + }, + { + "Id": "TVM-05", + "Description": "Define, implement and evaluate processes, procedures and technical measures to identify updates for applications which use third party or open source libraries according to the organization's vulnerability management policy.", + "Name": "External Library Vulnerabilities", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "CSP-Owned", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC3.2" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "No mapping" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1", + "SD2.3" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: 6.1.3", + "27001: A.12.6.2", + "27002: 12.6.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: 6.1.3", + "27001: A 5.6", + "27001: A.8.19", + "27001: A.8.8", + "27001: A.8.28", + "27001: A.8.31", + "27002: 5.6 (c)", + "27001: 8.19", + "27001: 8.8", + "27001: 8.28", + "27001: 8.31" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-5", + "RA-5(3)", + "SA-11", + "SA-11(2)", + "SA-11(5)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "DE.DP-5", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01", + "ID.RA-03", + "PR.PS-02" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "6.2", + "6.3.2" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "6.3.2", + "6.3.3" + ] + } + ] + } + ], + "Checks": [] + }, + { + "Id": "TVM-07", + "Description": "Define, implement and evaluate processes, procedures and technical measures for the detection of vulnerabilities on organizationally managed assets at least monthly.", + "Name": "Vulnerability Identification", + "Attributes": [ + { + "Section": "Threat & Vulnerability Management", + "CCMLite": "Yes", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC7.1" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "TVM-02" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "7.1", + "7.5", + "7.6" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.5", + "5.2.6" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "TM1.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.6", + "27001: A.12.6.1", + "27002: 12.6.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.8", + "27002: 8.8" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "RA-5", + "RA-5(4)", + "RA-5(5)", + "SA-11", + "SA-11(5)", + "SA-15(5)", + "SC-7", + "SC-7(10)", + "SI-3(8)", + "SI-3(10)", + "SI-7", + "SI-7(9)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "ID.RA-1", + "DE.CM-8", + "PR.IP-12" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "ID.RA-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "6.1", + "11.2", + "11.2.1" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "6.3.1", + "6.3.2", + "6.3.3", + "11.3.2", + "11.3.2.1" + ] + } + ] + } + ], + "Checks": [ + "cloudguard_enabled" + ] + }, + { + "Id": "UEM-08", + "Description": "Protect information from unauthorized disclosure on managed endpoint devices with storage encryption.", + "Name": "Storage Encryption", + "Attributes": [ + { + "Section": "Universal Endpoint Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.1", + "CC6.7" + ] + }, + { + "ReferenceId": "CCM v3.0.1", + "Identifiers": [ + "MOS-11" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.6" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.1.2", + "3.1.4" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "PA1.2", + "PA1.3", + "PA1.5", + "PA2.2", + "PM1.4" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.11.2.7", + "27002: 11.2.7", + "27001: A.18.1.1", + "27017: 18.1.1", + "27001: A.12.3.1", + "27017: 12.3.1", + "27018: A.11.4", + "27018: A.11.5" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.8.1", + "27002: 8.1 (h)" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "AC-19(5)", + "SC-28", + "SC-28(1)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-1" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-01" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "3.4", + "3.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "3.5.1", + "3.6" + ] + } + ] + } + ], + "Checks": [ + "blockstorage_block_volume_encrypted_with_cmk", + "blockstorage_boot_volume_encrypted_with_cmk", + "filestorage_file_system_encrypted_with_cmk" + ] + }, + { + "Id": "UEM-11", + "Description": "Configure managed endpoints with Data Loss Prevention (DLP) technologies and rules in accordance with a risk assessment.", + "Name": "Data Loss Prevention", + "Attributes": [ + { + "Section": "Universal Endpoint Management", + "CCMLite": "No", + "IaaS": "Shared", + "PaaS": "Shared", + "SaaS": "Shared", + "ScopeApplicability": [ + { + "ReferenceId": "AICPA TSC 2017", + "Identifiers": [ + "CC6.7" + ] + }, + { + "ReferenceId": "CIS v8.0", + "Identifiers": [ + "3.13" + ] + }, + { + "ReferenceId": "ENX ISA v6.0", + "Identifiers": [ + "5.2.7" + ] + }, + { + "ReferenceId": "ISF SOGP 2022", + "Identifiers": [ + "IM1.5", + "PA2.2" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2013,27002:2013,27017:2015,27018:2019", + "Identifiers": [ + "27001: A.12.3", + "27002: 12.3", + "27001: A.8.3.1", + "27002: 8.3.1", + "27001: A.12.2", + "27002: 12.2", + "27001: A.18.1.3", + "27002: 18.1.3", + "27001: A.6.1.1", + "27017: 6.1.1", + "27018: 12.3.1", + "27018: 10.1" + ] + }, + { + "ReferenceId": "ISO/IEC 27001:2022, 27002:2022", + "Identifiers": [ + "27001: A.5.12", + "27001: A.8.3" + ] + }, + { + "ReferenceId": "NIST 800-53 rev 5", + "Identifiers": [ + "SC-7", + "SC-7(10)" + ] + }, + { + "ReferenceId": "NIST CSF v1.1", + "Identifiers": [ + "PR.DS-5" + ] + }, + { + "ReferenceId": "NIST CSF v2.0", + "Identifiers": [ + "PR.DS-02", + "PR.DS-10", + "PR.PS-01", + "ID.AM-08", + "DE.CM-09" + ] + }, + { + "ReferenceId": "PCI DSS v3.2.1", + "Identifiers": [ + "A3.2.6" + ] + }, + { + "ReferenceId": "PCI DSS v4.0", + "Identifiers": [ + "A3.2.6" + ] + } + ] + } + ], + "Checks": [] + } + ] +} diff --git a/prowler/config/config.py b/prowler/config/config.py index daa7378280..77674c7f93 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -63,8 +63,13 @@ class Provider(str, Enum): ORACLECLOUD = "oraclecloud" ALIBABACLOUD = "alibabacloud" OPENSTACK = "openstack" + IMAGE = "image" +# Providers that delegate scanning to an external tool (e.g. Trivy, promptfoo) +# and bypass standard check/service loading. +EXTERNAL_TOOL_PROVIDERS = frozenset({"iac", "llm", "image"}) + # Compliance actual_directory = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) @@ -75,7 +80,10 @@ def get_available_compliance_frameworks(provider=None): if provider: providers = [provider] for provider in providers: - with os.scandir(f"{actual_directory}/../compliance/{provider}") as files: + compliance_dir = f"{actual_directory}/../compliance/{provider}" + if not os.path.isdir(compliance_dir): + continue + with os.scandir(compliance_dir) as files: for file in files: if file.is_file() and file.name.endswith(".json"): available_compliance_frameworks.append( diff --git a/prowler/lib/check/compliance_models.py b/prowler/lib/check/compliance_models.py index 349e80902b..5dadf2c405 100644 --- a/prowler/lib/check/compliance_models.py +++ b/prowler/lib/check/compliance_models.py @@ -226,6 +226,18 @@ class C5Germany_Requirement_Attribute(BaseModel): ComplementaryCriteria: str +# CSA CCM v4 Requirement Attribute +class CSA_CCM_Requirement_Attribute(BaseModel): + """CSA Cloud Controls Matrix (CCM) v4 Requirement Attribute""" + + Section: str + CCMLite: str + IaaS: str + PaaS: str + SaaS: str + ScopeApplicability: list[dict] + + # Base Compliance Model # TODO: move this to compliance folder class Compliance_Requirement(BaseModel): @@ -244,6 +256,7 @@ class Compliance_Requirement(BaseModel): Prowler_ThreatScore_Requirement_Attribute, CCC_Requirement_Attribute, C5Germany_Requirement_Attribute, + CSA_CCM_Requirement_Attribute, # Generic_Compliance_Requirement_Attribute must be the last one since it is the fallback for generic compliance framework Generic_Compliance_Requirement_Attribute, ] diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 3ec9a10f47..648b962039 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -10,7 +10,7 @@ from typing import Any, Dict, Optional, Set from pydantic.v1 import BaseModel, Field, ValidationError, validator from pydantic.v1.error_wrappers import ErrorWrapper -from prowler.config.config import Provider +from prowler.config.config import EXTERNAL_TOOL_PROVIDERS, Provider from prowler.lib.check.compliance_models import Compliance from prowler.lib.check.utils import recover_checks_from_provider from prowler.lib.logger import logger @@ -159,11 +159,7 @@ class CheckMetadata(BaseModel): raise ValueError("ServiceName must be a non-empty string") check_id = values.get("CheckID") - if ( - check_id - and values.get("Provider") != "iac" - and values.get("Provider") != "llm" - ): + if check_id and values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS: service_from_check_id = check_id.split("_")[0] if service_name != service_from_check_id: raise ValueError( @@ -179,11 +175,7 @@ class CheckMetadata(BaseModel): if not check_id: raise ValueError("CheckID must be a non-empty string") - if ( - check_id - and values.get("Provider") != "iac" - and values.get("Provider") != "llm" - ): + if check_id and values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS: if "-" in check_id: raise ValueError( f"CheckID {check_id} contains a hyphen, which is not allowed" @@ -865,6 +857,49 @@ class CheckReportIAC(Check_Report): ) +@dataclass +class CheckReportImage(Check_Report): + """Contains the Container Image Check's finding information using Trivy.""" + + resource_name: str + resource_id: str + image_digest: str + package_name: str + installed_version: str + fixed_version: str + + def __init__( + self, + metadata: Optional[dict] = None, + finding: Optional[dict] = None, + image_name: str = "", + ) -> None: + """ + Initialize the Container Image Check's finding information from a Trivy vulnerability/secret dict. + + Args: + metadata (Dict): Check metadata. + finding (dict): A single vulnerability/secret result from Trivy's JSON output. + image_name (str): The container image name being scanned. + """ + if metadata is None: + metadata = {} + if finding is None: + finding = {} + super().__init__(metadata, finding) + + self.resource_name = image_name + self.resource_id = ( + finding.get("VulnerabilityID", "") + or finding.get("RuleID", "") + or finding.get("ID", "") + ) + self.image_digest = finding.get("PkgID", "") + self.package_name = finding.get("PkgName", "") + self.installed_version = finding.get("InstalledVersion", "") + self.fixed_version = finding.get("FixedVersion", "") + + @dataclass class CheckReportLLM(Check_Report): """Contains the LLM Check's finding information.""" diff --git a/prowler/lib/check/utils.py b/prowler/lib/check/utils.py index 6d5ff69644..45f16cde32 100644 --- a/prowler/lib/check/utils.py +++ b/prowler/lib/check/utils.py @@ -14,8 +14,8 @@ def recover_checks_from_provider( Returns a list of tuples with the following format (check_name, check_path) """ try: - # Bypass check loading for IAC provider since it uses Trivy directly - if provider == "iac" or provider == "llm": + # Bypass check loading for IAC, LLM, and Image providers since they use external tools directly + if provider in ("iac", "llm", "image"): return [] checks = [] diff --git a/prowler/lib/cli/parser.py b/prowler/lib/cli/parser.py index e21fb8e9d2..c3045dbd76 100644 --- a/prowler/lib/cli/parser.py +++ b/prowler/lib/cli/parser.py @@ -27,10 +27,10 @@ class ProwlerArgumentParser: self.parser = argparse.ArgumentParser( prog="prowler", formatter_class=RawTextHelpFormatter, - usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,dashboard,iac} ...", + usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,dashboard,iac,image} ...", epilog=""" Available Cloud Providers: - {aws,azure,gcp,kubernetes,m365,github,iac,llm,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack} + {aws,azure,gcp,kubernetes,m365,github,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack} aws AWS Provider azure Azure Provider gcp GCP Provider @@ -43,6 +43,7 @@ Available Cloud Providers: alibabacloud Alibaba Cloud Provider iac IaC Provider (Beta) llm LLM Provider (Beta) + image Container Image Provider nhn NHN Provider (Unofficial) mongodbatlas MongoDB Atlas Provider (Beta) diff --git a/prowler/lib/outputs/compliance/cis/cis_m365.py b/prowler/lib/outputs/compliance/cis/cis_m365.py index 6feabfb8a8..3c7de43542 100644 --- a/prowler/lib/outputs/compliance/cis/cis_m365.py +++ b/prowler/lib/outputs/compliance/cis/cis_m365.py @@ -77,8 +77,8 @@ class M365CIS(ComplianceOutput): compliance_row = M365CISModel( Provider=compliance.Provider.lower(), Description=compliance.Description, - TenantId=finding.account_uid, - Location=finding.region, + TenantId="", + Location="", AssessmentDate=str(timestamp), Requirements_Id=requirement.Id, Requirements_Description=requirement.Description, diff --git a/prowler/lib/outputs/compliance/compliance.py b/prowler/lib/outputs/compliance/compliance.py index 56f6084d03..bb7fbf1146 100644 --- a/prowler/lib/outputs/compliance/compliance.py +++ b/prowler/lib/outputs/compliance/compliance.py @@ -4,6 +4,7 @@ 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.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 from prowler.lib.outputs.compliance.generic.generic_table import ( get_generic_compliance_table, @@ -85,6 +86,15 @@ def display_compliance_table( output_directory, compliance_overview, ) + elif "csa_ccm_" in compliance_framework: + get_csa_table( + findings, + bulk_checks_metadata, + compliance_framework, + output_filename, + output_directory, + compliance_overview, + ) elif "c5_" in compliance_framework: get_c5_table( findings, diff --git a/prowler/lib/outputs/compliance/csa/__init__.py b/prowler/lib/outputs/compliance/csa/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/lib/outputs/compliance/csa/csa.py b/prowler/lib/outputs/compliance/csa/csa.py new file mode 100644 index 0000000000..ab8a021a70 --- /dev/null +++ b/prowler/lib/outputs/compliance/csa/csa.py @@ -0,0 +1,101 @@ +from colorama import Fore, Style +from tabulate import tabulate + +from prowler.config.config import orange_color + + +def get_csa_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 == "CSA-CCM" + and compliance.Version in compliance_framework + ): + 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/csa/csa_alibabacloud.py b/prowler/lib/outputs/compliance/csa/csa_alibabacloud.py new file mode 100644 index 0000000000..084edbaa4b --- /dev/null +++ b/prowler/lib/outputs/compliance/csa/csa_alibabacloud.py @@ -0,0 +1,96 @@ +from prowler.config.config import timestamp +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput +from prowler.lib.outputs.compliance.csa.models import AlibabaCloudCSAModel +from prowler.lib.outputs.finding import Finding + + +class AlibabaCloudCSA(ComplianceOutput): + """ + This class represents the Alibaba Cloud CSA compliance output. + + Attributes: + - _data (list): A list to store transformed data from findings. + - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. + + Methods: + - transform: Transforms findings into Alibaba Cloud CSA compliance format. + """ + + def transform( + self, + findings: list[Finding], + compliance: Compliance, + compliance_name: str, + ) -> None: + """ + Transforms a list of findings into Alibaba Cloud CSA compliance format. + + Parameters: + - findings (list): A list of findings. + - compliance (Compliance): A compliance model. + - compliance_name (str): The name of the compliance model. + + Returns: + - None + """ + for finding in findings: + # Get the compliance requirements for the finding + finding_requirements = finding.compliance.get(compliance_name, []) + for requirement in compliance.Requirements: + if requirement.Id in finding_requirements: + for attribute in requirement.Attributes: + compliance_row = AlibabaCloudCSAModel( + Provider=finding.provider, + Description=compliance.Description, + AccountId=finding.account_uid, + Region=finding.region, + AssessmentDate=str(timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Name=requirement.Name, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_CCMLite=attribute.CCMLite, + Requirements_Attributes_IaaS=attribute.IaaS, + Requirements_Attributes_PaaS=attribute.PaaS, + Requirements_Attributes_SaaS=attribute.SaaS, + Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability, + Status=finding.status, + StatusExtended=finding.status_extended, + ResourceId=finding.resource_uid, + ResourceName=finding.resource_name, + CheckId=finding.check_id, + Muted=finding.muted, + Framework=compliance.Framework, + Name=compliance.Name, + ) + self._data.append(compliance_row) + # Add manual requirements to the compliance output + for requirement in compliance.Requirements: + if not requirement.Checks: + for attribute in requirement.Attributes: + compliance_row = AlibabaCloudCSAModel( + Provider=compliance.Provider.lower(), + Description=compliance.Description, + AccountId="", + Region="", + AssessmentDate=str(timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Name=requirement.Name, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_CCMLite=attribute.CCMLite, + Requirements_Attributes_IaaS=attribute.IaaS, + Requirements_Attributes_PaaS=attribute.PaaS, + Requirements_Attributes_SaaS=attribute.SaaS, + Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability, + Status="MANUAL", + StatusExtended="Manual check", + ResourceId="manual_check", + ResourceName="Manual check", + CheckId="manual", + Muted=False, + Framework=compliance.Framework, + Name=compliance.Name, + ) + self._data.append(compliance_row) diff --git a/prowler/lib/outputs/compliance/csa/csa_aws.py b/prowler/lib/outputs/compliance/csa/csa_aws.py new file mode 100644 index 0000000000..0f371bfb3a --- /dev/null +++ b/prowler/lib/outputs/compliance/csa/csa_aws.py @@ -0,0 +1,96 @@ +from prowler.config.config import timestamp +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput +from prowler.lib.outputs.compliance.csa.models import AWSCSAModel +from prowler.lib.outputs.finding import Finding + + +class AWSCSA(ComplianceOutput): + """ + This class represents the AWS CSA compliance output. + + Attributes: + - _data (list): A list to store transformed data from findings. + - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. + + Methods: + - transform: Transforms findings into AWS CSA compliance format. + """ + + def transform( + self, + findings: list[Finding], + compliance: Compliance, + compliance_name: str, + ) -> None: + """ + Transforms a list of findings into AWS CSA compliance format. + + Parameters: + - findings (list): A list of findings. + - compliance (Compliance): A compliance model. + - compliance_name (str): The name of the compliance model. + + Returns: + - None + """ + for finding in findings: + # Get the compliance requirements for the finding + finding_requirements = finding.compliance.get(compliance_name, []) + for requirement in compliance.Requirements: + if requirement.Id in finding_requirements: + for attribute in requirement.Attributes: + compliance_row = AWSCSAModel( + Provider=finding.provider, + Description=compliance.Description, + AccountId=finding.account_uid, + Region=finding.region, + AssessmentDate=str(timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Name=requirement.Name, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_CCMLite=attribute.CCMLite, + Requirements_Attributes_IaaS=attribute.IaaS, + Requirements_Attributes_PaaS=attribute.PaaS, + Requirements_Attributes_SaaS=attribute.SaaS, + Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability, + Status=finding.status, + StatusExtended=finding.status_extended, + ResourceId=finding.resource_uid, + ResourceName=finding.resource_name, + CheckId=finding.check_id, + Muted=finding.muted, + Framework=compliance.Framework, + Name=compliance.Name, + ) + self._data.append(compliance_row) + # Add manual requirements to the compliance output + for requirement in compliance.Requirements: + if not requirement.Checks: + for attribute in requirement.Attributes: + compliance_row = AWSCSAModel( + Provider=compliance.Provider.lower(), + Description=compliance.Description, + AccountId="", + Region="", + AssessmentDate=str(timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Name=requirement.Name, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_CCMLite=attribute.CCMLite, + Requirements_Attributes_IaaS=attribute.IaaS, + Requirements_Attributes_PaaS=attribute.PaaS, + Requirements_Attributes_SaaS=attribute.SaaS, + Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability, + Status="MANUAL", + StatusExtended="Manual check", + ResourceId="manual_check", + ResourceName="Manual check", + CheckId="manual", + Muted=False, + Framework=compliance.Framework, + Name=compliance.Name, + ) + self._data.append(compliance_row) diff --git a/prowler/lib/outputs/compliance/csa/csa_azure.py b/prowler/lib/outputs/compliance/csa/csa_azure.py new file mode 100644 index 0000000000..9bb16e6f11 --- /dev/null +++ b/prowler/lib/outputs/compliance/csa/csa_azure.py @@ -0,0 +1,96 @@ +from prowler.config.config import timestamp +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput +from prowler.lib.outputs.compliance.csa.models import AzureCSAModel +from prowler.lib.outputs.finding import Finding + + +class AzureCSA(ComplianceOutput): + """ + This class represents the Azure CSA compliance output. + + Attributes: + - _data (list): A list to store transformed data from findings. + - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. + + Methods: + - transform: Transforms findings into Azure CSA compliance format. + """ + + def transform( + self, + findings: list[Finding], + compliance: Compliance, + compliance_name: str, + ) -> None: + """ + Transforms a list of findings into Azure CSA compliance format. + + Parameters: + - findings (list): A list of findings. + - compliance (Compliance): A compliance model. + - compliance_name (str): The name of the compliance model. + + Returns: + - None + """ + for finding in findings: + # Get the compliance requirements for the finding + finding_requirements = finding.compliance.get(compliance_name, []) + for requirement in compliance.Requirements: + if requirement.Id in finding_requirements: + for attribute in requirement.Attributes: + compliance_row = AzureCSAModel( + Provider=finding.provider, + Description=compliance.Description, + SubscriptionId=finding.account_uid, + Location=finding.region, + AssessmentDate=str(timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Name=requirement.Name, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_CCMLite=attribute.CCMLite, + Requirements_Attributes_IaaS=attribute.IaaS, + Requirements_Attributes_PaaS=attribute.PaaS, + Requirements_Attributes_SaaS=attribute.SaaS, + Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability, + Status=finding.status, + StatusExtended=finding.status_extended, + ResourceId=finding.resource_uid, + ResourceName=finding.resource_name, + CheckId=finding.check_id, + Muted=finding.muted, + Framework=compliance.Framework, + Name=compliance.Name, + ) + self._data.append(compliance_row) + # Add manual requirements to the compliance output + for requirement in compliance.Requirements: + if not requirement.Checks: + for attribute in requirement.Attributes: + compliance_row = AzureCSAModel( + Provider=compliance.Provider.lower(), + Description=compliance.Description, + SubscriptionId="", + Location="", + AssessmentDate=str(timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Name=requirement.Name, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_CCMLite=attribute.CCMLite, + Requirements_Attributes_IaaS=attribute.IaaS, + Requirements_Attributes_PaaS=attribute.PaaS, + Requirements_Attributes_SaaS=attribute.SaaS, + Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability, + Status="MANUAL", + StatusExtended="Manual check", + ResourceId="manual_check", + ResourceName="Manual check", + CheckId="manual", + Muted=False, + Framework=compliance.Framework, + Name=compliance.Name, + ) + self._data.append(compliance_row) diff --git a/prowler/lib/outputs/compliance/csa/csa_gcp.py b/prowler/lib/outputs/compliance/csa/csa_gcp.py new file mode 100644 index 0000000000..5c1b400525 --- /dev/null +++ b/prowler/lib/outputs/compliance/csa/csa_gcp.py @@ -0,0 +1,96 @@ +from prowler.config.config import timestamp +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput +from prowler.lib.outputs.compliance.csa.models import GCPCSAModel +from prowler.lib.outputs.finding import Finding + + +class GCPCSA(ComplianceOutput): + """ + This class represents the GCP CSA compliance output. + + Attributes: + - _data (list): A list to store transformed data from findings. + - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. + + Methods: + - transform: Transforms findings into GCP CSA compliance format. + """ + + def transform( + self, + findings: list[Finding], + compliance: Compliance, + compliance_name: str, + ) -> None: + """ + Transforms a list of findings into GCP CSA compliance format. + + Parameters: + - findings (list): A list of findings. + - compliance (Compliance): A compliance model. + - compliance_name (str): The name of the compliance model. + + Returns: + - None + """ + for finding in findings: + # Get the compliance requirements for the finding + finding_requirements = finding.compliance.get(compliance_name, []) + for requirement in compliance.Requirements: + if requirement.Id in finding_requirements: + for attribute in requirement.Attributes: + compliance_row = GCPCSAModel( + Provider=finding.provider, + Description=compliance.Description, + ProjectId=finding.account_uid, + Location=finding.region, + AssessmentDate=str(timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Name=requirement.Name, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_CCMLite=attribute.CCMLite, + Requirements_Attributes_IaaS=attribute.IaaS, + Requirements_Attributes_PaaS=attribute.PaaS, + Requirements_Attributes_SaaS=attribute.SaaS, + Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability, + Status=finding.status, + StatusExtended=finding.status_extended, + ResourceId=finding.resource_uid, + ResourceName=finding.resource_name, + CheckId=finding.check_id, + Muted=finding.muted, + Framework=compliance.Framework, + Name=compliance.Name, + ) + self._data.append(compliance_row) + # Add manual requirements to the compliance output + for requirement in compliance.Requirements: + if not requirement.Checks: + for attribute in requirement.Attributes: + compliance_row = GCPCSAModel( + Provider=compliance.Provider.lower(), + Description=compliance.Description, + ProjectId="", + Location="", + AssessmentDate=str(timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Name=requirement.Name, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_CCMLite=attribute.CCMLite, + Requirements_Attributes_IaaS=attribute.IaaS, + Requirements_Attributes_PaaS=attribute.PaaS, + Requirements_Attributes_SaaS=attribute.SaaS, + Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability, + Status="MANUAL", + StatusExtended="Manual check", + ResourceId="manual_check", + ResourceName="Manual check", + CheckId="manual", + Muted=False, + Framework=compliance.Framework, + Name=compliance.Name, + ) + self._data.append(compliance_row) diff --git a/prowler/lib/outputs/compliance/csa/csa_oraclecloud.py b/prowler/lib/outputs/compliance/csa/csa_oraclecloud.py new file mode 100644 index 0000000000..9b3d36c168 --- /dev/null +++ b/prowler/lib/outputs/compliance/csa/csa_oraclecloud.py @@ -0,0 +1,96 @@ +from prowler.config.config import timestamp +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput +from prowler.lib.outputs.compliance.csa.models import OracleCloudCSAModel +from prowler.lib.outputs.finding import Finding + + +class OracleCloudCSA(ComplianceOutput): + """ + This class represents the OracleCloud CSA compliance output. + + Attributes: + - _data (list): A list to store transformed data from findings. + - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. + + Methods: + - transform: Transforms findings into OracleCloud CSA compliance format. + """ + + def transform( + self, + findings: list[Finding], + compliance: Compliance, + compliance_name: str, + ) -> None: + """ + Transforms a list of findings into OracleCloud CSA compliance format. + + Parameters: + - findings (list): A list of findings. + - compliance (Compliance): A compliance model. + - compliance_name (str): The name of the compliance model. + + Returns: + - None + """ + for finding in findings: + # Get the compliance requirements for the finding + finding_requirements = finding.compliance.get(compliance_name, []) + for requirement in compliance.Requirements: + if requirement.Id in finding_requirements: + for attribute in requirement.Attributes: + compliance_row = OracleCloudCSAModel( + Provider=finding.provider, + Description=compliance.Description, + TenancyId=finding.account_uid, + Region=finding.region, + AssessmentDate=str(timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Name=requirement.Name, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_CCMLite=attribute.CCMLite, + Requirements_Attributes_IaaS=attribute.IaaS, + Requirements_Attributes_PaaS=attribute.PaaS, + Requirements_Attributes_SaaS=attribute.SaaS, + Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability, + Status=finding.status, + StatusExtended=finding.status_extended, + ResourceId=finding.resource_uid, + ResourceName=finding.resource_name, + CheckId=finding.check_id, + Muted=finding.muted, + Framework=compliance.Framework, + Name=compliance.Name, + ) + self._data.append(compliance_row) + # Add manual requirements to the compliance output + for requirement in compliance.Requirements: + if not requirement.Checks: + for attribute in requirement.Attributes: + compliance_row = OracleCloudCSAModel( + Provider=compliance.Provider.lower(), + Description=compliance.Description, + TenancyId="", + Region="", + AssessmentDate=str(timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Name=requirement.Name, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_CCMLite=attribute.CCMLite, + Requirements_Attributes_IaaS=attribute.IaaS, + Requirements_Attributes_PaaS=attribute.PaaS, + Requirements_Attributes_SaaS=attribute.SaaS, + Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability, + Status="MANUAL", + StatusExtended="Manual check", + ResourceId="manual_check", + ResourceName="Manual check", + CheckId="manual", + Muted=False, + Framework=compliance.Framework, + Name=compliance.Name, + ) + self._data.append(compliance_row) diff --git a/prowler/lib/outputs/compliance/csa/models.py b/prowler/lib/outputs/compliance/csa/models.py new file mode 100644 index 0000000000..78c7384fc6 --- /dev/null +++ b/prowler/lib/outputs/compliance/csa/models.py @@ -0,0 +1,146 @@ +from pydantic.v1 import BaseModel + + +class AWSCSAModel(BaseModel): + """ + AWSCSAModel generates a finding's output in CSV CSA format for AWS. + """ + + Provider: str + Description: str + AccountId: str + Region: str + AssessmentDate: str + Requirements_Id: str + Requirements_Description: str + Requirements_Name: str + Requirements_Attributes_Section: str + Requirements_Attributes_CCMLite: str + Requirements_Attributes_IaaS: str + Requirements_Attributes_PaaS: str + Requirements_Attributes_SaaS: str + Requirements_Attributes_ScopeApplicability: list[dict] + Status: str + StatusExtended: str + ResourceId: str + CheckId: str + Muted: bool + ResourceName: str + Framework: str + Name: str + + +class GCPCSAModel(BaseModel): + """ + GCPCSAModel generates a finding's output in CSV CSA format for GCP. + """ + + Provider: str + Description: str + ProjectId: str + Location: str + AssessmentDate: str + Requirements_Id: str + Requirements_Description: str + Requirements_Name: str + Requirements_Attributes_Section: str + Requirements_Attributes_CCMLite: str + Requirements_Attributes_IaaS: str + Requirements_Attributes_PaaS: str + Requirements_Attributes_SaaS: str + Requirements_Attributes_ScopeApplicability: list[dict] + Status: str + StatusExtended: str + ResourceId: str + CheckId: str + Muted: bool + ResourceName: str + Framework: str + Name: str + + +class OracleCloudCSAModel(BaseModel): + """ + OracleCloudCSAModel generates a finding's output in CSV CSA format for OracleCloud. + """ + + Provider: str + Description: str + TenancyId: str + Region: str + AssessmentDate: str + Requirements_Id: str + Requirements_Description: str + Requirements_Name: str + Requirements_Attributes_Section: str + Requirements_Attributes_CCMLite: str + Requirements_Attributes_IaaS: str + Requirements_Attributes_PaaS: str + Requirements_Attributes_SaaS: str + Requirements_Attributes_ScopeApplicability: list[dict] + Status: str + StatusExtended: str + ResourceId: str + CheckId: str + Muted: bool + ResourceName: str + Framework: str + Name: str + + +class AlibabaCloudCSAModel(BaseModel): + """ + AlibabaCloudCSAModel generates a finding's output in CSV CSA format for Alibaba Cloud. + """ + + Provider: str + Description: str + AccountId: str + Region: str + AssessmentDate: str + Requirements_Id: str + Requirements_Description: str + Requirements_Name: str + Requirements_Attributes_Section: str + Requirements_Attributes_CCMLite: str + Requirements_Attributes_IaaS: str + Requirements_Attributes_PaaS: str + Requirements_Attributes_SaaS: str + Requirements_Attributes_ScopeApplicability: list[dict] + Status: str + StatusExtended: str + ResourceId: str + CheckId: str + Muted: bool + ResourceName: str + Framework: str + Name: str + + +class AzureCSAModel(BaseModel): + """ + AzureCSAModel generates a finding's output in CSV CSA format for Azure. + """ + + Provider: str + Description: str + SubscriptionId: str + Location: str + AssessmentDate: str + Requirements_Id: str + Requirements_Description: str + Requirements_Name: str + Requirements_Attributes_Section: str + Requirements_Attributes_CCMLite: str + Requirements_Attributes_IaaS: str + Requirements_Attributes_PaaS: str + Requirements_Attributes_SaaS: str + Requirements_Attributes_ScopeApplicability: list[dict] + Status: str + StatusExtended: str + ResourceId: str + CheckId: str + Muted: bool + ResourceName: str + Framework: str + Name: str diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index 128adc8dae..82f64850f8 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -380,6 +380,23 @@ class Finding(BaseModel): output_data["resource_uid"] = check_output.resource_id output_data["region"] = check_output.region + elif provider.type == "image": + output_data["auth_method"] = provider.auth_method + output_data["account_uid"] = "image" + output_data["account_name"] = "image" + output_data["resource_name"] = getattr( + check_output, "resource_name", "" + ) + output_data["resource_uid"] = getattr(check_output, "resource_id", "") + output_data["region"] = getattr(check_output, "region", "container") + output_data["package_name"] = getattr(check_output, "package_name", "") + output_data["installed_version"] = getattr( + check_output, "installed_version", "" + ) + output_data["fixed_version"] = getattr( + check_output, "fixed_version", "" + ) + # check_output Unique ID # TODO: move this to a function # TODO: in Azure, GCP and K8s there are findings without resource_name diff --git a/prowler/lib/outputs/ocsf/ocsf.py b/prowler/lib/outputs/ocsf/ocsf.py index c8f5db9a89..b156fe552b 100644 --- a/prowler/lib/outputs/ocsf/ocsf.py +++ b/prowler/lib/outputs/ocsf/ocsf.py @@ -201,7 +201,7 @@ class OCSF(Output): for finding in self._data: try: self._file_descriptor.write( - finding.json(exclude_none=True, indent=4) + finding.model_dump_json(exclude_none=True, indent=4) ) self._file_descriptor.write(",") except Exception as error: diff --git a/prowler/lib/outputs/summary_table.py b/prowler/lib/outputs/summary_table.py index 1a403d6a75..6a1d978d7c 100644 --- a/prowler/lib/outputs/summary_table.py +++ b/prowler/lib/outputs/summary_table.py @@ -93,6 +93,9 @@ def display_summary_table( if provider.identity.project_name else provider.identity.project_id ) + elif provider.type == "image": + entity_type = "Image" + audited_entities = ", ".join(provider.images) # Check if there are findings and that they are not all MANUAL if findings and not all(finding.status == "MANUAL" for finding in findings): diff --git a/prowler/lib/powershell/powershell.py b/prowler/lib/powershell/powershell.py index 8142fa8e45..4f68e7bd8a 100644 --- a/prowler/lib/powershell/powershell.py +++ b/prowler/lib/powershell/powershell.py @@ -193,10 +193,21 @@ class PowerShellSession: result = default if error_result: - logger.error(f"PowerShell error output: {error_result}") + self._process_error(error_result) return result + def _process_error(self, error_result: str) -> None: + """ + Process error output from the PowerShell command. + + Subclasses can override this to provide custom error handling. + + Args: + error_result (str): The error output from the PowerShell command. + """ + logger.error(f"PowerShell error output: {error_result}") + def json_parse_output(self, output: str) -> dict: """ Parse command execution output to JSON format. diff --git a/prowler/providers/alibabacloud/services/oss/oss_service.py b/prowler/providers/alibabacloud/services/oss/oss_service.py index 49db1b870c..378807bc5d 100644 --- a/prowler/providers/alibabacloud/services/oss/oss_service.py +++ b/prowler/providers/alibabacloud/services/oss/oss_service.py @@ -6,9 +6,9 @@ from datetime import datetime from email.utils import formatdate from threading import Lock from typing import Optional -from xml.etree import ElementTree import requests +from defusedxml import ElementTree from pydantic.v1 import BaseModel from prowler.lib.logger import logger diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index 91ea6c6be3..db1e4c05ff 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -1285,7 +1285,12 @@ "b2bi": { "regions": { "aws": [ + "ap-south-2", + "ap-southeast-2", + "ca-central-1", + "eu-central-1", "eu-west-1", + "eu-west-3", "us-east-1", "us-east-2", "us-west-2" @@ -1479,6 +1484,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -1539,11 +1545,16 @@ "regions": { "aws": [ "ap-northeast-1", + "ap-northeast-2", "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ca-central-1", "eu-central-1", + "eu-north-1", "eu-west-1", + "eu-west-2", + "eu-west-3", "us-east-1", "us-east-2", "us-west-2" @@ -2748,7 +2759,9 @@ "us-west-2" ], "aws-cn": [], - "aws-eusc": [], + "aws-eusc": [ + "eusc-de-east-1" + ], "aws-us-gov": [ "us-gov-east-1", "us-gov-west-1" @@ -2794,7 +2807,9 @@ "aws-cn": [ "cn-north-1" ], - "aws-eusc": [], + "aws-eusc": [ + "eusc-de-east-1" + ], "aws-us-gov": [ "us-gov-east-1", "us-gov-west-1" @@ -2838,7 +2853,9 @@ "us-west-2" ], "aws-cn": [], - "aws-eusc": [], + "aws-eusc": [ + "eusc-de-east-1" + ], "aws-us-gov": [ "us-gov-east-1", "us-gov-west-1" @@ -3166,7 +3183,9 @@ "us-west-2" ], "aws-cn": [], - "aws-eusc": [], + "aws-eusc": [ + "eusc-de-east-1" + ], "aws-us-gov": [ "us-gov-east-1", "us-gov-west-1" @@ -3212,7 +3231,9 @@ "us-west-2" ], "aws-cn": [], - "aws-eusc": [], + "aws-eusc": [ + "eusc-de-east-1" + ], "aws-us-gov": [ "us-gov-east-1", "us-gov-west-1" @@ -3245,16 +3266,6 @@ "aws-us-gov": [] } }, - "cur": { - "regions": { - "aws": [], - "aws-cn": [ - "cn-northwest-1" - ], - "aws-eusc": [], - "aws-us-gov": [] - } - }, "customer-profiles": { "regions": { "aws": [ @@ -3395,6 +3406,7 @@ "datazone": { "regions": { "aws": [ + "af-south-1", "ap-east-1", "ap-northeast-1", "ap-northeast-2", @@ -3407,6 +3419,7 @@ "eu-central-1", "eu-central-2", "eu-north-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", @@ -4628,6 +4641,7 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-5", "ca-central-1", "ca-west-1", "eu-central-1", @@ -5021,6 +5035,7 @@ "ap-east-1", "ap-northeast-1", "ap-northeast-2", + "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2", @@ -5221,7 +5236,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" @@ -5269,7 +5286,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" @@ -5317,7 +5336,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" @@ -5364,7 +5385,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" @@ -5412,7 +5435,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" @@ -5600,7 +5625,10 @@ ], "aws-cn": [], "aws-eusc": [], - "aws-us-gov": [] + "aws-us-gov": [ + "us-gov-east-1", + "us-gov-west-1" + ] } }, "greengrass": { @@ -5668,6 +5696,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -7100,6 +7129,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -7787,6 +7817,7 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-4", + "ap-southeast-5", "ca-central-1", "eu-central-1", "eu-north-1", @@ -7853,6 +7884,7 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-4", + "ap-southeast-5", "ca-central-1", "eu-central-1", "eu-north-1", @@ -8162,6 +8194,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", @@ -8261,16 +8294,30 @@ "neptune-graph": { "regions": { "aws": [ + "af-south-1", + "ap-east-1", "ap-northeast-1", + "ap-northeast-2", + "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-5", "ca-central-1", + "ca-west-1", "eu-central-1", + "eu-central-2", + "eu-north-1", "eu-west-1", "eu-west-2", + "eu-west-3", + "il-central-1", + "me-central-1", + "me-south-1", + "sa-east-1", "us-east-1", "us-east-2", + "us-west-1", "us-west-2" ], "aws-cn": [], @@ -8402,18 +8449,28 @@ "networkmonitor": { "regions": { "aws": [ + "af-south-1", "ap-east-1", "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", "ca-central-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", "sa-east-1", "us-east-1", @@ -8575,6 +8632,7 @@ "regions": { "aws": [ "ap-northeast-1", + "ca-central-1", "eu-central-1", "us-east-1", "us-east-2", @@ -9146,6 +9204,7 @@ "regions": { "aws": [ "af-south-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -9155,6 +9214,7 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-6", "ca-central-1", "ca-west-1", "eu-central-1", @@ -10223,6 +10283,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -10233,6 +10294,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -11671,11 +11733,17 @@ "ap-south-2", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-6", "ca-central-1", + "ca-west-1", "eu-central-1", + "eu-north-1", "eu-south-2", "eu-west-1", "eu-west-2", + "me-central-1", + "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", diff --git a/prowler/providers/aws/services/iam/lib/privilege_escalation.py b/prowler/providers/aws/services/iam/lib/privilege_escalation.py index b2545dfce0..9fded2d5c9 100644 --- a/prowler/providers/aws/services/iam/lib/privilege_escalation.py +++ b/prowler/providers/aws/services/iam/lib/privilege_escalation.py @@ -18,16 +18,88 @@ from prowler.providers.aws.services.iam.lib.policy import get_effective_actions # - https://bishopfox.com/blog/privilege-escalation-in-aws # - https://github.com/RhinoSecurityLabs/Security-Research/blob/master/tools/aws-pentest-tools/aws_escalate.py # - https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/ +# - https://github.com/DataDog/pathfinding.cloud (AWS IAM Privilege Escalation Path Library) privilege_escalation_policies_combination = { + # IAM self-escalation and policy manipulation "OverPermissiveIAM": {"iam:*"}, "IAMPut": {"iam:Put*"}, "CreatePolicyVersion": {"iam:CreatePolicyVersion"}, "SetDefaultPolicyVersion": {"iam:SetDefaultPolicyVersion"}, + "iam:CreateAccessKey": {"iam:CreateAccessKey"}, + "iam:CreateLoginProfile": {"iam:CreateLoginProfile"}, + "iam:UpdateLoginProfile": {"iam:UpdateLoginProfile"}, + "iam:AttachUserPolicy": {"iam:AttachUserPolicy"}, + "iam:AttachGroupPolicy": {"iam:AttachGroupPolicy"}, + "iam:AttachRolePolicy": {"iam:AttachRolePolicy"}, + "iam:PutGroupPolicy": {"iam:PutGroupPolicy"}, + "iam:PutRolePolicy": {"iam:PutRolePolicy"}, + "iam:PutUserPolicy": {"iam:PutUserPolicy"}, + "iam:AddUserToGroup": {"iam:AddUserToGroup"}, + "iam:UpdateAssumeRolePolicy": {"iam:UpdateAssumeRolePolicy"}, + # IAM chained privilege escalation patterns + "CreateAccessKey+DeleteAccessKey": { + "iam:CreateAccessKey", + "iam:DeleteAccessKey", + }, + "AttachUserPolicy+CreateAccessKey": { + "iam:AttachUserPolicy", + "iam:CreateAccessKey", + }, + "PutUserPolicy+CreateAccessKey": { + "iam:PutUserPolicy", + "iam:CreateAccessKey", + }, + "AttachRolePolicy+UpdateAssumeRolePolicy": { + "iam:AttachRolePolicy", + "iam:UpdateAssumeRolePolicy", + }, + "CreatePolicyVersion+UpdateAssumeRolePolicy": { + "iam:CreatePolicyVersion", + "iam:UpdateAssumeRolePolicy", + }, + "PutRolePolicy+UpdateAssumeRolePolicy": { + "iam:PutRolePolicy", + "iam:UpdateAssumeRolePolicy", + }, + # STS-based privilege escalation patterns + "AssumeRole+AttachRolePolicy": {"sts:AssumeRole", "iam:AttachRolePolicy"}, + "AssumeRole+PutRolePolicy": {"sts:AssumeRole", "iam:PutRolePolicy"}, + "AssumeRole+UpdateAssumeRolePolicy": { + "sts:AssumeRole", + "iam:UpdateAssumeRolePolicy", + }, + "AssumeRole+CreatePolicyVersion": { + "sts:AssumeRole", + "iam:CreatePolicyVersion", + }, + # EC2-based privilege escalation patterns "PassRole+EC2": { "iam:PassRole", "ec2:RunInstances", }, + "PassRole+EC2SpotInstances": { + "iam:PassRole", + "ec2:RequestSpotInstances", + }, + # Prerequisite: Existing EC2 instance with admin role attached + "EC2ModifyInstanceAttribute": { + "ec2:ModifyInstanceAttribute", + "ec2:StopInstances", + "ec2:StartInstances", + }, + # Prerequisite: Existing launch template used by instances with admin role + "EC2ModifyLaunchTemplate": { + "ec2:CreateLaunchTemplateVersion", + "ec2:ModifyLaunchTemplate", + }, + # EC2 Instance Connect privilege escalation + # Prerequisite: Running EC2 with Instance Connect enabled and admin role + "EC2InstanceConnect+SendSSHPublicKey": { + "ec2-instance-connect:SendSSHPublicKey", + "ec2:DescribeInstances", + }, + # Lambda-based privilege escalation patterns "PassRole+CreateLambda+Invoke": { "iam:PassRole", "lambda:CreateFunction", @@ -45,68 +117,131 @@ privilege_escalation_policies_combination = { "dynamodb:CreateTable", "dynamodb:PutItem", }, - "PassRole+GlueEndpoint": { + "PassRole+CreateLambda+AddPermission": { + "iam:PassRole", + "lambda:CreateFunction", + "lambda:AddPermission", + }, + # Prerequisite: Existing Lambda function with admin execution role + "lambda:UpdateFunctionCode": {"lambda:UpdateFunctionCode"}, + # Prerequisite: Existing Lambda function with admin execution role + "lambda:UpdateFunctionConfiguration": {"lambda:UpdateFunctionConfiguration"}, + # Prerequisite: Existing Lambda function with admin execution role + "UpdateFunctionCode+InvokeFunction": { + "lambda:UpdateFunctionCode", + "lambda:InvokeFunction", + }, + # Prerequisite: Existing Lambda function with admin execution role + "UpdateFunctionCode+AddPermission": { + "lambda:UpdateFunctionCode", + "lambda:AddPermission", + }, + # Glue-based privilege escalation patterns + "PassRole+GlueCreateDevEndpoint": { "iam:PassRole", "glue:CreateDevEndpoint", - "glue:GetDevEndpoint", }, - "PassRole+GlueEndpoints": { + # Prerequisite: Existing Glue dev endpoint with admin role + "GlueUpdateDevEndpoint": {"glue:UpdateDevEndpoint"}, + "PassRole+GlueCreateJob+StartJobRun": { "iam:PassRole", - "glue:CreateDevEndpoint", - "glue:GetDevEndpoints", + "glue:CreateJob", + "glue:StartJobRun", }, - "PassRole+CloudFormation": { + "PassRole+GlueCreateJob+CreateTrigger": { + "iam:PassRole", + "glue:CreateJob", + "glue:CreateTrigger", + }, + # Prerequisite: Existing Glue job + "PassRole+GlueUpdateJob+StartJobRun": { + "iam:PassRole", + "glue:UpdateJob", + "glue:StartJobRun", + }, + # Prerequisite: Existing Glue job + "PassRole+GlueUpdateJob+CreateTrigger": { + "iam:PassRole", + "glue:UpdateJob", + "glue:CreateTrigger", + }, + # CloudFormation-based privilege escalation patterns + "PassRole+CloudFormationCreateStack": { "iam:PassRole", "cloudformation:CreateStack", - "cloudformation:DescribeStacks", }, + # Prerequisite: Existing CloudFormation stack with admin service role + "CloudFormationUpdateStack": {"cloudformation:UpdateStack"}, + "PassRole+CloudFormationCreateStackSet": { + "iam:PassRole", + "cloudformation:CreateStackSet", + "cloudformation:CreateStackInstances", + }, + # Prerequisite: Existing CloudFormation StackSet + "PassRole+CloudFormationUpdateStackSet": { + "iam:PassRole", + "cloudformation:UpdateStackSet", + }, + # Prerequisite: Existing CloudFormation stack with admin service role + "CloudFormationChangeSet": { + "cloudformation:CreateChangeSet", + "cloudformation:ExecuteChangeSet", + }, + # DataPipeline-based privilege escalation patterns "PassRole+DataPipeline": { "iam:PassRole", "datapipeline:CreatePipeline", "datapipeline:PutPipelineDefinition", "datapipeline:ActivatePipeline", }, - "GlueUpdateDevEndpoint": {"glue:UpdateDevEndpoint"}, - "lambda:UpdateFunctionCode": {"lambda:UpdateFunctionCode"}, - "lambda:UpdateFunctionConfiguration": {"lambda:UpdateFunctionConfiguration"}, + # CodeStar-based privilege escalation patterns "PassRole+CodeStar": { "iam:PassRole", "codestar:CreateProject", }, + # CodeBuild-based privilege escalation patterns + "PassRole+CodeBuildCreateProject+StartBuild": { + "iam:PassRole", + "codebuild:CreateProject", + "codebuild:StartBuild", + }, + "PassRole+CodeBuildCreateProject+StartBuildBatch": { + "iam:PassRole", + "codebuild:CreateProject", + "codebuild:StartBuildBatch", + }, + # Prerequisite: Existing CodeBuild project with admin service role + "CodeBuildStartBuild": {"codebuild:StartBuild"}, + # Prerequisite: Existing CodeBuild project with admin service role + "CodeBuildStartBuildBatch": {"codebuild:StartBuildBatch"}, + # AutoScaling-based privilege escalation patterns "PassRole+CreateAutoScaling": { "iam:PassRole", "autoscaling:CreateAutoScalingGroup", "autoscaling:CreateLaunchConfiguration", }, + # Prerequisite: Existing Auto Scaling group "PassRole+UpdateAutoScaling": { "iam:PassRole", "autoscaling:UpdateAutoScalingGroup", "autoscaling:CreateLaunchConfiguration", }, - "iam:CreateAccessKey": {"iam:CreateAccessKey"}, - "iam:CreateLoginProfile": {"iam:CreateLoginProfile"}, - "iam:UpdateLoginProfile": {"iam:UpdateLoginProfile"}, - "iam:AttachUserPolicy": {"iam:AttachUserPolicy"}, - "iam:AttachGroupPolicy": {"iam:AttachGroupPolicy"}, - "iam:AttachRolePolicy": {"iam:AttachRolePolicy"}, - "AssumeRole+AttachRolePolicy": {"sts:AssumeRole", "iam:AttachRolePolicy"}, - "iam:PutGroupPolicy": {"iam:PutGroupPolicy"}, - "iam:PutRolePolicy": {"iam:PutRolePolicy"}, - "AssumeRole+PutRolePolicy": {"sts:AssumeRole", "iam:PutRolePolicy"}, - "iam:PutUserPolicy": {"iam:PutUserPolicy"}, - "iam:AddUserToGroup": {"iam:AddUserToGroup"}, - "iam:UpdateAssumeRolePolicy": {"iam:UpdateAssumeRolePolicy"}, - "AssumeRole+UpdateAssumeRolePolicy": { - "sts:AssumeRole", - "iam:UpdateAssumeRolePolicy", - }, - # AgentCore privilege escalation patterns - "PassRole+AgentCoreCreateInterpreter+InvokeInterpreter": { - "iam:PassRole", - "bedrock-agentcore:CreateCodeInterpreter", - "bedrock-agentcore:InvokeCodeInterpreter", - }, # ECS-based privilege escalation patterns + "PassRole+ECS+RegisterTaskDef+CreateService": { + "iam:PassRole", + "ecs:RegisterTaskDefinition", + "ecs:CreateService", + }, + "PassRole+ECS+RegisterTaskDef+RunTask": { + "iam:PassRole", + "ecs:RegisterTaskDefinition", + "ecs:RunTask", + }, + "PassRole+ECS+RegisterTaskDef+StartTask": { + "iam:PassRole", + "ecs:RegisterTaskDefinition", + "ecs:StartTask", + }, # Reference: https://labs.reversec.com/posts/2025/08/another-ecs-privilege-escalation-path "PassRole+ECS+StartTask": { "iam:PassRole", @@ -114,10 +249,63 @@ privilege_escalation_policies_combination = { "ecs:RegisterContainerInstance", "ecs:DeregisterContainerInstance", }, + # Prerequisite: Existing ECS cluster and task definition with admin role "PassRole+ECS+RunTask": { "iam:PassRole", "ecs:RunTask", }, + # Prerequisite: Running ECS task with ECS Exec enabled and admin task role + "ECS+ExecuteCommand": { + "ecs:ExecuteCommand", + "ecs:DescribeTasks", + }, + # SageMaker-based privilege escalation patterns + "PassRole+SageMakerCreateNotebookInstance": { + "iam:PassRole", + "sagemaker:CreateNotebookInstance", + }, + "PassRole+SageMakerCreateTrainingJob": { + "iam:PassRole", + "sagemaker:CreateTrainingJob", + }, + "PassRole+SageMakerCreateProcessingJob": { + "iam:PassRole", + "sagemaker:CreateProcessingJob", + }, + # Prerequisite: Existing SageMaker notebook instance with admin role + "SageMakerCreatePresignedNotebookInstanceUrl": { + "sagemaker:CreatePresignedNotebookInstanceUrl", + }, + # Prerequisite: Existing SageMaker notebook instance with admin role + "SageMakerNotebookLifecycleConfig": { + "sagemaker:CreateNotebookInstanceLifecycleConfig", + "sagemaker:StopNotebookInstance", + "sagemaker:UpdateNotebookInstance", + "sagemaker:StartNotebookInstance", + }, + # SSM-based privilege escalation patterns + # Prerequisite: Running EC2 with SSM agent and admin instance profile + "SSMStartSession": {"ssm:StartSession"}, + # Prerequisite: Running EC2 with SSM agent and admin instance profile + "SSMSendCommand": {"ssm:SendCommand"}, + # AppRunner-based privilege escalation patterns + "PassRole+AppRunnerCreateService": { + "iam:PassRole", + "apprunner:CreateService", + }, + # Prerequisite: Existing App Runner service with admin role + "AppRunnerUpdateService": {"apprunner:UpdateService"}, + # Bedrock AgentCore privilege escalation patterns + "PassRole+AgentCoreCreateInterpreter+InvokeInterpreter": { + "iam:PassRole", + "bedrock-agentcore:CreateCodeInterpreter", + "bedrock-agentcore:InvokeCodeInterpreter", + }, + # Prerequisite: Existing Bedrock code interpreter with admin role + "AgentCoreSessionInvoke": { + "bedrock-agentcore:StartCodeInterpreterSession", + "bedrock-agentcore:InvokeCodeInterpreter", + }, # TO-DO: We have to handle AssumeRole just if the resource is * and without conditions # "sts:AssumeRole": {"sts:AssumeRole"}, } diff --git a/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json b/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json index 4d801f8da9..157e93faca 100644 --- a/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json +++ b/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json @@ -24,9 +24,9 @@ ], "Remediation": { "Code": { - "CLI": "aws kms put-key-policy --key-id --policy-name default --policy '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam:::root\"},\"Action\":\"kms:*\",\"Resource\":\"*\"}]}'", + "CLI": "aws kms put-key-policy --key-id --policy-name default --policy '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam:::root\"},\"Action\":\"kms:\\*\",\"Resource\":\"\\*\"}]}'", "NativeIaC": "```yaml\n# CloudFormation: restrict KMS key policy to account root (removes any public access)\nResources:\n :\n Type: AWS::KMS::Key\n Properties:\n KeyPolicy:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Principal:\n AWS: arn:aws:iam:::root # Critical: only account root can access; prevents public \"*\" principals\n Action: kms:*\n Resource: '*'\n```", - "Other": "1. Open AWS Console > Key Management Service (KMS)\n2. Select the affected key and go to the Key policy tab\n3. Click Edit and remove any statement with Principal set to \"*\" (or AWS: \"*\")\n4. Ensure a statement exists that allows only arn:aws:iam:::root\n5. Save changes", + "Other": "1. Open AWS Console > Key Management Service (KMS)\n2. Select the affected key and go to the Key policy tab\n3. Click Edit and remove any statement with Principal set to \"\\*\" (or AWS: \"\\*\")\n4. Ensure a statement exists that allows only arn:aws:iam:::root\n5. Save changes", "Terraform": "```hcl\n# Restrict KMS key policy to the account root to avoid any public (\"*\") principals\ndata \"aws_caller_identity\" \"current\" {}\n\nresource \"aws_kms_key\" \"\" {\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [\n {\n Effect = \"Allow\"\n Principal = { AWS = \"arn:aws:iam::${data.aws_caller_identity.current.account_id}:root\" } # Critical: limit to account root to remove public access\n Action = \"kms:*\"\n Resource = \"*\"\n }\n ]\n })\n}\n```" }, "Recommendation": { diff --git a/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.py b/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.py index 408034647a..011760ac03 100644 --- a/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.py +++ b/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.py @@ -19,7 +19,7 @@ class kms_key_not_publicly_accessible(Check): if is_policy_public( key.policy, kms_client.audited_account, - not_allowed_actions=["kms:*"], + not_allowed_actions=[], ): report.status = "FAIL" report.status_extended = ( diff --git a/prowler/providers/azure/azure_provider.py b/prowler/providers/azure/azure_provider.py index db434c0675..506b433690 100644 --- a/prowler/providers/azure/azure_provider.py +++ b/prowler/providers/azure/azure_provider.py @@ -1,4 +1,5 @@ import asyncio +import logging import os import re from argparse import ArgumentTypeError @@ -217,6 +218,9 @@ class AzureProvider(Provider): """ logger.info("Setting Azure provider ...") + # Mute HPACK library logs to prevent token leakage in debug mode + logging.getLogger("hpack").setLevel(logging.CRITICAL) + logger.info("Checking if any credentials mode is set ...") # Validate the authentication arguments diff --git a/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api.metadata.json b/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api.metadata.json index 89981231ad..c542b7997b 100644 --- a/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api.metadata.json +++ b/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "entra_conditional_access_policy_require_mfa_for_management_api", - "CheckTitle": "Ensure Multifactor Authentication is Required for Windows Azure Service Management API", + "CheckTitle": "Tenant requires MFA for all users to access Windows Azure Service Management API", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "#microsoft.graph.conditionalAccess", + "Severity": "critical", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "This recommendation ensures that users accessing the Windows Azure Service Management API (i.e. Azure Powershell, Azure CLI, Azure Resource Manager API, etc.) are required to use multifactor authentication (MFA) credentials when accessing resources through the Windows Azure Service Management API.", - "Risk": "Administrative access to the Windows Azure Service Management API should be secured with a higher level of scrutiny to authenticating mechanisms. Enabling multifactor authentication is recommended to reduce the potential for abuse of Administrative actions, and to prevent intruders or compromised admin credentials from changing administrative settings.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management", + "Description": "**Microsoft Entra Conditional Access** requires **MFA** for the **Windows Azure Service Management API** when an `enabled` policy targets `All users` and grants `Require multifactor authentication` to tokens for this management endpoint.", + "Risk": "Without MFA on Azure management endpoints, stolen or phished passwords can enable control-plane access.\n\nAttackers can change configs, create/delete resources, extract secrets, pivot laterally, and disrupt services-compromising confidentiality, integrity, and availability, with added cost exposure.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-sg/entra/identity/conditional-access/policy-old-require-mfa-azure-mgmt", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps", + "https://support.icompaas.com/support/solutions/articles/62000233942-ensure-that-multi-factor-authentication-is-required-for-windows-azure-service-management-api-manual-" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "New-MgIdentityConditionalAccessPolicy -DisplayName \"Require MFA for Azure management\" -State \"enabled\" -Conditions @{Users=@{IncludeUsers=@(\"All\")}; Applications=@{IncludeApplications=@(\"797f4846-ba00-4fd7-ba43-dac1f8f63013\")}} -GrantControls @{BuiltInControls=@(\"mfa\")}", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. In the Microsoft Entra admin center, go to Protection > Conditional Access > Policies\n2. Click New policy\n3. Users: Include > All users\n4. Target resources: Resources > Include > Select resources > choose \"Windows Azure Service Management API\"\n5. Grant: Grant access > check Require multifactor authentication > Select\n6. Enable policy: On > Create", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"Require MFA for Azure management\"\n state = \"enabled\" # Critical: policy must be enabled to pass\n\n conditions {\n client_app_types = [\"all\"]\n applications {\n included_applications = [\n \"797f4846-ba00-4fd7-ba43-dac1f8f63013\" # Critical: Windows Azure Service Management API (Azure management)\n ]\n }\n users {\n included_users = [\"All\"] # Critical: apply to all users\n }\n }\n\n grant_controls {\n operator = \"OR\"\n built_in_controls = [\"mfa\"] # Critical: require multifactor authentication\n }\n}\n```" }, "Recommendation": { - "Text": "1. From the Azure Admin Portal dashboard, open Microsoft Entra ID. 2. Click Security in the Entra ID blade. 3. Click Conditional Access in the Security blade. 4. Click Policies in the Conditional Access blade. 5. Click + New policy. 6. Enter a name for the policy. 7. Click the blue text under Users. 8. Under Include, select All users. 9. Under Exclude, check Users and groups. 10. Select users or groups to be exempted from this policy (e.g. break-glass emergency accounts, and non-interactive service accounts) then click the Select button. 11. Click the blue text under Target Resources. 12. Under Include, click the Select apps radio button. 13. Click the blue text under Select. 14. Check the box next to Windows Azure Service Management APIs then click the Select button. 15. Click the blue text under Grant. 16. Under Grant access check the box for Require multifactor authentication then click the Select button. 17. Before creating, set Enable policy to Report-only. 18. Click Create. After testing the policy in report-only mode, update the Enable policy setting from Report-only to On.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps" + "Text": "Enforce **MFA** via Conditional Access for `Windows Azure Service Management API` scoped to `All users`, with only break-glass exclusions. Prefer **phishing-resistant** methods, apply **least privilege** and **separation of duties**, and monitor sign-ins. Also secure related admin apps and explicitly protect Azure DevOps as a distinct target.", + "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_require_mfa_for_management_api" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Conditional Access policies require Microsoft Entra ID P1 or P2 licenses. Similarly, they may require additional overhead to maintain if users lose access to their MFA. Any users or groups which are granted an exception to this policy should be carefully tracked, be granted only minimal necessary privileges, and conditional access exceptions should be regularly reviewed or investigated." diff --git a/prowler/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users.metadata.json b/prowler/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users.metadata.json index 7598b0f901..8c69462bac 100644 --- a/prowler/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users.metadata.json +++ b/prowler/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "entra_global_admin_in_less_than_five_users", - "CheckTitle": "Ensure fewer than 5 users have global administrator assignment", + "CheckTitle": "Global Administrator role has fewer than 5 members", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "#microsoft.graph.directoryRole", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "This recommendation aims to maintain a balance between security and operational efficiency by ensuring that a minimum of 2 and a maximum of 4 users are assigned the Global Administrator role in Microsoft Entra ID. Having at least two Global Administrators ensures redundancy, while limiting the number to four reduces the risk of excessive privileged access.", - "Risk": "The Global Administrator role has extensive privileges across all services in Microsoft Entra ID. The Global Administrator role should never be used in regular daily activities, administrators should have a regular user account for daily activities, and a separate account for administrative responsibilities. Limiting the number of Global Administrators helps mitigate the risk of unauthorized access, reduces the potential impact of human error, and aligns with the principle of least privilege to reduce the attack surface of an Azure tenant. Conversely, having at least two Global Administrators ensures that administrative functions can be performed without interruption in case of unavailability of a single admin.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#5-limit-the-number-of-global-administrators-to-less-than-5", + "Description": "**Microsoft Entra Global Administrator** assignments are evaluated by counting current role members per tenant and identifying when the number of assignees is `5` or more.", + "Risk": "Having **5+ Global Administrators** expands the privileged attack surface. Compromised credentials or tokens can enable tenant-wide changes, disable security controls, exfiltrate data, and create persistence, impacting **confidentiality**, **integrity**, and **availability** across Entra, Microsoft 365, and Azure.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#5-limit-the-number-of-global-administrators-to-less-than-5", + "https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/about-admin-roles?view=o365-worldwide#security-guidelines-for-assigning-roles" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "Remove-MgDirectoryRoleMember -DirectoryRoleId (Get-MgDirectoryRole -Filter \"displayName eq 'Global Administrator'\").Id -DirectoryObjectId ''", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Identity > Roles & admins > Global Administrator\n3. Select View assignments (or Assignments)\n4. Remove members until the total Global Administrator assignments are fewer than 5\n5. Save changes", + "Terraform": "```hcl\n# Keep Global Administrator assignments below 5 by defining only required principals\ndata \"azuread_directory_role\" \"global_admin\" {\n display_name = \"Global Administrator\"\n}\n\n# Critical: This assignment grants GA to a specific principal; keep total GA assignments < 5\nresource \"azuread_directory_role_assignment\" \"ga_primary\" {\n role_id = data.azuread_directory_role.global_admin.id # Assigns the Global Administrator role\n principal_object_id = \"\" # Required account (e.g., break-glass)\n}\n\n# Critical: Add only necessary GA assignments; remove extras to ensure count < 5\nresource \"azuread_directory_role_assignment\" \"ga_secondary\" {\n role_id = data.azuread_directory_role.global_admin.id # Assigns the Global Administrator role\n principal_object_id = \"\" # Second required account\n}\n```" }, "Recommendation": { - "Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Select Roles and Administrators 4. Select Global Administrator 5. Ensure less than 5 users are actively assigned the role. 6. Ensure that at least 2 users are actively assigned the role.", - "Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/about-admin-roles?view=o365-worldwide#security-guidelines-for-assigning-roles" + "Text": "Limit the **Global Administrator** role to **fewer than 5** users.\n- Apply **least privilege**; use narrower roles where possible\n- Use **PIM** for just-in-time, no standing access\n- Enforce **MFA** and dedicated admin accounts\n- Run **access reviews** regularly and keep cloud-only `break-glass` accounts for emergencies", + "Url": "https://hub.prowler.com/check/entra_global_admin_in_less_than_five_users" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Implementing this recommendation may require changes in administrative workflows or the redistribution of roles and responsibilities. Adequate training and awareness should be provided to all Global Administrators." diff --git a/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.metadata.json b/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.metadata.json index fd646bc397..e6bd208eaf 100644 --- a/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.metadata.json +++ b/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "entra_non_privileged_user_has_mfa", - "CheckTitle": "Ensure that 'Multi-Factor Auth Status' is 'Enabled' for all Non-Privileged Users", + "CheckTitle": "Non-privileged user has multi-factor authentication enabled", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "#microsoft.graph.users", + "Severity": "medium", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "Enable multi-factor authentication for all non-privileged users.", - "Risk": "Multi-factor authentication requires an individual to present a minimum of two separate forms of authentication before access is granted. Multi-factor authentication provides additional assurance that the individual attempting to gain access is who they claim to be. With multi-factor authentication, an attacker would need to compromise at least two different authentication mechanisms, increasing the difficulty of compromise and thus reducing the risk.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks", + "Description": "**Microsoft Entra** non-privileged users are assessed for **multifactor authentication** by verifying they have **two or more registered authentication methods** (*MFA enrollment*).", + "Risk": "Absent **MFA** on standard accounts enables password-only logins after phishing, reuse, or spraying, leading to **account takeover**. Attackers can access email, files, and apps, send internal phishing, and escalate, undermining **confidentiality** and **integrity**, and risking **availability** via malicious changes.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa", + "https://support.icompaas.com/support/solutions/articles/62000219680-ensure-that-multi-factor-auth-status-is-enabled-for-all-non-privileged-users", + "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "az rest --method POST --url https://graph.microsoft.com/v1.0/users//authentication/temporaryAccessPassMethods --body '{\"lifetimeInMinutes\":60,\"isUsableOnce\":true}'", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/multi-factor-authentication-for-all-non-privileged-users.html#", + "Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Entra ID > Users and select the non-privileged user\n3. Select Security > Authentication methods\n4. Click Add authentication method > Temporary Access Pass\n5. Click Create (accept defaults)\n6. Confirm the method appears under the user's authentication methods", "Terraform": "" }, "Recommendation": { - "Text": "Activate one of the available multi-factor authentication methods for users in Microsoft Entra ID.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa" + "Text": "Enforce **MFA** for all users, including non-privileged. Prefer **phishing-resistant** methods (FIDO2/passkeys or Authenticator with number matching); avoid SMS/voice when possible. Use **Conditional Access** to require MFA by risk and context. Pair with **least privilege**, device trust, and sign-in monitoring.", + "Url": "https://hub.prowler.com/check/entra_non_privileged_user_has_mfa" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Users would require two forms of authentication before any access is granted. Also, this requires an overhead for managing dual forms of authentication." diff --git a/prowler/providers/azure/services/entra/entra_policy_default_users_cannot_create_security_groups/entra_policy_default_users_cannot_create_security_groups.metadata.json b/prowler/providers/azure/services/entra/entra_policy_default_users_cannot_create_security_groups/entra_policy_default_users_cannot_create_security_groups.metadata.json index 26447cd5d3..a491370fab 100644 --- a/prowler/providers/azure/services/entra/entra_policy_default_users_cannot_create_security_groups/entra_policy_default_users_cannot_create_security_groups.metadata.json +++ b/prowler/providers/azure/services/entra/entra_policy_default_users_cannot_create_security_groups/entra_policy_default_users_cannot_create_security_groups.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "entra_policy_default_users_cannot_create_security_groups", - "CheckTitle": "Ensure that 'Users can create security groups in Azure portals, API or PowerShell' is set to 'No'", + "CheckTitle": "Authorization policy disallows non-privileged users from creating security groups", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "#microsoft.graph.authorizationPolicy", + "Severity": "medium", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "Restrict security group creation to administrators only.", - "Risk": "When creating security groups is enabled, all users in the directory are allowed to create new security groups and add members to those groups. Unless a business requires this day-to-day delegation, security group creation should be restricted to administrators only.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management", + "Description": "**Microsoft Entra authorization policy** setting for default user role permissions governing creation of **security groups** by non-privileged users.\n\nThe value of `allowed_to_create_security_groups` is examined to ensure group creation is limited to administrators across portals, API, and PowerShell.", + "Risk": "Allowing standard users to create security groups drives **entitlement sprawl** and can grant **unauthorized access** when those groups are tied to apps, sites, or roles. This weakens **least privilege**, complicates audits, and enables **lateral movement** or data exfiltration via misassigned group-based permissions.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/users-can-create-security-groups.html", + "https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "az rest --method PATCH --url https://graph.microsoft.com/v1.0/policies/authorizationPolicy/authorizationPolicy --body '{\"defaultUserRolePermissions\":{\"allowedToCreateSecurityGroups\":false}}'", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/users-can-create-security-groups.html", - "Terraform": "" + "Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Identity > Users > User settings\n3. Find \"Users can create security groups in Azure portals, API, or PowerShell\"\n4. Set it to \"No\"\n5. Click Save", + "Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"\" {\n default_user_role_permissions {\n allowed_to_create_security_groups = false # Critical: disables security group creation for non-privileged users\n }\n}\n```" }, "Recommendation": { - "Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Select Groups 4. Select General under Settings 5. Set Users can create security groups in Azure portals, API or PowerShell to No", - "Url": "" + "Text": "Restrict creation to **administrators** or a narrowly delegated role per **least privilege**. Set `allowed_to_create_security_groups` to `false` and use request/approval for new groups. Apply **governance**: naming standards, owner accountability, periodic **access reviews**, and monitor group lifecycle in audit logs.", + "Url": "https://hub.prowler.com/check/entra_policy_default_users_cannot_create_security_groups" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Enabling this setting could create a number of requests that would need to be managed by an administrator." diff --git a/prowler/providers/azure/services/entra/entra_policy_ensure_default_user_cannot_create_apps/entra_policy_ensure_default_user_cannot_create_apps.metadata.json b/prowler/providers/azure/services/entra/entra_policy_ensure_default_user_cannot_create_apps/entra_policy_ensure_default_user_cannot_create_apps.metadata.json index d3ee6d8bd5..1c5e2c46fb 100644 --- a/prowler/providers/azure/services/entra/entra_policy_ensure_default_user_cannot_create_apps/entra_policy_ensure_default_user_cannot_create_apps.metadata.json +++ b/prowler/providers/azure/services/entra/entra_policy_ensure_default_user_cannot_create_apps/entra_policy_ensure_default_user_cannot_create_apps.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "entra_policy_ensure_default_user_cannot_create_apps", - "CheckTitle": "Ensure That 'Users Can Register Applications' Is Set to 'No'", + "CheckTitle": "Tenant does not allow non-admin users to register applications", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "#microsoft.graph.authorizationPolicy", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "Require administrators or appropriately delegated users to register third-party applications.", - "Risk": "It is recommended to only allow an administrator to register custom-developed applications. This ensures that the application undergoes a formal security review and approval process prior to exposing Azure Active Directory data. Certain users like developers or other high-request users may also be delegated permissions to prevent them from waiting on an administrative user. Your organization should review your policies and decide your needs.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity-platform/how-applications-are-added#who-has-permission-to-add-applications-to-my-azure-ad-instance", + "Description": "**Microsoft Entra authorization policy** controls whether default users can create application registrations via `allowed_to_create_apps`. App creation is expected to be limited to administrators or explicitly delegated roles.", + "Risk": "Permitting default users to register apps enables **unvetted service principals**, **consent phishing**, and **over-privileged API access**, threatening data **confidentiality** and **integrity**. Adversaries can persist with app credentials, exfiltrate mail/files, and perform **lateral movement** using rogue permissions.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/users-can-register-applications.html", + "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/delegate-app-roles#restrict-who-can-create-applications", + "https://learn.microsoft.com/en-us/entra/identity-platform/how-applications-are-added#who-has-permission-to-add-applications-to-my-azure-ad-instance" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "az rest --method PATCH --url https://graph.microsoft.com/v1.0/policies/authorizationPolicy/authorizationPolicy --body '{\"defaultUserRolePermissions\":{\"allowedToCreateApps\":false}}'", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/users-can-register-applications.html", - "Terraform": "" + "Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Microsoft Entra ID > Users > User settings\n3. Set \"Users can register applications\" to \"No\"\n4. Click Save", + "Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"\" {\n default_user_role_permissions {\n allowed_to_create_apps = false # Critical: disables application registration for non-privileged users\n }\n}\n```" }, "Recommendation": { - "Text": "1. From Azure Home select the Portal Menu 2. Select Azure Active Directory 3. Select Users 4. Select User settings 5. Ensure that Users can register applications is set to No", - "Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/delegate-app-roles#restrict-who-can-create-applications" + "Text": "Apply **least privilege**: restrict app registration to admins or delegated roles; set `Users can register applications` to `No`. Use the **Application Developer** role for exceptions, require **admin consent** workflows, routinely review app/service principal permissions, and audit changes for **defense in depth**.", + "Url": "https://hub.prowler.com/check/entra_policy_ensure_default_user_cannot_create_apps" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Enforcing this setting will create additional requests for approval that will need to be addressed by an administrator. If permissions are delegated, a user may approve a malevolent third party application, potentially giving it access to your data." diff --git a/prowler/providers/azure/services/entra/entra_policy_ensure_default_user_cannot_create_tenants/entra_policy_ensure_default_user_cannot_create_tenants.metadata.json b/prowler/providers/azure/services/entra/entra_policy_ensure_default_user_cannot_create_tenants/entra_policy_ensure_default_user_cannot_create_tenants.metadata.json index ef0533a1e3..139c6056e5 100644 --- a/prowler/providers/azure/services/entra/entra_policy_ensure_default_user_cannot_create_tenants/entra_policy_ensure_default_user_cannot_create_tenants.metadata.json +++ b/prowler/providers/azure/services/entra/entra_policy_ensure_default_user_cannot_create_tenants/entra_policy_ensure_default_user_cannot_create_tenants.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "entra_policy_ensure_default_user_cannot_create_tenants", - "CheckTitle": "Ensure that 'Restrict non-admin users from creating tenants' is set to 'Yes'", + "CheckTitle": "Authorization policy restricts non-admin users from creating tenants", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "#microsoft.graph.authorizationPolicy", + "Severity": "medium", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "Require administrators or appropriately delegated users to create new tenants.", - "Risk": "It is recommended to only allow an administrator to create new tenants. This prevent users from creating new Azure AD or Azure AD B2C tenants and ensures that only authorized users are able to do so.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions", + "Description": "**Microsoft Entra authorization policy** governs whether default users can create new tenants. This evaluates if tenant creation is disabled for non-admin users via `allowed_to_create_tenants=false`.", + "Risk": "Permitting default users to create tenants fuels **shadow IT** and identity sprawl. Creators become **Global Administrators** of unmanaged tenants, eroding **confidentiality** and **integrity** through unsanctioned apps and unmonitored data flows, and degrading **availability** of centralized governance.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference#tenant-creator", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/disable-user-tenant-creation.html", + "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "Update-MgPolicyAuthorizationPolicy -AuthorizationPolicyId authorizationPolicy -BodyParameter @{ defaultUserRolePermissions = @{ allowedToCreateTenants = $false } }", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. Go to Microsoft Entra admin center (https://entra.microsoft.com)\n2. Navigate: Microsoft Entra ID > Users > User settings\n3. Set \"Restrict non-admin users from creating tenants\" to Yes\n4. Click Save", + "Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"\" {\n default_user_role_permissions {\n allowed_to_create_tenants = false # Critical: disables tenant creation for non-privileged users\n }\n}\n```" }, "Recommendation": { - "Text": "1. From Azure Home select the Portal Menu 2. Select Azure Active Directory 3. Select Users 4. Select User settings 5. Set 'Restrict non-admin users from creating' tenants to 'Yes'", - "Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference#tenant-creator" + "Text": "Apply **least privilege**: set `allowed_to_create_tenants=false` so only vetted admins or the **Tenant Creator** role (managed with **PIM**) can create tenants. Enforce **separation of duties**, require approvals, and monitor audits. Review this setting regularly to prevent tenant sprawl and maintain **defense in depth**.", + "Url": "https://hub.prowler.com/check/entra_policy_ensure_default_user_cannot_create_tenants" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Enforcing this setting will ensure that only authorized users are able to create new tenants." diff --git a/prowler/providers/azure/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.metadata.json b/prowler/providers/azure/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.metadata.json index 25070687a9..5925a43184 100644 --- a/prowler/providers/azure/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.metadata.json +++ b/prowler/providers/azure/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "entra_policy_guest_invite_only_for_admin_roles", - "CheckTitle": "Ensure that 'Guest invite restrictions' is set to 'Only users assigned to specific admin roles can invite guest users'", + "CheckTitle": "Tenant authorization policy restricts guest invitations to users with specific admin roles or disables guest invitations", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "#microsoft.graph.authorizationPolicy", + "Severity": "high", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "Restrict invitations to users with specific administrative roles only.", - "Risk": "Restricting invitations to users with specific administrator roles ensures that only authorized accounts have access to cloud resources. This helps to maintain 'Need to Know' permissions and prevents inadvertent access to data. By default the setting Guest invite restrictions is set to Anyone in the organization can invite guest users including guests and non-admins. This would allow anyone within the organization to invite guests and non-admins to the tenant, posing a security risk.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/external-id/external-collaboration-settings-configure", + "Description": "**Microsoft Entra authorization policy** controls who can send **B2B guest invitations**.\n\nSecure posture is when invitations are restricted to specific admin roles (`adminsAndGuestInviters`) or completely disabled (`none`).", + "Risk": "**Open guest invitation** rights let members or guests add external users without oversight, expanding the attack surface.\n\nImpacts:\n- **Confidentiality**: data leakage via overshared resources\n- **Integrity**: privilege escalation through group/team access\n- **Availability**: difficult containment due to account sprawl", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/answers/questions/685101/how-to-allow-only-admins-to-add-guests", + "https://learn.microsoft.com/en-us/entra/external-id/external-collaboration-settings-configure" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "az rest --method PATCH --url https://graph.microsoft.com/v1.0/policies/authorizationPolicy/authorizationPolicy --headers 'Content-Type=application/json' --body '{\"allowInvitesFrom\":\"adminsAndGuestInviters\"}'", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Entra ID > External Identities > External collaboration settings\n3. Under Guest invite settings, select \"Only users assigned to specific admin roles can invite guest users\" (or select \"No one in the organization can invite guest users\")\n4. Click Save", + "Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"\" {\n allow_invites_from = \"adminsAndGuestInviters\" # Restricts guest invitations to specific admin roles, making the check PASS\n}\n```" }, "Recommendation": { - "Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Then External Identities 4. Select External collaboration settings 5. Under Guest invite settings, for Guest invite restrictions, ensure that Only users assigned to specific admin roles can invite guest users is selected", - "Url": "https://learn.microsoft.com/en-us/answers/questions/685101/how-to-allow-only-admins-to-add-guests" + "Text": "Restrict invitations to `Only users assigned to specific admin roles can invite guest users`, or disable them where not needed. Apply **least privilege** (use dedicated Guest Inviter role), enforce approvals, allowlist trusted domains, and run periodic access reviews with audit monitoring to remove stale or risky guests.", + "Url": "https://hub.prowler.com/check/entra_policy_guest_invite_only_for_admin_roles" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "With the option of Only users assigned to specific admin roles can invite guest users selected, users with specific admin roles will be in charge of sending invitations to the external users, requiring additional overhead by them to manage user accounts. This will mean coordinating with other departments as they are onboarding new users." diff --git a/prowler/providers/azure/services/entra/entra_policy_guest_users_access_restrictions/entra_policy_guest_users_access_restrictions.metadata.json b/prowler/providers/azure/services/entra/entra_policy_guest_users_access_restrictions/entra_policy_guest_users_access_restrictions.metadata.json index 46212071eb..eadeb50deb 100644 --- a/prowler/providers/azure/services/entra/entra_policy_guest_users_access_restrictions/entra_policy_guest_users_access_restrictions.metadata.json +++ b/prowler/providers/azure/services/entra/entra_policy_guest_users_access_restrictions/entra_policy_guest_users_access_restrictions.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "entra_policy_guest_users_access_restrictions", - "CheckTitle": "Ensure That 'Guest users access restrictions' is set to 'Guest user access is restricted to properties and memberships of their own directory objects'", + "CheckTitle": "Authorization policy restricts guest user access to properties and memberships of their own directory objects", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "#microsoft.graph.authorizationPolicy", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "Limit guest user permissions.", - "Risk": "Limiting guest access ensures that guest accounts do not have permission for certain directory tasks, such as enumerating users, groups or other directory resources, and cannot be assigned to administrative roles in your directory. Guest access has three levels of restriction. 1. Guest users have the same access as members (most inclusive), 2. Guest users have limited access to properties and memberships of directory objects (default value), 3. Guest user access is restricted to properties and memberships of their own directory objects (most restrictive). The recommended option is the 3rd, most restrictive: 'Guest user access is restricted to their own directory object'.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/users/users-restrict-guest-permissions", + "Description": "**Microsoft Entra authorization policy** guest settings are assessed to determine whether guest user access is limited to the properties and memberships of their own directory objects (`Restricted access`) instead of broader visibility into users and groups", + "Risk": "Excess guest visibility enables **directory reconnaissance**, exposing user and group details for **phishing**, **password spraying**, and targeted attacks. This weakens **confidentiality** and can facilitate **privilege escalation** and lateral movement through informed abuse of group memberships and access paths.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/users/users-restrict-guest-permissions", + "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions#member-and-guest-users" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "az rest --method patch --url https://graph.microsoft.com/v1.0/policies/authorizationPolicy/authorizationPolicy --body '{\"guestUserRoleId\":\"2af84b1e-32c8-42b7-82bc-daa82404023b\"}'", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. Go to Microsoft Entra admin center > External Identities > External collaboration settings\n2. Select \"Guest user access is restricted to properties and memberships of their own directory objects\"\n3. Click Save\n4. Allow up to 15 minutes for the change to take effect", + "Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"\" {\n # Critical: sets guests to 'Restricted access' so they can only access their own directory object\n guest_user_role_id = \"2af84b1e-32c8-42b7-82bc-daa82404023b\"\n}\n```" }, "Recommendation": { - "Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Then External Identities 4. Select External collaboration settings 5. Under Guest user access, change Guest user access restrictions to be Guest user access is restricted to properties and memberships of their own directory objects", - "Url": "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions#member-and-guest-users" + "Text": "Apply **least privilege** to external users:\n- Set guest access to `Restricted access` so guests can only view their own directory objects\n- Avoid assigning admin roles to guests; use **PIM** for rare exceptions\n- Constrain external collaboration and group visibility, and run periodic **access reviews** to remove stale guest access", + "Url": "https://hub.prowler.com/check/entra_policy_guest_users_access_restrictions" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "This may create additional requests for permissions to access resources that administrators will need to approve. According to https://learn.microsoft.com/en-us/azure/active-directory/enterprise- users/users-restrict-guest-permissions#services-currently-not-supported Service without current support might have compatibility issues with the new guest restriction setting." diff --git a/prowler/providers/azure/services/entra/entra_policy_restricts_user_consent_for_apps/entra_policy_restricts_user_consent_for_apps.metadata.json b/prowler/providers/azure/services/entra/entra_policy_restricts_user_consent_for_apps/entra_policy_restricts_user_consent_for_apps.metadata.json index ee1516fc51..c01f007d4f 100644 --- a/prowler/providers/azure/services/entra/entra_policy_restricts_user_consent_for_apps/entra_policy_restricts_user_consent_for_apps.metadata.json +++ b/prowler/providers/azure/services/entra/entra_policy_restricts_user_consent_for_apps/entra_policy_restricts_user_consent_for_apps.metadata.json @@ -1,30 +1,38 @@ { "Provider": "azure", "CheckID": "entra_policy_restricts_user_consent_for_apps", - "CheckTitle": "Ensure 'User consent for applications' is set to 'Do not allow user consent'", + "CheckTitle": "Entra authorization policy disallows user consent for applications", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "#microsoft.graph.authorizationPolicy", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "Require administrators to provide consent for applications before use.", - "Risk": "If Microsoft Entra ID is running as an identity provider for third-party applications, permissions and consent should be limited to administrators or pre-approved. Malicious applications may attempt to exfiltrate data or abuse privileged user accounts.", - "RelatedUrl": "https://learn.microsoft.com/en-gb/entra/identity/enterprise-apps/configure-user-consent?pivots=portal", + "Description": "Microsoft Entra authorization settings are evaluated to determine if the default user role permits **user consent to applications**. The check looks at permission grant policies to see whether end users can authorize apps to access organization data on their behalf, or if consent is restricted (e.g., `Do not allow user consent`).", + "Risk": "Permitting end-user consent enables **consent phishing** and over-privileged OAuth grants. Attackers can obtain tokens to read/send mail, access files, or act as the user, causing **data exfiltration**, persistence beyond password resets/MFA changes, and abuse of connected apps, impacting confidentiality and integrity.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/users-can-consent-to-apps-accessing-company-data-on-their-behalf.html#", + "https://learn.microsoft.com/en-gb/entra/identity/enterprise-apps/configure-user-consent?pivots=portal", + "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "Update-MgPolicyAuthorizationPolicy -BodyParameter @{ permissionGrantPolicyIdsAssignedToDefaultUserRole = @() }", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/users-can-consent-to-apps-accessing-company-data-on-their-behalf.html#", - "Terraform": "" + "Other": "1. Sign in to the Microsoft Entra admin center (entra.microsoft.com) with a Global Administrator\n2. Go to Identity > Applications > Enterprise applications\n3. Select Consent and permissions > User consent settings\n4. Choose Do not allow user consent\n5. Click Save", + "Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"\" {\n # Critical: remove all self-consent policies so users cannot consent to apps\n permission_grant_policy_ids_assigned_to_default_user_role = []\n}\n```" }, "Recommendation": { - "Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Select Enterprise Applications 4. Select Consent and permissions 5. Select User consent settings 6. Set User consent for applications to Do not allow user consent 7. Click save", - "Url": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users" + "Text": "Enforce **least privilege** by setting user consent to `Do not allow user consent`. Use the **admin consent workflow** to review requests and pre-approve only vetted apps. *If needed*, allow consent only for verified publishers with low-impact scopes. Regularly review existing grants and monitor audit/sign-in logs.", + "Url": "https://hub.prowler.com/check/entra_policy_restricts_user_consent_for_apps" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Enforcing this setting may create additional requests that administrators need to review." diff --git a/prowler/providers/azure/services/entra/entra_policy_user_consent_for_verified_apps/entra_policy_user_consent_for_verified_apps.metadata.json b/prowler/providers/azure/services/entra/entra_policy_user_consent_for_verified_apps/entra_policy_user_consent_for_verified_apps.metadata.json index d75677b8b8..65e41bcea4 100644 --- a/prowler/providers/azure/services/entra/entra_policy_user_consent_for_verified_apps/entra_policy_user_consent_for_verified_apps.metadata.json +++ b/prowler/providers/azure/services/entra/entra_policy_user_consent_for_verified_apps/entra_policy_user_consent_for_verified_apps.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "entra_policy_user_consent_for_verified_apps", - "CheckTitle": "Ensure 'User consent for applications' Is Set To 'Allow for Verified Publishers'", + "CheckTitle": "Entra tenant does not allow users to consent to non-verified applications", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "#microsoft.graph.authorizationPolicy", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "Allow users to provide consent for selected permissions when a request is coming from a verified publisher.", - "Risk": "If Microsoft Entra ID is running as an identity provider for third-party applications, permissions and consent should be limited to administrators or pre-approved. Malicious applications may attempt to exfiltrate data or abuse privileged user accounts.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?pivots=portal#configure-user-consent-to-applications", + "Description": "**Microsoft Entra** authorization policy for the default user role is assessed for assignment of the user-consent policy `microsoft-user-default-legacy`. Its presence means users can self-consent to app permissions; its absence indicates consent is restricted (e.g., only verified publishers or low-impact scopes).", + "Risk": "Broad self-consent enables **OAuth consent phishing** and rogue apps to gain tokens to tenant data (**confidentiality**), request write scopes to change resources (**integrity**), and persist via refresh tokens after password changes. Mis-scoped grants can drive lateral movement and privilege escalation.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users", + "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?pivots=portal#configure-user-consent-to-applications" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "Update-MgPolicyAuthorizationPolicy -BodyParameter @{permissionGrantPolicyIdsAssignedToDefaultUserRole=@('ManagePermissionGrantsForSelf.microsoft-user-default-low')}", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. Sign in to Microsoft Entra admin center as Global Administrator or Privileged Role Administrator\n2. Go to Identity > Applications > Enterprise applications\n3. Select Consent and permissions > User consent settings\n4. Under User consent for applications, select \"Allow user consent for apps from verified publishers, for selected permissions\"\n5. Click Save", + "Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"\" {\n # Critical: restricts user consent to verified publishers with low-impact permissions only\n permission_grant_policy_ids_assigned_to_default_user_role = [\"ManagePermissionGrantsForSelf.microsoft-user-default-low\"]\n}\n```" }, "Recommendation": { - "Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Select Enterprise Applications 4. Select Consent and permissions 5. Select User consent settings 6. Under User consent for applications, select Allow user consent for apps from verified publishers, for selected permissions 7. Select Save", - "Url": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users" + "Text": "Enforce **least privilege** for app consent:\n- Remove `microsoft-user-default-legacy`\n- Allow consent only for verified publishers and low-impact permissions (e.g., `microsoft-user-default-low`)\n- Require admin approval for higher-risk scopes via the admin consent workflow\n- Periodically review and revoke unused consent grants", + "Url": "https://hub.prowler.com/check/entra_policy_user_consent_for_verified_apps" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Enforcing this setting may create additional requests that administrators need to review." diff --git a/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.metadata.json b/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.metadata.json index ff432ea9a6..97ddb4252f 100644 --- a/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.metadata.json +++ b/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "entra_privileged_user_has_mfa", - "CheckTitle": "Ensure that 'Multi-Factor Auth Status' is 'Enabled' for all Privileged Users", + "CheckTitle": "Privileged user has multi-factor authentication enabled", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "#microsoft.graph.users", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "Enable multi-factor authentication for all roles, groups, and users that have write access or permissions to Azure resources. These include custom created objects or built-in roles such as, - Service Co-Administrators - Subscription Owners - Contributors", - "Risk": "Multi-factor authentication requires an individual to present a minimum of two separate forms of authentication before access is granted. Multi-factor authentication provides additional assurance that the individual attempting to gain access is who they claim to be. With multi-factor authentication, an attacker would need to compromise at least two different authentication mechanisms, increasing the difficulty of compromise and thus reducing the risk.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks", + "Description": "**Microsoft Entra** privileged accounts are expected to use **multifactor authentication**. This evaluates users assigned to elevated directory roles and confirms they have **multiple authentication methods** registered for sign-in.", + "Risk": "Without **MFA**, privileged accounts face **phishing**, **password spraying**, and **credential reuse** risks. Compromise can grant tenant-wide admin control to alter roles, create backdoors, exfiltrate data, and weaken defenses, impacting **confidentiality**, **integrity**, and **availability**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/multi-factor-authentication-for-all-privileged-users.html#", + "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "az rest --method post --url https://graph.microsoft.com/v1.0/users//authentication/phoneMethods --body '{\"phoneNumber\":\"+10000000000\",\"phoneType\":\"mobile\"}'", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/multi-factor-authentication-for-all-privileged-users.html#", - "Terraform": "" + "Other": "1. Sign in to Microsoft Entra admin center\n2. Go to Identity > Protection > Conditional Access > + New policy\n3. Name the policy\n4. Under Users > Select users and groups > Directory roles, select the privileged roles to protect\n5. Under Target resources (or Cloud apps), select All cloud apps\n6. Under Grant, select Grant access and check Require multifactor authentication\n7. Set Enable policy to On and click Create\n8. Have each privileged user go to https://myaccount.microsoft.com/security-info and add at least one MFA method (e.g., Microsoft Authenticator or phone) to complete registration", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"\"\n state = \"enabled\"\n\n conditions {\n users {\n included_roles = [\"\"] # Critical: targets privileged role(s)\n }\n applications {\n included_applications = [\"All\"]\n }\n }\n\n grant_controls {\n operator = \"OR\"\n built_in_controls = [\"mfa\"] # Critical: requires MFA to access\n }\n}\n```" }, "Recommendation": { - "Text": "Activate one of the available multi-factor authentication methods for users in Microsoft Entra ID.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa" + "Text": "Enforce **MFA** for all privileged roles via **Conditional Access** or security defaults. Prefer **phishing-resistant** methods (FIDO2, passkeys, Authenticator push) over SMS/voice. Require registration before granting privileges, block legacy/basic auth, and apply **least privilege** with protected break-glass accounts.", + "Url": "https://hub.prowler.com/check/entra_privileged_user_has_mfa" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Users would require two forms of authentication before any access is granted. Additional administrative time will be required for managing dual forms of authentication when enabling multi-factor authentication." diff --git a/prowler/providers/azure/services/entra/entra_security_defaults_enabled/entra_security_defaults_enabled.metadata.json b/prowler/providers/azure/services/entra/entra_security_defaults_enabled/entra_security_defaults_enabled.metadata.json index 2686f41566..c3e9511afb 100644 --- a/prowler/providers/azure/services/entra/entra_security_defaults_enabled/entra_security_defaults_enabled.metadata.json +++ b/prowler/providers/azure/services/entra/entra_security_defaults_enabled/entra_security_defaults_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "entra_security_defaults_enabled", - "CheckTitle": "Ensure Security Defaults is enabled on Microsoft Entra ID", + "CheckTitle": "Microsoft Entra ID tenant has Security Defaults enabled", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "#microsoft.graph.identitySecurityDefaultsEnforcementPolicy", + "Severity": "critical", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "security", - "Description": "Security defaults in Microsoft Entra ID make it easier to be secure and help protect your organization. Security defaults contain preconfigured security settings for common attacks. Security defaults is available to everyone. The goal is to ensure that all organizations have a basic level of security enabled at no extra cost. You may turn on security defaults in the Azure portal.", - "Risk": "Security defaults provide secure default settings that we manage on behalf of organizations to keep customers safe until they are ready to manage their own identity security settings. For example, doing the following: - Requiring all users and admins to register for MFA. - Challenging users with MFA - when necessary, based on factors such as location, device, role, and task. - Disabling authentication from legacy authentication clients, which can’t do MFA.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/fundamentals/security-defaults", + "Description": "Microsoft Entra **Security defaults** provide tenant-wide baseline identity protections:\n- MFA registration and challenges\n- Legacy auth (`IMAP/POP/SMTP`) blocked\n- Extra checks for privileged access\n\nThis evaluation identifies whether that baseline is enabled at the tenant level.", + "Risk": "Absent these defaults, users can sign in with **password-only** or via **legacy protocols** that bypass MFA, enabling **password spray**, replay, and phishing-based takeovers. Compromise risks data exposure (confidentiality), unauthorized changes (integrity), and service disruption (availability).", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/security-defaults-enabled.html", + "https://learn.microsoft.com/en-us/entra/fundamentals/security-defaults" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "az rest --method PATCH --url https://graph.microsoft.com/v1.0/policies/identitySecurityDefaultsEnforcementPolicy --body '{\"isEnabled\":true}'", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/security-defaults-enabled.html#", + "Other": "1. Sign in to the Microsoft Entra admin center with a Conditional Access Administrator or Global Administrator account\n2. Go to Identity > Overview > Properties\n3. Click Manage security defaults\n4. Select Enabled and click Save", "Terraform": "" }, "Recommendation": { - "Text": "1. From Azure Home select the Portal Menu. 2. Browse to Microsoft Entra ID > Properties 3. Select Manage security defaults 4. Set the Enable security defaults to Enabled 5. Select Save", - "Url": "https://techcommunity.microsoft.com/t5/microsoft-entra-blog/introducing-security-defaults/ba-p/1061414" + "Text": "Activate **Security defaults** or implement equivalent **Conditional Access** as defense in depth:\n- Require MFA for all identities\n- Block legacy authentication\n- Safeguard admin portals and APIs\nApply **least privilege** and **zero trust**, and regularly review access patterns and break-glass exceptions to keep coverage complete.", + "Url": "https://hub.prowler.com/check/entra_security_defaults_enabled" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "This recommendation should be implemented initially and then may be overridden by other service/product specific CIS Benchmarks. Administrators should also be aware that certain configurations in Microsoft Entra ID may impact other Microsoft services such as Microsoft 365." diff --git a/prowler/providers/azure/services/entra/entra_trusted_named_locations_exists/entra_trusted_named_locations_exists.metadata.json b/prowler/providers/azure/services/entra/entra_trusted_named_locations_exists/entra_trusted_named_locations_exists.metadata.json index 807b6dee58..95f6a5fc7d 100644 --- a/prowler/providers/azure/services/entra/entra_trusted_named_locations_exists/entra_trusted_named_locations_exists.metadata.json +++ b/prowler/providers/azure/services/entra/entra_trusted_named_locations_exists/entra_trusted_named_locations_exists.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "entra_trusted_named_locations_exists", - "CheckTitle": "Ensure Trusted Locations Are Defined", + "CheckTitle": "Entra tenant has a trusted named location with IP ranges defined", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "#microsoft.graph.ipNamedLocation", + "Severity": "low", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "network", - "Description": "Microsoft Entra ID Conditional Access allows an organization to configure Named locations and configure whether those locations are trusted or untrusted. These settings provide organizations the means to specify Geographical locations for use in conditional access policies, or define actual IP addresses and IP ranges and whether or not those IP addresses and/or ranges are trusted by the organization.", - "Risk": "Defining trusted source IP addresses or ranges helps organizations create and enforce Conditional Access policies around those trusted or untrusted IP addresses and ranges. Users authenticating from trusted IP addresses and/or ranges may have less access restrictions or access requirements when compared to users that try to authenticate to Microsoft Entra ID from untrusted locations or untrusted source IP addresses/ranges.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/location-condition", + "Description": "**Microsoft Entra ID Conditional Access** supports **trusted named locations** defined by **public IP ranges**. Presence of at least one location marked `trusted` with IP CIDR ranges available for use in policy conditions.", + "Risk": "Without trusted IP-based locations, policies can't reliably distinguish corporate networks from unknown sources. This weakens **confidentiality and integrity**, enabling risky sign-ins to avoid stricter controls and forcing coarse rules that over-prompt users or leave **account takeover** and **data exfiltration** paths open.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/location-condition" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "az rest --method post --url https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations --headers Content-Type=application/json --body '{\"@odata.type\":\"#microsoft.graph.ipNamedLocation\",\"displayName\":\"\",\"isTrusted\":true,\"ipRanges\":[{\"@odata.type\":\"#microsoft.graph.iPv4CidrRange\",\"cidrAddress\":\"203.0.113.0/24\"}]}'", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. Sign in to the Microsoft Entra admin center (entra.microsoft.com)\n2. Go to Microsoft Entra ID > Protection > Conditional Access > Named locations\n3. Click New location\n4. Enter Name: \n5. Choose IP ranges location and add an IP range (e.g., 203.0.113.0/24)\n6. Check Mark as trusted location\n7. Click Create", + "Terraform": "```hcl\nresource \"azuread_named_location\" \"\" {\n display_name = \"\"\n\n ip {\n ip_ranges = [\"203.0.113.0/24\"]\n trusted = true # Critical: marks the location as trusted for Conditional Access policies\n }\n}\n```" }, "Recommendation": { - "Text": "1. Navigate to the Microsoft Entra ID Conditional Access Blade 2. Click on the Named locations blade 3. Within the Named locations blade, click on IP ranges location 4. Enter a name for this location setting in the Name text box 5. Click on the + sign 6. Add an IP Address Range in CIDR notation inside the text box that appears 7. Click on the Add button 8. Repeat steps 5 through 7 for each IP Range that needs to be added 9. If the information entered are trusted ranges, select the Mark as trusted location check box 10. Once finished, click on Create", - "Url": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions" + "Text": "Define **named locations** for your organization's egress IP ranges and mark them as `trusted`. Keep ranges accurate and narrow; review regularly. Use them in **Conditional Access** to enforce stronger controls off trusted networks. Apply **zero trust** and **least privilege**, and require MFA or device compliance when outside trusted locations.", + "Url": "https://hub.prowler.com/check/entra_trusted_named_locations_exists" } }, - "Categories": [], + "Categories": [ + "identity-access", + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "When configuring Named locations, the organization can create locations using Geographical location data or by defining source IP addresses or ranges. Configuring Named locations using a Country location does not provide the organization the ability to mark those locations as trusted, and any Conditional Access policy relying on those Countries location setting will not be able to use the All trusted locations setting within the Conditional Access policy. They instead will have to rely on the Select locations setting. This may add additional resource requirements when configuring, and will require thorough organizational testing. In general, Conditional Access policies may completely prevent users from authenticating to Microsoft Entra ID, and thorough testing is recommended. To avoid complete lockout, a 'Break Glass' account with full Global Administrator rights is recommended in the event all other administrators are locked out of authenticating to Microsoft Entra ID. This 'Break Glass' account should be excluded from Conditional Access Policies and should be configured with the longest pass phrase feasible. This account should only be used in the event of an emergency and complete administrator lockout." diff --git a/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.metadata.json b/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.metadata.json index f549b47082..5a436eb7d4 100644 --- a/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.metadata.json +++ b/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "entra_user_with_vm_access_has_mfa", - "CheckTitle": "Ensure only MFA enabled identities can access privileged Virtual Machine", + "CheckTitle": "Entra ID user with VM access has multi-factor authentication enabled", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "#microsoft.graph.users", + "Severity": "high", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "Verify identities without MFA that can log in to a privileged virtual machine using separate login credentials. An adversary can leverage the access to move laterally and perform actions with the virtual machine's managed identity. Make sure the virtual machine only has necessary permissions, and revoke the admin-level permissions according to the least privileges principal", - "Risk": "Managed disks are by default encrypted on the underlying hardware, so no additional encryption is required for basic protection. It is available if additional encryption is required. Managed disks are by design more resilient that storage accounts. For ARM-deployed Virtual Machines, Azure Adviser will at some point recommend moving VHDs to managed disks both from a security and cost management perspective.", + "Description": "**Microsoft Entra** users with Azure roles that grant VM sign-in or management access-such as `Owner`, `Contributor`, `Virtual Machine * Login`, and `Virtual Machine Contributor`-are evaluated for **multi-factor authentication** enrollment. The finding highlights accounts with VM access that lack more than one authentication factor.", + "Risk": "Without **MFA**, accounts with VM access are vulnerable to phishing, password spraying, and credential stuffing. Compromise can enable remote VM login, abuse of the VM's managed identity, privilege escalation, and lateral movement-impacting confidentiality, integrity, and availability of workloads.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.rebeladmin.com/step-step-guide-enable-mfa-azure-admins-preview/", + "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-mfa-userdevicesettings", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/vm-access-with-mfa-enabled-identities.html" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "az rest --method post --url https://graph.microsoft.com/v1.0/users//authentication/phoneMethods --body '{\"phoneNumber\":\"+10000000000\",\"phoneType\":\"mobile\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Sign in to Microsoft Entra admin center (entra.microsoft.com) with an admin account\n2. Go to Identity > Users > select the user with VM access\n3. Select Authentication methods > Add authentication method > choose Phone\n4. Enter the user's E.164 phone number (e.g., +15551234567) and click Add", "Terraform": "" }, "Recommendation": { - "Text": "1. Log in to the Azure portal. Reducing access of managed identities attached to virtual machines. 2. This can be remediated by enabling MFA for user, Removing user access or • Case I : Enable MFA for users having access on virtual machines. 1. Navigate to Azure AD from the left pane and select Users from the Manage section. 2. Click on Per-User MFA from the top menu options and select each user with MULTI-FACTOR AUTH STATUS as Disabled and can login to virtual machines:  From quick steps on the right side select enable.  Click on enable multi-factor auth and share the link with the user to setup MFA as required. • Case II : Removing user access on a virtual machine. 1. Select the Subscription, then click on Access control (IAM). 2. Select Role assignments and search for Virtual Machine Administrator Login or Virtual Machine User Login or any role that provides access to log into virtual machines. 3. Click on Role Name, Select Assignments, and remove identities with no MFA configured. • Case III : Reducing access of managed identities attached to virtual machines. 1. Select the Subscription, then click on Access control (IAM). 2. Select Role Assignments from the top menu and apply filters on Assignment type as Privileged administrator roles and Type as Virtual Machines. 3. Click on Role Name, Select Assignments, and remove identities access make sure this follows the least privileges principal.", - "Url": "" + "Text": "Enforce **MFA** for all identities that can sign in to or manage VMs via **Conditional Access**, preferring strong, phishing-resistant methods. Apply **least privilege** by removing broad roles (`Owner`, `Contributor`) when not required. Use **PIM/JIT** for admin access and monitor sign-in risk for continuous assurance.", + "Url": "https://hub.prowler.com/check/entra_user_with_vm_access_has_mfa" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "This recommendation requires an Azure AD P2 License to implement. Ensure that identities that are provisioned to a virtual machine utilizes an RBAC/ABAC group and is allocated a role using Azure PIM, and the Role settings require MFA or use another PAM solution (like CyberArk) for accessing Virtual Machines." diff --git a/prowler/providers/azure/services/entra/entra_users_cannot_create_microsoft_365_groups/entra_users_cannot_create_microsoft_365_groups.metadata.json b/prowler/providers/azure/services/entra/entra_users_cannot_create_microsoft_365_groups/entra_users_cannot_create_microsoft_365_groups.metadata.json index feb6b37513..f11b93c9d3 100644 --- a/prowler/providers/azure/services/entra/entra_users_cannot_create_microsoft_365_groups/entra_users_cannot_create_microsoft_365_groups.metadata.json +++ b/prowler/providers/azure/services/entra/entra_users_cannot_create_microsoft_365_groups/entra_users_cannot_create_microsoft_365_groups.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "entra_users_cannot_create_microsoft_365_groups", - "CheckTitle": "Ensure that 'Users can create Microsoft 365 groups in Azure portals, API or PowerShell' is set to 'No'", + "CheckTitle": "Microsoft 365 group creation by users is disabled", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Microsoft.Users/Settings", + "Severity": "medium", + "ResourceType": "microsoft.aadiam/tenants", "ResourceGroup": "IAM", - "Description": "Restrict Microsoft 365 group creation to administrators only.", - "Risk": "Restricting Microsoft 365 group creation to administrators only ensures that creation of Microsoft 365 groups is controlled by the administrator. Appropriate groups should be created and managed by the administrator and group creation rights should not be delegated to any other user.", - "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/community/all-about-groups#microsoft-365-groups", + "Description": "**Microsoft Entra** directory setting **Group.Unified** governs who can create **Microsoft 365 Groups**. The evaluation inspects `EnableGroupCreation` and, when present, `GroupCreationAllowedGroupId` to determine if group creation is broadly allowed or restricted to a designated group.", + "Risk": "Unrestricted group creation drives sprawl of Teams, SharePoint sites, and mailboxes, undermining **confidentiality** via public spaces and guest invites. Compromised accounts can create groups to stage exfiltration or impersonation. It also heightens **integrity** risks from unsanctioned owners and **operational** burden for lifecycle and governance.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/users-can-create-office-365-groups.html#", + "https://learn.microsoft.com/en-us/microsoft-365/community/all-about-groups#microsoft-365-groups", + "https://learn.microsoft.com/en-us/microsoft-365/solutions/manage-creation-of-groups?view=o365-worldwide&redirectSourcePath=%252fen-us%252farticle%252fControl-who-can-create-Office-365-Groups-4c46c8cb-17d0-44b5-9776-005fced8e618" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "Update-MgDirectorySetting -DirectorySettingId (Get-MgDirectorySetting | Where-Object {$_.DisplayName -eq 'Group.Unified'}).Id -BodyParameter @{Values = @(@{Name = 'EnableGroupCreation'; Value = 'false'})}", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/users-can-create-office-365-groups.html#", - "Terraform": "" + "Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Groups > General\n3. Set \"Users can create Microsoft 365 groups in Azure portals, API, or PowerShell\" to \"No\"\n4. Click Save", + "Terraform": "```hcl\ndata \"azuread_directory_setting_template\" \"unified\" {\n display_name = \"Group.Unified\"\n}\n\nresource \"azuread_directory_setting\" \"example_resource_name\" {\n template_id = data.azuread_directory_setting_template.unified.id\n\n values = {\n EnableGroupCreation = \"false\"\n # Critical: sets EnableGroupCreation to false so users cannot create Microsoft 365 groups\n }\n}\n```" }, "Recommendation": { - "Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Then Groups 4. Select General in settings 5. Set Users can create Microsoft 365 groups in Azure portals, API or PowerShell to No", - "Url": "https://learn.microsoft.com/en-us/microsoft-365/solutions/manage-creation-of-groups?view=o365-worldwide&redirectSourcePath=%252fen-us%252farticle%252fControl-who-can-create-Office-365-Groups-4c46c8cb-17d0-44b5-9776-005fced8e618" + "Text": "Apply **least privilege**: set `EnableGroupCreation=false` and allow only a controlled group via `GroupCreationAllowedGroupId`. Use **governed provisioning** with naming policies, sensitivity labels, and expiration/owner reviews. Monitor creation events and enforce **separation of duties** with approvals and lifecycle management.", + "Url": "https://hub.prowler.com/check/entra_users_cannot_create_microsoft_365_groups" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Enabling this setting could create a number of requests that would need to be managed by an administrator." diff --git a/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.metadata.json b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.metadata.json index 84d939cb65..9dfb9e8ad5 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.metadata.json +++ b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.metadata.json @@ -1,30 +1,38 @@ { "Provider": "azure", "CheckID": "keyvault_access_only_through_private_endpoints", - "CheckTitle": "Ensure that public network access when using private endpoint is disabled.", + "CheckTitle": "Key Vault using private endpoints has public network access disabled", "CheckType": [], "ServiceName": "keyvault", "SubServiceName": "", - "ResourceIdTemplate": "/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.KeyVault/vaults/{vault_name}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "KeyVault", + "ResourceType": "microsoft.keyvault/vaults", "ResourceGroup": "security", - "Description": "Checks if Key Vaults with private endpoints have public network access disabled.", - "Risk": "Allowing public network access to Key Vault when using private endpoint can expose sensitive data to unauthorized access.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/key-vault/general/network-security", + "Description": "**Azure Key Vaults** configured with **private endpoints** have **public network access** set to `Disabled`, so connectivity occurs only over the private link.", + "Risk": "Internet exposure alongside a **private endpoint** breaks isolation and expands attack surface:\n- Brute-force or token replay on the data plane\n- Abuse of misconfigured allowlists or trusted bypass\n- DDoS on the public endpoint\nThis can enable secret exfiltration or unauthorized key use, impacting **confidentiality** and **integrity**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.icompaas.com/support/solutions/articles/62000234050-ensure-that-public-network-access-when-using-private-endpoint-is-disabled-automated-", + "https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview", + "https://learn.microsoft.com/en-us/azure/key-vault/general/network-security" + ], "Remediation": { "Code": { - "CLI": "az keyvault update --resource-group --name --public-network-access disabled", - "NativeIaC": "{\n \"type\": \"Microsoft.KeyVault/vaults\",\n \"apiVersion\": \"2022-07-01\",\n \"properties\": {\n \"publicNetworkAccess\": \"disabled\"\n }\n}", - "Terraform": "resource \"azurerm_key_vault\" \"example\" {\n # ... other configuration ...\n\n public_network_access_enabled = false\n}", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/KeyVault/use-private-endpoints.html" + "CLI": "az keyvault update --resource-group --name --public-network-access Disabled", + "NativeIaC": "```bicep\n// Disable public network access for the Key Vault\nresource kv 'Microsoft.KeyVault/vaults@2023-07-01' = {\n name: ''\n location: ''\n properties: {\n tenantId: ''\n sku: {\n name: 'standard'\n family: 'A'\n }\n publicNetworkAccess: 'Disabled' // Critical: disables public access so only private endpoints are used\n }\n}\n```", + "Other": "1. In the Azure portal, go to Key vaults and select your vault\n2. Open Networking\n3. Under Public access, set Public network access to Disabled\n4. Click Save", + "Terraform": "```hcl\nresource \"azurerm_key_vault\" \"kv\" {\n name = \"\"\n location = \"\"\n resource_group_name = \"\"\n tenant_id = \"\"\n sku_name = \"standard\"\n\n public_network_access_enabled = false # Critical: disables public access when using private endpoints\n}\n```" }, "Recommendation": { - "Text": "Disable public network access for Key Vaults that use private endpoint to ensure network traffic only flows through the private endpoint.", - "Url": "https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview" + "Text": "Restrict access to **private endpoints** only:\n- Set `publicNetworkAccess` to `Disabled`\n- Avoid broad allowlists; limit `Trusted services`\n- Use private DNS with controlled egress\n- Enforce **least privilege** and monitor access logs\nThis sustains **defense in depth** and prevents Internet exposure.", + "Url": "https://hub.prowler.com/check/keyvault_access_only_through_private_endpoints" } }, - "Categories": [], + "Categories": [ + "internet-exposed", + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/azure/services/keyvault/keyvault_key_expiration_set_in_non_rbac/keyvault_key_expiration_set_in_non_rbac.metadata.json b/prowler/providers/azure/services/keyvault/keyvault_key_expiration_set_in_non_rbac/keyvault_key_expiration_set_in_non_rbac.metadata.json index 2c09e30ea2..a82171835f 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_key_expiration_set_in_non_rbac/keyvault_key_expiration_set_in_non_rbac.metadata.json +++ b/prowler/providers/azure/services/keyvault/keyvault_key_expiration_set_in_non_rbac/keyvault_key_expiration_set_in_non_rbac.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "keyvault_key_expiration_set_in_non_rbac", - "CheckTitle": "Ensure that the Expiration Date is set for all Keys in Non-RBAC Key Vaults.", + "CheckTitle": "Key Vault without RBAC authorization has expiration date set for all enabled keys", "CheckType": [], "ServiceName": "keyvault", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "KeyVault", + "ResourceType": "microsoft.keyvault/vaults", "ResourceGroup": "security", - "Description": "Ensure that all Keys in Non Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.", - "Risk": "Azure Key Vault enables users to store and use cryptographic keys within the Microsoft Azure environment. The exp (expiration date) attribute identifies the expiration date on or after which the key MUST NOT be used for a cryptographic operation. By default, keys never expire. It is thus recommended that keys be rotated in the key vault and set an explicit expiration date for all keys. This ensures that the keys cannot be used beyond their assigned lifetimes.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis", + "Description": "**Azure Key Vaults** using access **policies (non-RBAC)** are assessed to confirm all **enabled keys** have an `expiration` (`exp`) defined. The finding highlights keys in these vaults that lack a set lifetime.", + "Risk": "Non-expiring keys enable indefinite use, degrading **confidentiality** and **integrity**. Stale or compromised keys can decrypt data, forge signatures, and maintain persistence. Absent lifetimes weaken rotation discipline and impede timely revocation, increasing exposure to cryptographic and operational drift.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/key-vault/general/basic-concepts", + "https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#key-vault-keys", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/KeyVault/key-expiration-check.html#" + ], "Remediation": { "Code": { - "CLI": "az keyvault key set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z'", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/KeyVault/key-expiration-check.html#", - "Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/set-an-expiration-date-on-all-keys#terraform" + "CLI": "az keyvault key set-attributes --vault-name --name --expires 2030-01-01T00:00:00Z", + "NativeIaC": "```bicep\n// Set expiration on a Key Vault key\nresource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {\n name: ''\n}\n\nresource key 'Microsoft.KeyVault/vaults/keys@2023-07-01' = {\n name: ''\n parent: kv\n properties: {\n kty: 'RSA'\n attributes: {\n exp: 1893456000 // CRITICAL: sets the key expiration (Unix epoch seconds) to pass the check\n }\n }\n}\n```", + "Other": "1. In the Azure portal, go to Key vaults and open the vault\n2. Select Keys and choose the enabled key that failed\n3. Open the current version and click Edit (or Update)\n4. Set Expiration date (UTC) to a future date\n5. Click Save", + "Terraform": "```hcl\nresource \"azurerm_key_vault_key\" \"\" {\n name = \"\"\n key_vault_id = \"\"\n key_type = \"RSA\"\n\n expires_on = \"2030-01-01T00:00:00Z\" # CRITICAL: sets key expiration to pass the check\n}\n```" }, "Recommendation": { - "Text": "From Azure Portal: 1. Go to Key vaults. 2. For each Key vault, click on Keys. 3. In the main pane, ensure that an appropriate Expiration date is set for any keys that are Enabled. From Azure CLI: Update the Expiration date for the key using the below command: az keyvault key set-attributes --name --vault-name -- expires Y-m-d'T'H:M:S'Z' Note: To view the expiration date on all keys in a Key Vault using Microsoft API, the 'List' Key permission is required. To update the expiration date for the keys: 1. Go to the Key vault, click on Access Control (IAM). 2. Click on Add role assignment and assign the role of Key Vault Crypto Officer to the appropriate user. From PowerShell: Set-AzKeyVaultKeyAttribute -VaultName -Name -Expires ", - "Url": "https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-keys" + "Text": "Set an `expiration` on all keys and enforce **automated rotation** with advance alerts. Retire or disable old versions promptly and rotate after any suspected exposure. Apply **least privilege** and **separation of duties** for key administration. Prefer standardized lifecycle policies (e.g., RBAC-based governance) to enforce consistent control.", + "Url": "https://hub.prowler.com/check/keyvault_key_expiration_set_in_non_rbac" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Keys cannot be used beyond their assigned expiration dates respectively. Keys need to be rotated periodically wherever they are used." diff --git a/prowler/providers/azure/services/keyvault/keyvault_key_rotation_enabled/keyvault_key_rotation_enabled.metadata.json b/prowler/providers/azure/services/keyvault/keyvault_key_rotation_enabled/keyvault_key_rotation_enabled.metadata.json index c60613991c..45c459c948 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_key_rotation_enabled/keyvault_key_rotation_enabled.metadata.json +++ b/prowler/providers/azure/services/keyvault/keyvault_key_rotation_enabled/keyvault_key_rotation_enabled.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "keyvault_key_rotation_enabled", - "CheckTitle": "Ensure Automatic Key Rotation is Enabled Within Azure Key Vault for the Supported Services", + "CheckTitle": "Key Vault key has automatic rotation enabled", "CheckType": [], "ServiceName": "keyvault", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "KeyVault", + "ResourceType": "microsoft.keyvault/vaults", "ResourceGroup": "security", - "Description": "Automatic Key Rotation is available in Public Preview. The currently supported applications are Key Vault, Managed Disks, and Storage accounts accessing keys within Key Vault. The number of supported applications will incrementally increased.", - "Risk": "Once set up, Automatic Private Key Rotation removes the need for manual administration when keys expire at intervals determined by your organization's policy. The recommended key lifetime is 2 years. Your organization should determine its own key expiration policy.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/keys/how-to-configure-key-rotation", + "Description": "**Azure Key Vault** keys configured with a **rotation policy** that includes a `Rotate` lifetime action.\n\nThe evaluation looks for lifetime actions that schedule automatic key version creation; keys without this policy are not configured for auto-rotation.", + "Risk": "Without **auto-rotation**, keys may outlive policy, increasing exposure if material is leaked and weakening **confidentiality**.\n\nExpired keys without planned rollover can break decrypt/unwrap operations, impacting **availability**. Long-lived keys hinder incident response and enable prolonged misuse of stale versions.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/storage/common/customer-managed-keys-overview#update-the-key-version", + "https://learn.microsoft.com/en-us/azure/key-vault/keys/how-to-configure-key-rotation", + "https://www.techtarget.com/searchcloudcomputing/tutorial/How-to-perform-and-automate-key-rotation-in-Azure-Key-Vault" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "az keyvault key rotation-policy update --vault-name --name --value '{\"lifetimeActions\":[{\"trigger\":{\"timeAfterCreate\":\"P18M\"},\"action\":{\"type\":\"Rotate\"}}]}'", + "NativeIaC": "```bicep\nresource key 'Microsoft.KeyVault/vaults/keys@2023-02-01' = {\n name: '/'\n location: resourceGroup().location\n properties: {\n kty: 'RSA'\n rotationPolicy: {\n lifetimeActions: [\n {\n trigger: { timeAfterCreate: 'P18M' }\n action: { type: 'Rotate' } // Critical: enables automatic rotation, satisfying the check\n }\n ]\n }\n }\n}\n```", + "Other": "1. In Azure Portal, go to Key Vaults > > Keys\n2. Select the key \n3. Click Rotation policy\n4. Enable auto-rotation and set a rotation interval (e.g., After creation: P18M)\n5. Click Save", + "Terraform": "```hcl\nresource \"azurerm_key_vault_key\" \"key\" {\n name = \"\"\n key_vault_id = \"\"\n key_type = \"RSA\"\n\n rotation_policy {\n automatic {\n time_after_creation = \"P18M\" # Critical: creates a Rotate lifetime action to enable auto-rotation\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Note: Azure CLI and Powershell use ISO8601 flags to input timespans. Every timespan input will be in the format P(Y,M,D). The leading P is required with it denoting period. The (Y,M,D) are for the duration of Year, Month,and Day respectively. A time frame of 2 years, 2 months, 2 days would be (P2Y2M2D). From Azure Portal 1. From Azure Portal select the Portal Menu in the top left. 2. Select Key Vaults. 3. Select a Key Vault to audit. 4. Under Objects select Keys. 5. Select a key to audit. 6. In the top row select Rotation policy. 7. Select an Expiry time. 8. Set Enable auto rotation to Enabled. 9. Set an appropriate Rotation option and Rotation time. 10. Optionally set the Notification time. 11. Select Save. 12. Repeat steps 3-11 for each Key Vault and Key. From PowerShell Run the following command for each key to update its policy: Set-AzKeyVaultKeyRotationPolicy -VaultName test-kv -Name test-key -PolicyPath rotation_policy.json", - "Url": "https://docs.microsoft.com/en-us/azure/storage/common/customer-managed-keys-overview#update-the-key-version" + "Text": "Define a per-key **rotation policy** to automatically `Rotate` on a fixed cadence (e.g., `P2Y`) and set an **expiry** to enforce lifecycle.\n\nUse versionless key URIs in dependent services, apply **least privilege** to rotation roles, enable near-expiry notifications, and monitor events for **defense in depth**.", + "Url": "https://hub.prowler.com/check/keyvault_key_rotation_enabled" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "There are an additional costs per operation in running the needed applications." diff --git a/prowler/providers/azure/services/keyvault/keyvault_logging_enabled/keyvault_logging_enabled.metadata.json b/prowler/providers/azure/services/keyvault/keyvault_logging_enabled/keyvault_logging_enabled.metadata.json index 5235f1c99f..b093492c7e 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_logging_enabled/keyvault_logging_enabled.metadata.json +++ b/prowler/providers/azure/services/keyvault/keyvault_logging_enabled/keyvault_logging_enabled.metadata.json @@ -1,30 +1,38 @@ { "Provider": "azure", "CheckID": "keyvault_logging_enabled", - "CheckTitle": "Ensure that logging for Azure Key Vault is 'Enabled'", + "CheckTitle": "Key Vault has a diagnostic setting capturing audit logs", "CheckType": [], "ServiceName": "keyvault", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "KeyVault", + "Severity": "high", + "ResourceType": "microsoft.keyvault/vaults", "ResourceGroup": "security", - "Description": "Enable AuditEvent logging for key vault instances to ensure interactions with key vaults are logged and available.", - "Risk": "Monitoring how and when key vaults are accessed, and by whom, enables an audit trail of interactions with confidential information, keys, and certificates managed by Azure Keyvault. Enabling logging for Key Vault saves information in an Azure storage account which the user provides. This creates a new container named insights-logs-auditevent automatically for the specified storage account. This same storage account can be used for collecting logs for multiple key vaults.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-logging", + "Description": "**Azure Key Vault** diagnostic settings capture **audit logs** (`AuditEvent`) when category groups `audit` and `allLogs` are enabled and routed to a supported destination. Logged events include management and data-plane operations on vaults, keys, secrets, and certificates.", + "Risk": "Without **Key Vault audit logging**, access and changes to keys, secrets, and certificates are untracked.\n\nAttackers can misuse keys to decrypt data, alter or delete crypto material, and evade detection-eroding **confidentiality** and **integrity** and delaying **incident response**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/key-vault/general/logging?tabs=Vault", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/KeyVault/enable-audit-event-logging-for-azure-key-vaults.html", + "https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-8-ensure-security-of-key-and-certificate-repository", + "https://learn.microsoft.com/en-us/azure/key-vault/general/howto-logging?tabs=azure-cli" + ], "Remediation": { "Code": { - "CLI": "az monitor diagnostic-settings create --name --resource --logs'[{category:AuditEvents,enabled:true,retention-policy:{enabled:true,days:180}}]' --metrics'[{category:AllMetrics,enabled:true,retention-policy:{enabled:true,days:180}}]' <[--event-hub --event-hub-rule | --storage-account |--workspace | --marketplace-partner-id ]>", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/KeyVault/enable-audit-event-logging-for-azure-key-vaults.html", - "Terraform": "" + "CLI": "az monitor diagnostic-settings create --name --resource --workspace --logs '[{\"categoryGroup\":\"audit\",\"enabled\":true},{\"categoryGroup\":\"allLogs\",\"enabled\":true}]'", + "NativeIaC": "```bicep\n// Enable Key Vault diagnostic settings with audit + allLogs\nparam keyVaultName string\nparam workspaceId string\n\nresource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {\n name: keyVaultName\n}\n\nresource diag 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {\n name: ''\n scope: kv\n properties: {\n workspaceId: workspaceId\n logs: [\n {\n categoryGroup: 'audit' // critical: enables audit logs\n enabled: true // required to pass the check\n }\n {\n categoryGroup: 'allLogs' // critical: enables allLogs group\n enabled: true // required to pass the check\n }\n ]\n }\n}\n```", + "Other": "1. In Azure Portal, go to your Key Vault > Monitoring > Diagnostic settings\n2. Click Add diagnostic setting\n3. Under Category groups, select audit and allLogs\n4. Choose a destination (e.g., Send to Log Analytics workspace) and select the workspace\n5. Click Save", + "Terraform": "```hcl\n# Enable diagnostic settings on Key Vault with audit + allLogs\nresource \"azurerm_monitor_diagnostic_setting\" \"\" {\n name = \"\"\n target_resource_id = \"\" # Key Vault resource ID\n log_analytics_workspace_id = \"\" # Destination workspace ID\n\n enabled_log { # critical: audit category group\n category_group = \"audit\" # enables audit logs\n }\n enabled_log { # critical: allLogs category group\n category_group = \"allLogs\" # enables all logs\n }\n}\n```" }, "Recommendation": { - "Text": "1. Go to Key vaults 2. For each Key vault 3. Go to Diagnostic settings 4. Click on Edit Settings 5. Ensure that Archive to a storage account is Enabled 6. Ensure that AuditEvent is checked, and the retention days is set to 180 days or as appropriate", - "Url": "https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-8-ensure-security-of-key-and-certificate-repository" + "Text": "Enable **diagnostic settings** to collect `AuditEvent` logs-covering category groups `audit` and `allLogs`-and send them to a central sink. Apply **least privilege** to log access, enforce secure **retention/immutability**, monitor with alerts for anomalous operations, and use **separation of duties** to prevent logging bypass.", + "Url": "https://hub.prowler.com/check/keyvault_logging_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, Diagnostic AuditEvent logging is not enabled for Key Vault instances." diff --git a/prowler/providers/azure/services/keyvault/keyvault_non_rbac_secret_expiration_set/keyvault_non_rbac_secret_expiration_set.metadata.json b/prowler/providers/azure/services/keyvault/keyvault_non_rbac_secret_expiration_set/keyvault_non_rbac_secret_expiration_set.metadata.json index 393b8c2532..edfb367858 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_non_rbac_secret_expiration_set/keyvault_non_rbac_secret_expiration_set.metadata.json +++ b/prowler/providers/azure/services/keyvault/keyvault_non_rbac_secret_expiration_set/keyvault_non_rbac_secret_expiration_set.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "keyvault_non_rbac_secret_expiration_set", - "CheckTitle": "Ensure that the Expiration Date is set for all Secrets in Non-RBAC Key Vaults", + "CheckTitle": "Non-RBAC Key Vault has expiration date set for all secrets", "CheckType": [], "ServiceName": "keyvault", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "KeyVault", + "Severity": "medium", + "ResourceType": "microsoft.keyvault/vaults", "ResourceGroup": "security", - "Description": "Ensure that all Secrets in Non Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.", - "Risk": "The Azure Key Vault enables users to store and keep secrets within the Microsoft Azure environment. Secrets in the Azure Key Vault are octet sequences with a maximum size of 25k bytes each. The exp (expiration date) attribute identifies the expiration date on or after which the secret MUST NOT be used. By default, secrets never expire. It is thus recommended to rotate secrets in the key vault and set an explicit expiration date for all secrets. This ensures that the secrets cannot be used beyond their assigned lifetimes.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis", + "Description": "**Azure Key Vault (non-RBAC)** secrets are expected to have an **explicit expiration date**.\n\nThis examines each **enabled secret** to confirm the `expires` attribute is defined.", + "Risk": "Secrets without expiration persist indefinitely, widening the window for misuse.\n\nIf leaked or forgotten, they allow long-term, covert access to services and data, undermining **confidentiality** and **integrity**, and complicating incident response and revocation.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/key-vault/general/basic-concepts", + "https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#key-vault-secrets" + ], "Remediation": { "Code": { - "CLI": "az keyvault secret set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z'", - "NativeIaC": "", - "Other": "", - "Terraform": "https://docs.prowler.com/checks/azure/azure-secrets-policies/set-an-expiration-date-on-all-secrets#terraform" + "CLI": "az keyvault secret set-attributes --vault-name --name --expires ", + "NativeIaC": "```bicep\n// Set an expiration date on a Key Vault secret\nresource secret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {\n name: '/'\n properties: {\n value: ''\n attributes: {\n exp: 1767225599 // CRITICAL: sets the secret expiration (Unix time in seconds) so the check passes\n }\n }\n}\n```", + "Other": "1. In the Azure portal, go to Key vaults and open your vault\n2. Select Secrets, then click the secret that failed\n3. Click + New version\n4. Set Expiration date and click Create\n5. Repeat for any other secret without an expiration", + "Terraform": "```hcl\nresource \"azurerm_key_vault_secret\" \"\" {\n name = \"\"\n value = \"\"\n key_vault_id = \"\"\n\n expiration_date = \"2025-12-31T23:59:59Z\" # CRITICAL: sets the secret expiration so the check passes\n}\n```" }, "Recommendation": { - "Text": "From Azure Portal: 1. Go to Key vaults. 2. For each Key vault, click on Secrets. 3. In the main pane, ensure that the status of the secret is Enabled. 4. Set an appropriate Expiration date on all secrets. From Azure CLI: Update the Expiration date for the secret using the below command: az keyvault secret set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z' Note: To view the expiration date on all secrets in a Key Vault using Microsoft API, the List Key permission is required. To update the expiration date for the secrets: 1. Go to Key vault, click on Access policies. 2. Click on Create and add an access policy with the Update permission (in the Secret Permissions - Secret Management Operations section). From PowerShell: For each Key vault with the EnableRbacAuthorization setting set to False or empty, run the following command. Set-AzKeyVaultSecret -VaultName -Name -Expires ", - "Url": "https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-secrets" + "Text": "Set an **expiration** on every secret and enforce a **rotation policy** aligned with risk and compliance.\n\nAutomate rotation and alerts, disable or purge stale versions, and apply **least privilege**. *Where possible*, use **managed identities** to reduce secret sprawl.", + "Url": "https://hub.prowler.com/check/keyvault_non_rbac_secret_expiration_set" } }, - "Categories": [], + "Categories": [ + "secrets" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Secrets cannot be used beyond their assigned expiry date respectively. Secrets need to be rotated periodically wherever they are used." diff --git a/prowler/providers/azure/services/keyvault/keyvault_private_endpoints/keyvault_private_endpoints.metadata.json b/prowler/providers/azure/services/keyvault/keyvault_private_endpoints/keyvault_private_endpoints.metadata.json index d79d1f15d9..6b52e35361 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_private_endpoints/keyvault_private_endpoints.metadata.json +++ b/prowler/providers/azure/services/keyvault/keyvault_private_endpoints/keyvault_private_endpoints.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "keyvault_private_endpoints", - "CheckTitle": "Ensure that Private Endpoints are Used for Azure Key Vault", + "CheckTitle": "Key Vault uses private endpoints", "CheckType": [], "ServiceName": "keyvault", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "KeyVault", + "ResourceType": "microsoft.keyvault/vaults", "ResourceGroup": "security", - "Description": "Private endpoints will secure network traffic from Azure Key Vault to the resources requesting secrets and keys.", - "Risk": "Private endpoints will keep network requests to Azure Key Vault limited to the endpoints attached to the resources that are whitelisted to communicate with each other. Assigning the Key Vault to a network without an endpoint will allow other resources on that network to view all traffic from the Key Vault to its destination. In spite of the complexity in configuration, this is recommended for high security secrets.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-overview", + "Description": "**Azure Key Vault** has **private endpoint connections** to serve secret and key operations over a private IP within your virtual network via Azure Private Link.", + "Risk": "Without **private endpoints**, the vault relies on a public endpoint, expanding exposure to scanning and misconfigured allowlists. Egress controls are harder to enforce, enabling unauthorized secret retrieval for **data exfiltration** and potential key misuse, impacting confidentiality and integrity.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview", + "https://learn.microsoft.com/en-us/azure/storage/common/storage-private-endpoints" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "az network private-endpoint create --name --resource-group --location --vnet-name --subnet --private-connection-resource-id --group-ids vault --connection-name ", + "NativeIaC": "```bicep\n// Create a Private Endpoint for an existing Key Vault\nresource pe 'Microsoft.Network/privateEndpoints@2021-08-01' = {\n name: ''\n location: ''\n properties: {\n subnet: {\n id: '' // Critical: subnet resource ID where the private endpoint NIC will be placed\n }\n privateLinkServiceConnections: [\n {\n name: ''\n properties: {\n privateLinkServiceId: '' // Critical: Key Vault resource ID to connect to\n groupIds: [ 'vault' ] // Critical: targets the Key Vault subresource to create the private endpoint connection\n }\n }\n ]\n }\n}\n```", + "Other": "1. In Azure portal, open your Key Vault\n2. Go to Networking > Private endpoint connections > + Create\n3. Basics: select Subscription and Resource group, then Next\n4. Resource: Service = Microsoft.KeyVault/vaults (your vault is preselected), Subresource = vault, Next\n5. Configuration: choose Virtual network and Subnet, then Next and Create\n6. Wait for the connection state to show Approved (auto-approves if you have permission)", + "Terraform": "```hcl\n# Create a Private Endpoint for an existing Key Vault\nresource \"azurerm_private_endpoint\" \"\" {\n name = \"\"\n location = \"\"\n resource_group_name = \"\"\n subnet_id = \"\" # Critical: subnet resource ID for the private endpoint NIC\n\n private_service_connection {\n name = \"\"\n private_connection_resource_id = \"\" # Critical: Key Vault resource ID to connect\n subresource_names = [\"vault\"] # Critical: targets Key Vault subresource to create the connection\n }\n}\n```" }, "Recommendation": { - "Text": "Please see the additional information about the requirements needed before starting this remediation procedure. From Azure Portal 1. From Azure Home open the Portal Menu in the top left. 2. Select Key Vaults. 3. Select a Key Vault to audit. 4. Select Networking in the left column. 5. Select Private endpoint connections from the top row. 6. Select + Create. 7. Select the subscription the Key Vault is within, and other desired configuration. 8. Select Next. 9. For resource type select Microsoft.KeyVault/vaults. 10. Select the Key Vault to associate the Private Endpoint with. 11. Select Next. 12. In the Virtual Networking field, select the network to assign the Endpoint. 13. Select other configuration options as desired, including an existing or new application security group. 14. Select Next. 15. Select the private DNS the Private Endpoints will use. 16. Select Next. 17. Optionally add Tags. 18. Select Next : Review + Create. 19. Review the information and select Create. Follow the Audit Procedure to determine if it has successfully applied. 20. Repeat steps 3-19 for each Key Vault. From Azure CLI 1. To create an endpoint, run the following command: az network private-endpoint create --resource-group --subnet --name -- private-connection-resource-id '/subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults/' --group-ids vault --connection-name -- location --manual-request 2. To manually approve the endpoint request, run the following command: az keyvault private-endpoint-connection approve --resource-group --vault-name –name 4. Determine the Private Endpoint's IP address to connect the Key Vault to the Private DNS you have previously created: 5. Look for the property networkInterfaces then id, the value must be placed in the variable within step 7. az network private-endpoint show -g -n 6. Look for the property networkInterfaces then id, the value must be placed on in step 7. az network nic show --ids 7. Create a Private DNS record within the DNS Zone you created for the Private Endpoint: az network private-dns record-set a add-record -g -z 'privatelink.vaultcore.azure.net' -n -a 8. nslookup the private endpoint to determine if the DNS record is correct: nslookup .vault.azure.net nslookup .privatelink.vaultcore.azure.n", - "Url": "https://docs.microsoft.com/en-us/azure/storage/common/storage-private-endpoints" + "Text": "Enable **Private Endpoints** for each Key Vault and disable public network access. Use private DNS so the vault FQDN resolves to the private IP. Apply **least privilege** with RBAC and managed identities, restrict traffic with NSGs and routing, and monitor access logs as part of **defense in depth**.", + "Url": "https://hub.prowler.com/check/keyvault_private_endpoints" } }, - "Categories": [], + "Categories": [ + "internet-exposed", + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Incorrect or poorly-timed changing of network configuration could result in service interruption. There are also additional costs tiers for running a private endpoint perpetabyte or more of networking traffic." diff --git a/prowler/providers/azure/services/keyvault/keyvault_rbac_enabled/keyvault_rbac_enabled.metadata.json b/prowler/providers/azure/services/keyvault/keyvault_rbac_enabled/keyvault_rbac_enabled.metadata.json index 96d465a5ae..d2b23d59c0 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_rbac_enabled/keyvault_rbac_enabled.metadata.json +++ b/prowler/providers/azure/services/keyvault/keyvault_rbac_enabled/keyvault_rbac_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "keyvault_rbac_enabled", - "CheckTitle": "Enable Role Based Access Control for Azure Key Vault", + "CheckTitle": "Key Vault uses Azure RBAC for access control", "CheckType": [], "ServiceName": "keyvault", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "KeyVault", + "ResourceType": "microsoft.keyvault/vaults", "ResourceGroup": "security", - "Description": "WARNING: Role assignments disappear when a Key Vault has been deleted (soft-delete) and recovered. Afterwards it will be required to recreate all role assignments. This is a limitation of the soft-delete feature across all Azure services.", - "Risk": "The new RBAC permissions model for Key Vaults enables a much finer grained access control for key vault secrets, keys, certificates, etc., than the vault access policy. This in turn will permit the use of privileged identity management over these roles, thus securing the key vaults with JIT Access management.", - "RelatedUrl": "https://docs.microsoft.com/en-gb/azure/key-vault/general/rbac-migration#vault-access-policy-to-azure-rbac-migration-steps", + "Description": "**Azure Key Vault** uses the **Azure RBAC permission model** for data-plane access to keys, secrets, and certificates, rather than legacy access policies.\n\nEvaluates whether data access is managed through role assignments at the vault.", + "Risk": "Without **Azure RBAC**, data access relies on coarse access policies. **Control-plane Contributors** can grant themselves data-plane rights, enabling secret or key exfiltration and unauthorized crypto operations.\n\nLack of JIT and least-privilege weakens **confidentiality** and **integrity** and hinders auditing.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/key-vault/general/rbac-guide?tabs=azure-cli", + "https://learn.microsoft.com/en-gb/azure/role-based-access-control/role-assignments-portal?tabs=current" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "az keyvault update --name --resource-group --enable-rbac-authorization true", + "NativeIaC": "```bicep\n// Enable Azure RBAC on a Key Vault\nresource kv 'Microsoft.KeyVault/vaults@2023-07-01' = {\n name: ''\n location: ''\n properties: {\n tenantId: ''\n enableRbacAuthorization: true // Critical: switches permission model to Azure RBAC to pass the check\n sku: {\n family: 'A'\n name: 'standard'\n }\n }\n}\n```", + "Other": "1. In Azure Portal, go to Key Vaults and open \n2. Under Settings, select Properties\n3. Set Permission model to Azure role-based access control\n4. Click Save", + "Terraform": "```hcl\nresource \"azurerm_key_vault\" \"\" {\n name = \"\"\n location = \"\"\n resource_group_name = \"\"\n tenant_id = \"\"\n sku_name = \"standard\"\n enable_rbac_authorization = true // Critical: enables Azure RBAC to satisfy the control\n}\n```" }, "Recommendation": { - "Text": "From Azure Portal Key Vaults can be configured to use Azure role-based access control on creation. For existing Key Vaults: 1. From Azure Home open the Portal Menu in the top left corner 2. Select Key Vaults 3. Select a Key Vault to audit 4. Select Access configuration 5. Set the Permission model radio button to Azure role-based access control, taking note of the warning message 6. Click Save 7. Select Access Control (IAM) 8. Select the Role Assignments tab 9. Reapply permissions as needed to groups or users", - "Url": "https://docs.microsoft.com/en-gb/azure/role-based-access-control/role-assignments-portal?tabs=current" + "Text": "Adopt **Azure RBAC** for Key Vault data access and design roles with **least privilege** at appropriate scopes (prefer vault-level per app/env). Use **Privileged Identity Management** for JIT, restrict control-plane Contributor rights, and monitor role assignments. *Role assignments aren't preserved after soft-delete recovery*.", + "Url": "https://hub.prowler.com/check/keyvault_rbac_enabled" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Implementation needs to be properly designed from the ground up, as this is a fundamental change to the way key vaults are accessed/managed. Changing permissions to key vaults will result in loss of service as permissions are re-applied. For the least amount of downtime, map your current groups and users to their corresponding permission needs." diff --git a/prowler/providers/azure/services/keyvault/keyvault_rbac_key_expiration_set/keyvault_rbac_key_expiration_set.metadata.json b/prowler/providers/azure/services/keyvault/keyvault_rbac_key_expiration_set/keyvault_rbac_key_expiration_set.metadata.json index e3664c3aba..531840721b 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_rbac_key_expiration_set/keyvault_rbac_key_expiration_set.metadata.json +++ b/prowler/providers/azure/services/keyvault/keyvault_rbac_key_expiration_set/keyvault_rbac_key_expiration_set.metadata.json @@ -1,30 +1,39 @@ { "Provider": "azure", "CheckID": "keyvault_rbac_key_expiration_set", - "CheckTitle": "Ensure that the Expiration Date is set for all Keys in RBAC Key Vaults", + "CheckTitle": "RBAC-enabled Key Vault has expiration date set for all keys", "CheckType": [], "ServiceName": "keyvault", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "KeyVault", + "Severity": "medium", + "ResourceType": "microsoft.keyvault/vaults", "ResourceGroup": "security", - "Description": "Ensure that all Keys in Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set", - "Risk": "Azure Key Vault enables users to store and use cryptographic keys within the Microsoft Azure environment. The exp (expiration date) attribute identifies the expiration date on or after which the key MUST NOT be used for encryption of new data, wrapping of new keys, and signing. By default, keys never expire. It is thus recommended that keys be rotated in the key vault and set an explicit expiration date for all keys to help enforce the key rotation. This ensures that the keys cannot be used beyond their assigned lifetimes.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis", + "Description": "**Azure Key Vaults** with **RBAC-enabled access control** are evaluated to confirm every **enabled key** defines an **expiration** (`exp`). Any key lacking this attribute is identified.", + "Risk": "**Keys without expiration** can remain active indefinitely.\nIf exposed, attackers can decrypt data, forge signatures (code/tokens), and maintain persistence, undermining **confidentiality** and **integrity**. Absent end-of-life also weakens rotation discipline and crypto agility.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/key-vault/general/basic-concepts", + "https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#key-vault-keys", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/KeyVault/key-expiration-check.html#", + "https://learn.microsoft.com/en-us/azure/key-vault/general/azure-policy", + "https://learn.microsoft.com/en-us/azure/defender-for-cloud/recommendations-reference-keyvault" + ], "Remediation": { "Code": { - "CLI": "az keyvault key set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z'", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/KeyVault/key-expiration-check.html#", - "Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/set-an-expiration-date-on-all-keys#terraform" + "CLI": "az keyvault key set-attributes --vault-name --name --expires ", + "NativeIaC": "```bicep\n// Set an expiration date on a Key Vault key\nresource key 'Microsoft.KeyVault/vaults/keys@2023-07-01' = {\n name: '/'\n properties: {\n kty: 'RSA'\n attributes: {\n exp: 1767225599 // Critical: expiration timestamp (UTC epoch seconds). Ensures the key has an expiration date to pass the check.\n }\n }\n}\n```", + "Other": "1. In Azure Portal, go to Key vaults and open \n2. Select Keys and choose the key missing an expiration\n3. Open the current version and click Update/Edit\n4. Set Expiration date (UTC) and click Save", + "Terraform": "```hcl\n# Set an expiration date on a Key Vault key\nresource \"azurerm_key_vault_key\" \"\" {\n name = \"\"\n key_vault_id = \"\"\n key_type = \"RSA\"\n\n expiration_date = \"2025-12-31T23:59:59Z\" # Critical: ensures the key has an expiration date to pass the check\n}\n```" }, "Recommendation": { - "Text": "From Azure Portal: 1. Go to Key vaults. 2. For each Key vault, click on Keys. 3. In the main pane, ensure that an appropriate Expiration date is set for any keys that are Enabled. From Azure CLI: Update the Expiration date for the key using the below command: az keyvault key set-attributes --name --vault-name -- expires Y-m-d'T'H:M:S'Z' Note: To view the expiration date on all keys in a Key Vault using Microsoft API, the 'List' Key permission is required. To update the expiration date for the keys: 1. Go to the Key vault, click on Access Control (IAM). 2. Click on Add role assignment and assign the role of Key Vault Crypto Officer to the appropriate user. From PowerShell: Set-AzKeyVaultKeyAttribute -VaultName -Name -Expires ", - "Url": "https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-keys" + "Text": "- Set `exp` on all enabled keys and enforce a **rotation policy** with short lifetimes and automated renewal.\n- Use **governance policies** to require expiration and alert before expiry.\n- Apply **least privilege** and **separation of duties** for key admins vs consumers.", + "Url": "https://hub.prowler.com/check/keyvault_rbac_key_expiration_set" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Keys cannot be used beyond their assigned expiration dates respectively. Keys need to be rotated periodically wherever they are used." diff --git a/prowler/providers/azure/services/keyvault/keyvault_rbac_secret_expiration_set/keyvault_rbac_secret_expiration_set.metadata.json b/prowler/providers/azure/services/keyvault/keyvault_rbac_secret_expiration_set/keyvault_rbac_secret_expiration_set.metadata.json index d679f33441..681668cb63 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_rbac_secret_expiration_set/keyvault_rbac_secret_expiration_set.metadata.json +++ b/prowler/providers/azure/services/keyvault/keyvault_rbac_secret_expiration_set/keyvault_rbac_secret_expiration_set.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "keyvault_rbac_secret_expiration_set", - "CheckTitle": "Ensure that the Expiration Date is set for all Secrets in RBAC Key Vaults", + "CheckTitle": "RBAC-enabled Key Vault has expiration date set for all enabled secrets", "CheckType": [], "ServiceName": "keyvault", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "KeyVault", + "Severity": "medium", + "ResourceType": "microsoft.keyvault/vaults", "ResourceGroup": "security", - "Description": "Ensure that all Secrets in Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.", - "Risk": "The Azure Key Vault enables users to store and keep secrets within the Microsoft Azure environment. Secrets in the Azure Key Vault are octet sequences with a maximum size of 25k bytes each. The exp (expiration date) attribute identifies the expiration date on or after which the secret MUST NOT be used. By default, secrets never expire. It is thus recommended to rotate secrets in the key vault and set an explicit expiration date for all secrets. This ensures that the secrets cannot be used beyond their assigned lifetimes.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis", + "Description": "**Azure Key Vault (RBAC)** secrets are assessed to confirm every **enabled secret** has an `exp` (expiration) date configured", + "Risk": "Without an **expiration**, secrets become perpetual credentials. Leaked or abandoned values can grant persistent access, undermining **confidentiality** and **integrity**. Attackers can reuse old secrets to maintain footholds, perform unauthorized API calls, and exfiltrate data.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/key-vault/general/basic-concepts", + "https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#key-vault-secrets" + ], "Remediation": { "Code": { - "CLI": "az keyvault secret set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z'", - "NativeIaC": "", - "Other": "", - "Terraform": "https://docs.prowler.com/checks/azure/azure-secrets-policies/set-an-expiration-date-on-all-secrets#terraform" + "CLI": "az keyvault secret set-attributes --vault-name --name --expires ", + "NativeIaC": "```bicep\n// Set expiration on a Key Vault secret\nresource secret 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {\n name: '/'\n properties: {\n // CRITICAL: sets the secret expiration timestamp (Unix epoch seconds)\n attributes: {\n exp: 1735689600 // 2025-01-01T00:00:00Z\n }\n value: ''\n }\n}\n```", + "Other": "1. In Azure Portal, go to Key vaults and open your vault\n2. Select Secrets, choose the secret, then open its Current version\n3. Set Expiration date (UTC) and click Save", + "Terraform": "```hcl\nresource \"azurerm_key_vault_secret\" \"\" {\n name = \"\"\n value = \"\"\n key_vault_id = \"\"\n\n # CRITICAL: sets the secret expiration timestamp\n expiration_date = \"2025-01-01T00:00:00Z\"\n}\n```" }, "Recommendation": { - "Text": "From Azure Portal: 1. Go to Key vaults. 2. For each Key vault, click on Secrets. 3. In the main pane, ensure that the status of the secret is Enabled. 4. For each enabled secret, ensure that an appropriate Expiration date is set. From Azure CLI: Update the Expiration date for the secret using the below command: az keyvault secret set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z' Note: To view the expiration date on all secrets in a Key Vault using Microsoft API, the List Key permission is required. To update the expiration date for the secrets: 1. Go to the Key vault, click on Access Control (IAM). 2. Click on Add role assignment and assign the role of Key Vault Secrets Officer to the appropriate user. From PowerShell: Set-AzKeyVaultSecretAttribute -VaultName -Name - Expires ", - "Url": "https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-secrets" + "Text": "Set an **expiration** on all enabled secrets and enforce a **regular rotation policy**.\n\nPrefer **short-lived, identity-based access** to reduce secret usage. Apply **least privilege** for secret access, alert on upcoming expirations, and automate rotation and version cleanup to minimize exposure.", + "Url": "https://hub.prowler.com/check/keyvault_rbac_secret_expiration_set" } }, - "Categories": [], + "Categories": [ + "secrets" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Secrets cannot be used beyond their assigned expiry date respectively. Secrets need to be rotated periodically wherever they are used." diff --git a/prowler/providers/azure/services/keyvault/keyvault_recoverable/keyvault_recoverable.metadata.json b/prowler/providers/azure/services/keyvault/keyvault_recoverable/keyvault_recoverable.metadata.json index 5473403443..1f34db1e7f 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_recoverable/keyvault_recoverable.metadata.json +++ b/prowler/providers/azure/services/keyvault/keyvault_recoverable/keyvault_recoverable.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "keyvault_recoverable", - "CheckTitle": "Ensure the Key Vault is Recoverable", + "CheckTitle": "Key Vault has soft delete and purge protection enabled", "CheckType": [], "ServiceName": "keyvault", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "KeyVault", + "ResourceType": "microsoft.keyvault/vaults", "ResourceGroup": "security", - "Description": "The Key Vault contains object keys, secrets, and certificates. Accidental unavailability of a Key Vault can cause immediate data loss or loss of security functions (authentication, validation, verification, non-repudiation, etc.) supported by the Key Vault objects. It is recommended the Key Vault be made recoverable by enabling the 'Do Not Purge' and 'Soft Delete' functions. This is in order to prevent loss of encrypted data, including storage accounts, SQL databases, and/or dependent services provided by Key Vault objects (Keys, Secrets, Certificates) etc. This may happen in the case of accidental deletion by a user or from disruptive activity by a malicious user. WARNING: A current limitation of the soft-delete feature across all Azure services is role assignments disappearing when Key Vault is deleted. All role assignments will need to be recreated after recovery.", - "Risk": "There could be scenarios where users accidentally run delete/purge commands on Key Vault or an attacker/malicious user deliberately does so in order to cause disruption. Deleting or purging a Key Vault leads to immediate data loss, as keys encrypting data and secrets/certificates allowing access/services will become non-accessible. There are 2 Key Vault properties that play a role in permanent unavailability of a Key Vault: 1. enableSoftDelete: Setting this parameter to 'true' for a Key Vault ensures that even if Key Vault is deleted, Key Vault itself or its objects remain recoverable for the next 90 days. Key Vault/objects can either be recovered or purged (permanent deletion) during those 90 days. If no action is taken, key vault and its objects will subsequently be purged. 2. enablePurgeProtection: enableSoftDelete only ensures that Key Vault is not deleted permanently and will be recoverable for 90 days from date of deletion. However, there are scenarios in which the Key Vault and/or its objects are accidentally purged and hence will not be recoverable. Setting enablePurgeProtection to 'true' ensures that the Key Vault and its objects cannot be purged. Enabling both the parameters on Key Vaults ensures that Key Vaults and their objects cannot be deleted/purged permanently.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-soft-delete-cli", + "Description": "**Azure Key Vault** recoverability requires both `enable_soft_delete` and `enable_purge_protection`. With these enabled, vault objects remain recoverable after deletion and cannot be permanently purged during the retention period.", + "Risk": "Absent these protections, deleted vaults or objects can be permanently removed, cutting access to keys, secrets, and certificates. This can render data unreadable, break app authentication, and halt signing/verification, degrading **availability** and **integrity**. Malicious insiders can purge to block recovery.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/key-vault/general/key-vault-recovery?tabs=azure-cli", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/KeyVault/enable-key-vault-recoverability.html#" + ], "Remediation": { "Code": { - "CLI": "az resource update --id /subscriptions/xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups//providers/Microsoft.KeyVault/vaults/ --set properties.enablePurgeProtection=trueproperties.enableSoftDelete=true", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/KeyVault/enable-key-vault-recoverability.html#", - "Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/ensure-the-key-vault-is-recoverable#terraform" + "CLI": "az keyvault update -g -n --enable-soft-delete true --enable-purge-protection true", + "NativeIaC": "```bicep\n// Enable soft delete and purge protection on an existing/new Key Vault\nresource kv 'Microsoft.KeyVault/vaults@2023-07-01' = {\n name: ''\n location: ''\n properties: {\n tenantId: ''\n sku: { name: 'standard' }\n enableSoftDelete: true // Critical: ensures soft delete is enabled\n enablePurgeProtection: true // Critical: prevents permanent purge during retention\n }\n}\n```", + "Other": "1. In Azure Portal, go to Key vaults and open \n2. Select Properties > Recovery\n3. Turn on Soft delete and Purge protection\n4. Click Save", + "Terraform": "```hcl\nresource \"azurerm_key_vault\" \"\" {\n name = \"\"\n location = \"\"\n resource_group_name = \"\"\n tenant_id = \"\"\n sku_name = \"standard\"\n\n soft_delete_enabled = true # Critical: enables soft delete\n purge_protection_enabled = true # Critical: enables purge protection\n}\n```" }, "Recommendation": { - "Text": "To enable 'Do Not Purge' and 'Soft Delete' for a Key Vault: From Azure Portal 1. Go to Key Vaults 2. For each Key Vault 3. Click Properties 4. Ensure the status of soft-delete reads Soft delete has been enabled on this key vault. 5. At the bottom of the page, click 'Enable Purge Protection' Note, once enabled you cannot disable it. From Azure CLI az resource update --id /subscriptions/xxxxxx-xxxx-xxxx-xxxx- xxxxxxxxxxxx/resourceGroups//providers/Microsoft.KeyVault /vaults/ --set properties.enablePurgeProtection=true properties.enableSoftDelete=true From PowerShell Update-AzKeyVault -VaultName ' --condition category=Administrative and operationName=Microsoft.Authorization/policyAssignments/write and level= --scope '/subscriptions/' --name '' -- subscription --action-group --location global", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/ActivityLog/create-alert-for-create-policy-assignment-events.html#trendmicro", - "Terraform": "" + "CLI": "az monitor activity-log alert create --name '' --resource-group '' --location global --scopes '/subscriptions/' --condition \"category=Administrative and operationName=Microsoft.Authorization/policyAssignments/write\" --enabled true", + "NativeIaC": "```bicep\n// Azure Monitor Activity Log Alert for Policy Assignment creation\nresource activityLogAlert 'Microsoft.Insights/activityLogAlerts@2020-10-01' = {\n name: ''\n location: 'global'\n properties: {\n enabled: true\n scopes: [ '/subscriptions/' ]\n condition: {\n allOf: [\n {\n field: 'category'\n equals: 'Administrative' // Critical: filter Activity Log category to Administrative\n }\n {\n field: 'operationName'\n equals: 'Microsoft.Authorization/policyAssignments/write' // Critical: alert on Policy Assignment creation\n }\n ]\n }\n }\n}\n```", + "Other": "1. In the Azure Portal, go to Monitor > Alerts > Alert rules\n2. Click + Create > Alert rule\n3. Scope: Select the target Subscription and click Apply\n4. Condition: Choose Activity log, then set Category = Administrative and Operation name = Microsoft.Authorization/policyAssignments/write; click Apply\n5. Actions: Skip or select an existing Action group (optional)\n6. Details: Enter a Name and ensure Enable alert rule upon creation is checked\n7. Click Review + create, then Create", + "Terraform": "```hcl\n# Azure Monitor Activity Log Alert for Policy Assignment creation\nresource \"azurerm_monitor_activity_log_alert\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n location = \"global\"\n scopes = [\"/subscriptions/\"]\n\n criteria {\n category = \"Administrative\" # Critical: Activity Log category\n operation_name = \"Microsoft.Authorization/policyAssignments/write\" # Critical: Policy Assignment creation\n }\n}\n```" }, "Recommendation": { - "Text": "1. Navigate to the Monitor blade. 2. Select Alerts. 3. Select Create. 4. Select Alert rule. 5. Under Filter by subscription, choose a subscription. 6. Under Filter by resource type, select Policy assignment (policyAssignments). 7. Under Filter by location, select All. 8. From the results, select the subscription. 9. Select Done. 10. Select the Condition tab. 11. Under Signal name, click Create policy assignment (Microsoft.Authorization/policyAssignments). 12. Select the Actions tab. 13. To use an existing action group, click elect action groups. To create a new action group, click Create action group. Fill out the appropriate details for the selection. 14. Select the Details tab. 15. Select a Resource group, provide an Alert rule name and an optional Alert rule description. 16. Click Review + create. 17. Click Create.", - "Url": "https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log" + "Text": "Implement an **activity log alert** for `Microsoft.Authorization/policyAssignments/write` and route to an action group for timely response.\n\nApply across all subscriptions, restrict assignment rights (**least privilege**), require change approval, and integrate notifications with your SIEM for **defense in depth**.", + "Url": "https://hub.prowler.com/check/monitor_alert_create_policy_assignment" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, no monitoring alerts are created." diff --git a/prowler/providers/azure/services/monitor/monitor_alert_create_update_nsg/monitor_alert_create_update_nsg.metadata.json b/prowler/providers/azure/services/monitor/monitor_alert_create_update_nsg/monitor_alert_create_update_nsg.metadata.json index 16cc7a23a0..ddef49c8c3 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_create_update_nsg/monitor_alert_create_update_nsg.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_alert_create_update_nsg/monitor_alert_create_update_nsg.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "monitor_alert_create_update_nsg", - "CheckTitle": "Ensure that Activity Log Alert exists for Create or Update Network Security Group", + "CheckTitle": "Subscription has an Activity Log alert for Network Security Group create or update operations", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Monitor", + "ResourceType": "microsoft.insights/activitylogalerts", "ResourceGroup": "monitoring", - "Description": "Create an Activity Log Alert for the Create or Update Network Security Group event.", - "Risk": "Monitoring for Create or Update Network Security Group events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", - "RelatedUrl": "https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log", + "Description": "**Azure Monitor Activity Log alert** monitors **Network Security Group** changes via the `Microsoft.Network/networkSecurityGroups/write` operation to capture create/update events across the subscription", + "Risk": "Lack of alerting on NSG changes allows **unauthorized network policy modifications** to go unnoticed. Adversaries or mistakes could open ports, reduce segmentation, and enable **lateral movement**, impacting data **confidentiality** and service **availability** through exposure or disruption of critical traffic", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-in/azure/azure-monitor/alerts/alerts-create-activity-log-alert-rule?tabs=activity-log", + "https://learn.microsoft.com/en-us/azure/azure-monitor/platform/activity-log-schema", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/create-update-network-security-group-rule-alert-in-use.html" + ], "Remediation": { "Code": { - "CLI": "az monitor activity-log alert create --resource-group '' --condition category=Administrative and operationName=Microsoft.Network/networkSecurityGroups/write and level=verbose --scope '/subscriptions/' --name '' --subscription --action-group --location global", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/ActivityLog/create-update-network-security-group-rule-alert-in-use.html#trendmicro", - "Terraform": "" + "CLI": "az monitor activity-log alert create --resource-group '' --name '' --scopes '/subscriptions/' --condition \"category=Administrative and operationName=Microsoft.Network/networkSecurityGroups/write\" --location global", + "NativeIaC": "```bicep\n// Activity Log alert for NSG create/update\nresource alert 'Microsoft.Insights/activityLogAlerts@2020-10-01' = {\n name: ''\n location: 'Global'\n properties: {\n scopes: [ subscription().id ]\n condition: {\n allOf: [\n { field: 'category', equals: 'Administrative' }\n { field: 'operationName', equals: 'Microsoft.Network/networkSecurityGroups/write' } // Critical: triggers on NSG create/update\n ]\n }\n enabled: true // Ensures the alert is active\n }\n}\n```", + "Other": "1. In the Azure portal, go to Monitor > Alerts > Alert rules > Create\n2. Scope: Select your subscription and click Apply\n3. Condition: Choose Activity log, set Category to Administrative, set Operation name to Microsoft.Network/networkSecurityGroups/write, then Done\n4. Actions: Skip (optional)\n5. Details: Name the rule and set Region to Global, ensure Enable upon creation is checked\n6. Review + create > Create", + "Terraform": "```hcl\n# Activity Log alert for NSG create/update\nresource \"azurerm_monitor_activity_log_alert\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n scopes = [\"/subscriptions/\"]\n\n criteria {\n category = \"Administrative\"\n operation_name = \"Microsoft.Network/networkSecurityGroups/write\" # Critical: triggers on NSG create/update\n }\n}\n```" }, "Recommendation": { - "Text": "1. Navigate to the Monitor blade. 2. Select Alerts. 3. Select Create. 4. Select Alert rule. 5. Under Filter by subscription, choose a subscription. 6. Under Filter by resource type, select Network security groups. 7. Under Filter by location, select All. 8. From the results, select the subscription. 9. Select Done. 10. Select the Condition tab. 11. Under Signal name, click Create or Update Network Security Group (Microsoft.Network/networkSecurityGroups). 12. Select the Actions tab. 13. To use an existing action group, click Select action groups. To create a new action group, click Create action group. Fill out the appropriate details for the selection. 14. Select the Details tab. 15. Select a Resource group, provide an Alert rule name and an optional Alert rule description. 16. Click Review + create. 17. Click Create.", - "Url": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement" + "Text": "Implement a subscription-wide **Activity Log alert** for NSG change operations and route notifications to an **action group** for rapid triage.\n\nApply **least privilege** for change tooling, enforce **change management**, and add complementary alerts for `Microsoft.Network/networkSecurityGroups/securityRules/write` and `.../delete`. *Integrate with SIEM for correlation*", + "Url": "https://hub.prowler.com/check/monitor_alert_create_update_nsg" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, no monitoring alerts are created." diff --git a/prowler/providers/azure/services/monitor/monitor_alert_create_update_public_ip_address_rule/monitor_alert_create_update_public_ip_address_rule.metadata.json b/prowler/providers/azure/services/monitor/monitor_alert_create_update_public_ip_address_rule/monitor_alert_create_update_public_ip_address_rule.metadata.json index d26f751076..71d3370f20 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_create_update_public_ip_address_rule/monitor_alert_create_update_public_ip_address_rule.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_alert_create_update_public_ip_address_rule/monitor_alert_create_update_public_ip_address_rule.metadata.json @@ -1,30 +1,39 @@ { "Provider": "azure", "CheckID": "monitor_alert_create_update_public_ip_address_rule", - "CheckTitle": "Ensure that Activity Log Alert exists for Create or Update Public IP Address rule", + "CheckTitle": "Subscription has an Activity Log Alert for Public IP address create or update operations", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Monitor", + "Severity": "medium", + "ResourceType": "microsoft.insights/activitylogalerts", "ResourceGroup": "monitoring", - "Description": "Create an activity log alert for the Create or Update Public IP Addresses rule.", - "Risk": "Monitoring for Create or Update Public IP Address events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", - "RelatedUrl": "https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log", + "Description": "**Azure Monitor activity log alert** for **Public IP addresses** tracks `Microsoft.Network/publicIPAddresses/write` events at the subscription level, covering any creation or update of public IP resources.", + "Risk": "Without this alert, unauthorized or mistaken public IP changes can go unnoticed, exposing workloads to the Internet.\n- Confidentiality: unexpected ingress paths\n- Integrity: shadow endpoints for control\n- Availability: larger DDoS surface and outages", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-in/azure/azure-monitor/alerts/alerts-create-activity-log-alert-rule?tabs=activity-log", + "https://trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/create-or-update-public-ip-alert.html", + "https://support.icompaas.com/support/solutions/articles/62000229918-ensure-that-activity-log-alert-exists-for-create-or-update-public-ip-address-rule", + "https://learn.microsoft.com/en-us/azure/virtual-network/ip-services/monitor-public-ip" + ], "Remediation": { "Code": { - "CLI": "az monitor activity-log alert create --resource-group '' --condition category=Administrative and operationName=Microsoft.Network/publicIPAddresses/write and level=--scope '/subscriptions/' --name '' -- subscription --action-group --location global", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/ActivityLog/create-or-update-public-ip-alert.html#trendmicro", - "Terraform": "" + "CLI": "az monitor activity-log alert create --resource-group --name --scopes /subscriptions/ --condition \"category=Administrative and operationName=Microsoft.Network/publicIPAddresses/write\" --location global", + "NativeIaC": "```bicep\n// Activity Log Alert for Public IP create/update\nresource alert 'Microsoft.Insights/activityLogAlerts@2020-10-01' = {\n name: ''\n location: 'global'\n properties: {\n enabled: true\n scopes: ['/subscriptions/']\n condition: {\n allOf: [\n { field: 'category', equals: 'Administrative' }\n { field: 'operationName', equals: 'Microsoft.Network/publicIPAddresses/write' } // Critical: alerts on Public IP create/update\n ]\n }\n }\n}\n```", + "Other": "1. In Azure Portal, go to Monitor > Alerts > Alert rules > Create\n2. Scope: Select your subscription and click Done\n3. Condition: Choose Activity log, then select the signal \"Create or Update Public Ip Address (publicIPAddresses)\"\n4. Details: Enter an alert rule name; Region: Global; Ensure Enable alert rule upon creation is checked\n5. Click Review + create, then Create", + "Terraform": "```hcl\n# Activity Log Alert for Public IP create/update\nresource \"azurerm_monitor_activity_log_alert\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n scopes = [\"/subscriptions/\"]\n\n criteria {\n category = \"Administrative\"\n operation_name = \"Microsoft.Network/publicIPAddresses/write\" # Critical: alerts on Public IP create/update\n }\n}\n```" }, "Recommendation": { - "Text": "1. Navigate to the Monitor blade. 2. Select Alerts. 3. Select Create. 4. Select Alert rule. 5. Under Filter by subscription, choose a subscription. 6. Under Filter by resource type, select Public IP addresses. 7. Under Filter by location, select All. 8. From the results, select the subscription. 9. Select Done. 10. Select the Condition tab. 11. Under Signal name, click Create or Update Public Ip Address (Microsoft.Network/publicIPAddresses). 12. Select the Actions tab. 13. To use an existing action group, click Select action groups. To create a new action group, click Create action group. Fill out the appropriate details for the selection. 14. Select the Details tab. 15. Select a Resource group, provide an Alert rule name and an optional Alert rule description. 16. Click Review + create. 17. Click Create.", - "Url": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement" + "Text": "Create a subscription-wide **activity log alert** on `Microsoft.Network/publicIPAddresses/write` and route it to an **action group**.\n\nEnforce **least privilege** for IP management, apply **change control**, and use **defense in depth** (private endpoints, bastions, VPN) to minimize public exposure and speed response.", + "Url": "https://hub.prowler.com/check/monitor_alert_create_update_public_ip_address_rule" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, no monitoring alerts are created." diff --git a/prowler/providers/azure/services/monitor/monitor_alert_create_update_security_solution/monitor_alert_create_update_security_solution.metadata.json b/prowler/providers/azure/services/monitor/monitor_alert_create_update_security_solution/monitor_alert_create_update_security_solution.metadata.json index 6fbdea162f..a19896c16b 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_create_update_security_solution/monitor_alert_create_update_security_solution.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_alert_create_update_security_solution/monitor_alert_create_update_security_solution.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "monitor_alert_create_update_security_solution", - "CheckTitle": "Ensure that Activity Log Alert exists for Create or Update Security Solution", + "CheckTitle": "Subscription has Activity Log alert for Security Solution create or update", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Monitor", + "Severity": "medium", + "ResourceType": "microsoft.insights/activitylogalerts", "ResourceGroup": "monitoring", - "Description": "Create an activity log alert for the Create or Update Security Solution event.", - "Risk": "Monitoring for Create or Update Security Solution events gives insight into changes to the active security solutions and may reduce the time it takes to detect suspicious activity.", - "RelatedUrl": "https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log", + "Description": "**Azure Monitor activity log alert** is configured to capture **Security Solutions** create/update operations (`Microsoft.Security/securitySolutions/write`) at subscription scope.", + "Risk": "Without this alert, **unauthorized or mistaken changes** to security tooling can go undetected. Attackers could disable defenses, alter integrations, or weaken policies, eroding the **integrity** of controls, creating blind spots that threaten **confidentiality**, and delaying incident response.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement", + "https://learn.microsoft.com/en-in/azure/azure-monitor/alerts/alerts-create-activity-log-alert-rule?tabs=activity-log", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/create-or-update-security-solution-alert.html#trendmicro" + ], "Remediation": { "Code": { - "CLI": "az monitor activity-log alert create --resource-group '' --condition category=Administrative and operationName=Microsoft.Security/securitySolutions/write and level=--scope '/subscriptions/' --name '' -- subscription --action-group --location global", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/ActivityLog/create-or-update-security-solution-alert.html#trendmicro", - "Terraform": "" + "CLI": "az monitor activity-log alert create --name \"\" --resource-group \"\" --scopes \"/subscriptions/\" --condition \"category=Administrative and operationName=Microsoft.Security/securitySolutions/write\" --location Global", + "NativeIaC": "```bicep\n// Activity Log Alert for Security Solution create/update\nresource activityLogAlert 'Microsoft.Insights/activityLogAlerts@2020-10-01' = {\n name: ''\n location: 'Global'\n properties: {\n scopes: [ '/subscriptions/' ]\n condition: {\n allOf: [\n {\n field: 'category'\n equals: 'Administrative'\n }\n {\n field: 'operationName' // Critical: match Security Solution create/update\n equals: 'Microsoft.Security/securitySolutions/write' // Triggers on this operation\n }\n ]\n }\n enabled: true\n }\n}\n```", + "Other": "1. In the Azure portal, go to Monitor > Alerts > + Create > Alert rule\n2. Scope: Select your Subscription and click Apply\n3. Condition: Choose Activity log, set Signal name to Administrative, then add a filter Operation name = Microsoft.Security/securitySolutions/write\n4. Actions: Skip (no action group required)\n5. Details: Enter a Name, set Region to Global, ensure Enable alert rule upon creation is checked\n6. Review + create > Create", + "Terraform": "```hcl\n# Activity Log Alert for Security Solution create/update\nresource \"azurerm_monitor_activity_log_alert\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n scopes = [\"/subscriptions/\"]\n\n criteria {\n category = \"Administrative\"\n operation_name = \"Microsoft.Security/securitySolutions/write\" # Critical: fires on Security Solution create/update\n }\n}\n```" }, "Recommendation": { - "Text": "1. Navigate to the Monitor blade. 2. Select Alerts. 3. Select Create. 4. Select Alert rule. 5. Under Filter by subscription, choose a subscription. 6. Under Filter by resource type, select Security Solutions (securitySolutions). 7. Under Filter by location, select All. 8. From the results, select the subscription. 9. Select Done. 10. Select the Condition tab. 11. Under Signal name, click Create or Update Security Solutions (Microsoft.Security/securitySolutions). 12. Select the Actions tab. 13. To use an existing action group, click Select action groups. To create a new action group, click Create action group. Fill out the appropriate details for the selection. 14. Select the Details tab. 15. Select a Resource group, provide an Alert rule name and an optional Alert rule description. 16. Click Review + create. 17. Click Create.", - "Url": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement" + "Text": "Configure an **activity log alert** for `Microsoft.Security/securitySolutions/write` and route it to action groups for prompt notification/automation.\n\nApply **least privilege**, require **change control**, and forward alerts to a central SIEM to strengthen **defense in depth**.", + "Url": "https://hub.prowler.com/check/monitor_alert_create_update_security_solution" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, no monitoring alerts are created." diff --git a/prowler/providers/azure/services/monitor/monitor_alert_create_update_sqlserver_fr/monitor_alert_create_update_sqlserver_fr.metadata.json b/prowler/providers/azure/services/monitor/monitor_alert_create_update_sqlserver_fr/monitor_alert_create_update_sqlserver_fr.metadata.json index 11f0d0b459..6458ac7e27 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_create_update_sqlserver_fr/monitor_alert_create_update_sqlserver_fr.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_alert_create_update_sqlserver_fr/monitor_alert_create_update_sqlserver_fr.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "monitor_alert_create_update_sqlserver_fr", - "CheckTitle": "Ensure that Activity Log Alert exists for Create or Update SQL Server Firewall Rule", + "CheckTitle": "Subscription has an Activity Log alert for SQL Server firewall rule create or update events", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Monitor", + "Severity": "medium", + "ResourceType": "microsoft.insights/activitylogalerts", "ResourceGroup": "monitoring", - "Description": "Create an activity log alert for the Create or Update SQL Server Firewall Rule event.", - "Risk": "Monitoring for Create or Update SQL Server Firewall Rule events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", - "RelatedUrl": "https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log", + "Description": "**Azure Monitor activity log alerts** are configured for **Azure SQL Server firewall rule changes**, targeting the `Microsoft.Sql/servers/firewallRules/write` operation.\n\nThis evaluates whether notifications or automated actions are set when firewall rules are created or updated.", + "Risk": "Without alerting on firewall rule changes, unauthorized or accidental openings can remain unnoticed, exposing databases to untrusted networks.\n\nThis harms **confidentiality** (data exfiltration via widened IP ranges) and **integrity** (unauthorized queries), while increasing attacker dwell time.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement", + "https://learn.microsoft.com/en-in/azure/azure-monitor/alerts/alerts-create-activity-log-alert-rule?tabs=activity-log", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/create-or-update-or-delete-sql-server-firewall-rule-alert.html#trendmicro" + ], "Remediation": { "Code": { - "CLI": "az monitor activity-log alert create --resource-group '' --condition category=Administrative and operationName=Microsoft.Sql/servers/firewallRules/write and level=--scope '/subscriptions/' --name '' -- subscription --action-group --location global", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/ActivityLog/create-or-update-or-delete-sql-server-firewall-rule-alert.html#trendmicro", - "Terraform": "" + "CLI": "az monitor activity-log alert create --name --resource-group --scopes /subscriptions/ --condition \"category=Administrative and operationName=Microsoft.Sql/servers/firewallRules/write\" --location global", + "NativeIaC": "```bicep\n// Activity Log alert for SQL Server firewall rule create/update\nresource example_activity_log_alert 'Microsoft.Insights/activityLogAlerts@2020-10-01' = {\n name: ''\n location: 'Global'\n properties: {\n enabled: true\n scopes: [ '/subscriptions/' ]\n condition: {\n allOf: [\n {\n field: 'category'\n equals: 'Administrative'\n }\n {\n field: 'operationName'\n equals: 'Microsoft.Sql/servers/firewallRules/write' // Critical: alert on SQL Server firewall rule create/update\n }\n ]\n }\n }\n}\n```", + "Other": "1. In the Azure portal, go to Monitor > Alerts > + Create > Alert rule\n2. Scope: Select the subscription and click Done\n3. Condition: Choose Signal type \"Activity log\", then set\n - Category: Administrative\n - Operation name: Microsoft.Sql/servers/firewallRules/write\n Click Done\n4. Actions: Skip (no action group required)\n5. Details: Enter an Alert rule name and ensure Enable alert rule upon creation is checked\n6. Review + create > Create", + "Terraform": "```hcl\n# Activity Log alert for SQL Server firewall rule create/update\nresource \"azurerm_monitor_activity_log_alert\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n scopes = [\"/subscriptions/\"]\n\n criteria {\n category = \"Administrative\"\n operation_name = \"Microsoft.Sql/servers/firewallRules/write\" # Critical: alert on SQL Server firewall rule create/update\n }\n}\n```" }, "Recommendation": { - "Text": "1. Navigate to the Monitor blade. 2. Select Alerts. 3. Select Create. 4. Select Alert rule. 5. Under Filter by subscription, choose a subscription. 6. Under Filter by resource type, select Server Firewall Rule (servers/firewallRules). 7. Under Filter by location, select All. 8. From the results, select the subscription. 9. Select Done. 10. Select the Condition tab. 11. Under Signal name, click Create/Update server firewall rule (Microsoft.Sql/servers/firewallRules). 12. Select the Actions tab. 13. To use an existing action group, click Select action groups. To create a new action group, click Create action group. Fill out the appropriate details for the selection. 14. Select the Details tab. 15. Select a Resource group, provide an Alert rule name and an optional Alert rule description. 16. Click Review + create. 17. Click Create.", - "Url": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement" + "Text": "Enable an activity log alert for `Microsoft.Sql/servers/firewallRules/write` and route it to responsive action groups.\n\nApply **least privilege** for firewall management, enforce change approvals, and use **defense in depth**: prefer **private endpoints** and avoid broad public network access.", + "Url": "https://hub.prowler.com/check/monitor_alert_create_update_sqlserver_fr" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, no monitoring alerts are created." diff --git a/prowler/providers/azure/services/monitor/monitor_alert_delete_nsg/monitor_alert_delete_nsg.metadata.json b/prowler/providers/azure/services/monitor/monitor_alert_delete_nsg/monitor_alert_delete_nsg.metadata.json index 955d4682be..bcdd0ddac8 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_delete_nsg/monitor_alert_delete_nsg.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_alert_delete_nsg/monitor_alert_delete_nsg.metadata.json @@ -1,30 +1,38 @@ { "Provider": "azure", "CheckID": "monitor_alert_delete_nsg", - "CheckTitle": "Ensure that Activity Log Alert exists for Delete Network Security Group", + "CheckTitle": "Subscription has an Activity Log alert for Network Security Group delete operations", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Monitor", + "ResourceType": "microsoft.insights/activitylogalerts", "ResourceGroup": "monitoring", - "Description": "Create an activity log alert for the Delete Network Security Group event.", - "Risk": "Monitoring for 'Delete Network Security Group' events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", - "RelatedUrl": "https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log", + "Description": "**Azure Monitor activity log alerts** include the NSG deletion signal (`Microsoft.Network/networkSecurityGroups/delete` or `Microsoft.ClassicNetwork/networkSecurityGroups/delete`). The finding indicates whether a subscription has an alert rule configured to trigger when a Network Security Group is deleted.", + "Risk": "Without alerting on **NSG deletions**, network segmentation can be removed unnoticed, exposing services to broad ingress/egress. Malicious actors or automation may delete NSGs to enable **lateral movement** and **data exfiltration**. Missing alerts delay response, impacting confidentiality and availability.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-in/azure/azure-monitor/alerts/alerts-create-activity-log-alert-rule?tabs=activity-log", + "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/delete-network-security-group-rule-alert-in-use.html#trendmicro" + ], "Remediation": { "Code": { - "CLI": "az monitor activity-log alert create --resource-group '' --condition category=Administrative and operationName=Microsoft.Network/networkSecurityGroups/delete and level=--scope '/subscriptions/' --name '' -- subscription --action-group --location global", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/ActivityLog/delete-network-security-group-rule-alert-in-use.html#trendmicro", - "Terraform": "" + "CLI": "az monitor activity-log alert create --name \"\" --resource-group \"\" --scopes \"/subscriptions/\" --condition \"category=Administrative and operationName=Microsoft.Network/networkSecurityGroups/delete\" --location global", + "NativeIaC": "```bicep\n// Activity Log alert for NSG delete\nresource activityAlert 'Microsoft.Insights/activityLogAlerts@2020-10-01' = {\n name: ''\n location: 'Global'\n properties: {\n scopes: ['/subscriptions/']\n enabled: true\n condition: {\n allOf: [\n { field: 'category', equals: 'Administrative' } // Critical: filter Activity Log to Administrative category\n { field: 'operationName', equals: 'Microsoft.Network/networkSecurityGroups/delete' } // Critical: triggers on NSG delete\n ]\n }\n }\n}\n```", + "Other": "1. In Azure Portal, go to Monitor > Alerts > Alert rules\n2. Click + Create > Alert rule\n3. Scope: Select the target subscription and click Apply\n4. Condition: Choose Activity log, select the signal \"Delete Network Security Group\" (operation Microsoft.Network/networkSecurityGroups/delete); ensure Category is Administrative\n5. Details: Enter a name; leave other settings as default\n6. Click Review + create, then Create", + "Terraform": "```hcl\n# Activity Log alert for NSG delete\nresource \"azurerm_monitor_activity_log_alert\" \"example\" {\n name = \"\"\n resource_group_name = \"\"\n scopes = [\"/subscriptions/\"]\n\n criteria {\n category = \"Administrative\" # Critical: Activity Log category filter\n operation_name = \"Microsoft.Network/networkSecurityGroups/delete\" # Critical: alert on NSG delete\n }\n}\n```" }, "Recommendation": { - "Text": "1. Navigate to the Monitor blade. 2. Select Alerts. 3. Select Create. 4. Select Alert rule. 5. Under Filter by subscription, choose a subscription. 6. Under Filter by resource type, select Network security groups. 7. Under Filter by location, select All. 8. From the results, select the subscription. 9. Select Done. 10. Select the Condition tab. 11. Under Signal name, click Delete Network Security Group (Microsoft.Network/networkSecurityGroups). 12. Select the Actions tab. 13. To use an existing action group, click Select action groups. To create a new action group, click Create action group. Fill out the appropriate details for the selection. 14. Select the Details tab. 15. Select a Resource group, provide an Alert rule name and an optional Alert rule description. 16. Click Review + create. 17. Click Create.", - "Url": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement" + "Text": "Configure a subscription-wide **activity log alert** for the NSG delete operation (`Microsoft.Network/networkSecurityGroups/delete`; include Classic if applicable) and route notifications via **action groups**. Enforce **least privilege** for NSG changes, require **change control**, and integrate with your **SIEM** for correlation.", + "Url": "https://hub.prowler.com/check/monitor_alert_delete_nsg" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, no monitoring alerts are created." diff --git a/prowler/providers/azure/services/monitor/monitor_alert_delete_policy_assignment/monitor_alert_delete_policy_assignment.metadata.json b/prowler/providers/azure/services/monitor/monitor_alert_delete_policy_assignment/monitor_alert_delete_policy_assignment.metadata.json index 113b2f85b5..12c674de4a 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_delete_policy_assignment/monitor_alert_delete_policy_assignment.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_alert_delete_policy_assignment/monitor_alert_delete_policy_assignment.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "monitor_alert_delete_policy_assignment", - "CheckTitle": "Ensure that Activity Log Alert exists for Delete Policy Assignment", + "CheckTitle": "Subscription has an Activity Log alert for policy assignment deletion", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Monitor", + "ResourceType": "microsoft.insights/activitylogalerts", "ResourceGroup": "monitoring", - "Description": "Create an activity log alert for the Delete Policy Assignment event.", - "Risk": "Monitoring for delete policy assignment events gives insight into changes done in 'azure policy - assignments' and can reduce the time it takes to detect unsolicited changes.", - "RelatedUrl": "https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate", + "Description": "**Azure Monitor Activity log alerts** for policy assignment deletions using the `Microsoft.Authorization/policyAssignments/delete` operation at subscription scope", + "Risk": "Without this alert, **policy assignment deletions** can go unnoticed, eroding configuration **integrity** and enabling governance drift. Malicious or accidental changes may remove guardrails, increasing exposure and threatening **confidentiality** of protected resources.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-in/azure/azure-monitor/alerts/alerts-create-activity-log-alert-rule?tabs=activity-log", + "https://learn.microsoft.com/en-in/rest/api/monitor/activity-log-alerts/create-or-update?view=rest-monitor-2020-10-01&tabs=HTTP", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/delete-policy-assignment-alert-in-use.html#trendmicro" + ], "Remediation": { "Code": { - "CLI": "az monitor activity-log alert create --resource-group '' --condition category=Administrative and operationName=Microsoft.Authorization/policyAssignments/delete and level= --scope '/subscriptions/' --name '' -- subscription --action-group --location global", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/ActivityLog/delete-policy-assignment-alert-in-use.html#trendmicro", - "Terraform": "" + "CLI": "az monitor activity-log alert create --resource-group --name --scopes \"/subscriptions/\" --condition \"operationName=Microsoft.Authorization/policyAssignments/delete\" --location global", + "NativeIaC": "```bicep\n// Activity Log alert for Policy Assignment deletion\nresource alert 'Microsoft.Insights/activityLogAlerts@2020-10-01' = {\n name: ''\n location: 'Global'\n properties: {\n scopes: [\n '/subscriptions/'\n ]\n condition: {\n allOf: [\n {\n field: 'operationName'\n equals: 'Microsoft.Authorization/policyAssignments/delete' // CRITICAL: alerts on policy assignment deletion\n }\n ]\n }\n actions: {\n actionGroups: [] // Required property; empty keeps rule minimal\n }\n }\n}\n```", + "Other": "1. In Azure Portal, go to Monitor > Alerts > Alert rules\n2. Click + Create > Alert rule\n3. Scope: Select your subscription and click Apply\n4. Condition: Choose Activity log, then set Operation name equals \"Microsoft.Authorization/policyAssignments/delete\"\n5. Actions: Skip (optional)\n6. Details: Enter a name and set Enable alert rule upon creation\n7. Click Create", + "Terraform": "```hcl\n# Activity Log alert for Policy Assignment deletion\nresource \"azurerm_monitor_activity_log_alert\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n scopes = [\"/subscriptions/\"]\n\n criteria {\n operation_name = \"Microsoft.Authorization/policyAssignments/delete\" # CRITICAL: alerts on policy assignment deletion\n }\n}\n```" }, "Recommendation": { - "Text": "1. Navigate to the Monitor blade. 2. Select Alerts. 3. Select Create. 4. Select Alert rule. 5. Under Filter by subscription, choose a subscription. 6. Under Filter by resource type, select Policy assignment (policyAssignments). 7. Under Filter by location, select All. 8. From the results, select the subscription. 9. Select Done. 10. Select the Condition tab. 11. Under Signal name, click Delete policy assignment (Microsoft.Authorization/policyAssignments). 12. Select the Actions tab. 13. To use an existing action group, click Select action groups. To create a new action group, click Create action group. Fill out the appropriate details for the selection. 14. Select the Details tab. 15. Select a Resource group, provide an Alert rule name and an optional Alert rule description. 16. Click Review + create. 17. Click Create.", - "Url": "https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log" + "Text": "- Configure an activity log alert for `Microsoft.Authorization/policyAssignments/delete` and route to an action group.\n- Enforce **least privilege** and **separation of duties** for policy changes and require approvals.\n- Integrate alerts with your SIEM and define playbooks for rapid response.", + "Url": "https://hub.prowler.com/check/monitor_alert_delete_policy_assignment" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, no monitoring alerts are created." diff --git a/prowler/providers/azure/services/monitor/monitor_alert_delete_public_ip_address_rule/monitor_alert_delete_public_ip_address_rule.metadata.json b/prowler/providers/azure/services/monitor/monitor_alert_delete_public_ip_address_rule/monitor_alert_delete_public_ip_address_rule.metadata.json index 704eb87afa..30187969bf 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_delete_public_ip_address_rule/monitor_alert_delete_public_ip_address_rule.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_alert_delete_public_ip_address_rule/monitor_alert_delete_public_ip_address_rule.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "monitor_alert_delete_public_ip_address_rule", - "CheckTitle": "Ensure that Activity Log Alert exists for Delete Public IP Address rule", + "CheckTitle": "Azure subscription has an Activity Log alert for public IP address deletion", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Monitor", + "Severity": "medium", + "ResourceType": "microsoft.insights/activitylogalerts", "ResourceGroup": "monitoring", - "Description": "Create an activity log alert for the Delete Public IP Address rule.", - "Risk": "Monitoring for Delete Public IP Address events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", - "RelatedUrl": "https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log", + "Description": "**Azure Monitor activity log alert** exists for the **Delete Public IP Address** operation (`Microsoft.Network/publicIPAddresses/delete`), capturing subscription-wide events when Public IP resources are removed.", + "Risk": "Unmonitored deletion of Public IPs can abruptly sever ingress/egress, break DNS and allowlists, and take services offline (**availability**). Attackers or misconfigurations can delete IPs to cause **DoS** or evade controls, and delayed visibility hinders **incident response** and **forensics**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/delete-public-ip-alert.html#trendmicro", + "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement", + "https://learn.microsoft.com/en-in/azure/azure-monitor/alerts/alerts-create-activity-log-alert-rule?tabs=activity-log" + ], "Remediation": { "Code": { - "CLI": "az monitor activity-log alert create --resource-group '' --condition category=Administrative and operationName=Microsoft.Network/publicIPAddresses/delete and level=--scope '/subscriptions/' --name '' -- subscription --action-group --location global", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/ActivityLog/delete-public-ip-alert.html#trendmicro", - "Terraform": "" + "CLI": "az monitor activity-log alert create --name --resource-group --location global --scopes /subscriptions/ --condition category=Administrative and operationName=Microsoft.Network/publicIPAddresses/delete", + "NativeIaC": "```bicep\n// Activity Log alert for Public IP deletion\nresource alert 'Microsoft.Insights/activityLogAlerts@2020-10-01' = {\n name: ''\n location: 'Global'\n properties: {\n enabled: true\n scopes: [\n '/subscriptions/' // Scope the alert to the subscription\n ]\n condition: {\n allOf: [\n { field: 'category', equals: 'Administrative' }\n { field: 'operationName', equals: 'Microsoft.Network/publicIPAddresses/delete' } // Critical: triggers when a Public IP is deleted\n ]\n }\n }\n}\n```", + "Other": "1. In the Azure portal, go to Monitor > Alerts > + Create > Alert rule\n2. Scope: Select your subscription and click Apply\n3. Condition: Choose Activity log, then set Category = Administrative and Operation name = Microsoft.Network/publicIPAddresses/delete; click Apply\n4. Actions: Skip (no action group required to pass)\n5. Details: Enter an alert name, set Region to Global, ensure Enable alert rule upon creation is checked\n6. Review + create > Create", + "Terraform": "```hcl\nresource \"azurerm_monitor_activity_log_alert\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n scopes = [\"/subscriptions/\"]\n\n criteria {\n category = \"Administrative\"\n operation_name = \"Microsoft.Network/publicIPAddresses/delete\" # Critical: alert when a Public IP is deleted\n }\n}\n```" }, "Recommendation": { - "Text": "1. Navigate to the Monitor blade. 2. Select Alerts. 3. Select Create. 4. Select Alert rule. 5. Under Filter by subscription, choose a subscription. 6. Under Filter by resource type, select Public IP addresses. 7. Under Filter by location, select All. 8. From the results, select the subscription. 9. Select Done. 10. Select the Condition tab. 11. Under Signal name, click Delete Public Ip Address (Microsoft.Network/publicIPAddresses). 12. Select the Actions tab. 13. To use an existing action group, click Select action groups. To create a new action group, click Create action group. Fill out the appropriate details for the selection. 14. Select the Details tab. 15. Select a Resource group, provide an Alert rule name and an optional Alert rule description. 16. Click Review + create. 17. Click Create.", - "Url": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement" + "Text": "Implement an activity log alert for `Microsoft.Network/publicIPAddresses/delete` and route it to an action group for rapid response.\n- Apply **least privilege** and change approval for IP deletions\n- Use **resource locks** on critical IPs\n- Centralize alerts in your SIEM and define runbooks for containment", + "Url": "https://hub.prowler.com/check/monitor_alert_delete_public_ip_address_rule" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, no monitoring alerts are created." diff --git a/prowler/providers/azure/services/monitor/monitor_alert_delete_security_solution/monitor_alert_delete_security_solution.metadata.json b/prowler/providers/azure/services/monitor/monitor_alert_delete_security_solution/monitor_alert_delete_security_solution.metadata.json index 97eb0e2df9..34cbc9522a 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_delete_security_solution/monitor_alert_delete_security_solution.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_alert_delete_security_solution/monitor_alert_delete_security_solution.metadata.json @@ -1,30 +1,38 @@ { "Provider": "azure", "CheckID": "monitor_alert_delete_security_solution", - "CheckTitle": "Ensure that Activity Log Alert exists for Delete Security Solution", + "CheckTitle": "Subscription has an Azure Monitor Activity Log alert for Microsoft.Security/securitySolutions delete operations", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Monitor", + "Severity": "medium", + "ResourceType": "microsoft.insights/activitylogalerts", "ResourceGroup": "monitoring", - "Description": "Create an activity log alert for the Delete Security Solution event.", - "Risk": "Monitoring for Delete Security Solution events gives insight into changes to the active security solutions and may reduce the time it takes to detect suspicious activity.", - "RelatedUrl": "https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log", + "Description": "**Azure activity log alerts** monitor deletions of **Security Solutions** by targeting the operation `Microsoft.Security/securitySolutions/delete` at subscription scope.\n\nIdentifies whether notifications are configured for security solution removal events.", + "Risk": "Without this alert, **unauthorized or accidental deletions** of security tooling may go **unnoticed**, reducing the **availability** of protections and the **integrity** of monitoring. Adversaries can evade defenses, prolong dwell time, and enable **data exfiltration** under reduced visibility.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/delete-security-solution-alert.html", + "https://learn.microsoft.com/en-in/azure/azure-monitor/alerts/alerts-create-activity-log-alert-rule?tabs=activity-log", + "https://learn.microsoft.com/en-us/cli/azure/monitor/activity-log/alert?view=azure-cli-latest", + "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement" + ], "Remediation": { "Code": { - "CLI": "az monitor activity-log alert create --resource-group '' --condition category=Administrative and operationName=Microsoft.Security/securitySolutions/delete and level=--scope '/subscriptions/' --name '' -- subscription --action-group --location global", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/ActivityLog/delete-security-solution-alert.html#trendmicro", - "Terraform": "" + "CLI": "az monitor activity-log alert create -g -n --condition operationName=Microsoft.Security/securitySolutions/delete --scope /subscriptions/", + "NativeIaC": "```bicep\n// Activity Log Alert for Security Solution delete\nresource alert 'Microsoft.Insights/activityLogAlerts@2020-10-01' = {\n name: ''\n location: 'global'\n properties: {\n enabled: true\n scopes: [ subscription().id ]\n condition: {\n allOf: [\n {\n field: 'operationName'\n equals: 'Microsoft.Security/securitySolutions/delete' // Critical: alerts on Security Solution delete\n }\n ]\n }\n }\n}\n```", + "Other": "1. In Azure portal, go to Monitor > Alerts > + Create > Alert rule\n2. Scope: Select your subscription and click Apply\n3. Condition: Click Add condition, search and select \"Delete Security Solutions (Microsoft.Security/securitySolutions)\", then Add\n4. Ensure no filters for Level or Status are set\n5. Details: Enter an Alert rule name and choose a resource group\n6. Create: Review + create, then Create", + "Terraform": "```hcl\n# Activity Log Alert for Security Solution delete\nresource \"azurerm_monitor_activity_log_alert\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n scopes = [\"/subscriptions/\"]\n\n criteria {\n operation_name = \"Microsoft.Security/securitySolutions/delete\" # Critical: alerts on delete operation\n }\n}\n```" }, "Recommendation": { - "Text": "1. Navigate to the Monitor blade. 2. Select Alerts. 3. Select Create. 4. Select Alert rule. 5. Under Filter by subscription, choose a subscription. 6. Under Filter by resource type, select Security Solutions (securitySolutions). 7. Under Filter by location, select All. 8. From the results, select the subscription. 9. Select Done. 10. Select the Condition tab. 11. Under Signal name, click Delete Security Solutions (Microsoft.Security/securitySolutions). 12. Select the Actions tab. 13. To use an existing action group, click Select action groups. To create a new action group, click Create action group. Fill out the appropriate details for the selection. 14. Select the Details tab. 15. Select a Resource group, provide an Alert rule name and an optional Alert rule description. 16. Click Review + create. 17. Click Create.curitySolutions). 7. Under Filter by location, select All. 8. From the results, select the subscription. 9. Select Done. 10. Select the Condition tab. 11. Under Signal name, click Create or Update Security Solutions (Microsoft.Security/securitySolutions). 12. Select the Actions tab. 13. To use an existing action group, click Select action groups. To create a new action group, click Create action group. Fill out the appropriate details for the selection. 14. Select the Details tab. 15. Select a Resource group, provide an Alert rule name and an optional Alert rule description. 16. Click Review + create. 17. Click Create.", - "Url": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement" + "Text": "Configure a **dedicated activity log alert** for `Microsoft.Security/securitySolutions/delete` and route it to resilient **action groups** (email, chat, ticketing, SIEM). Apply **least privilege** and **resource locks** to deter tampering. Test alerting routinely and integrate it into **defense-in-depth** monitoring.", + "Url": "https://hub.prowler.com/check/monitor_alert_delete_security_solution" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, no monitoring alerts are created." diff --git a/prowler/providers/azure/services/monitor/monitor_alert_delete_sqlserver_fr/monitor_alert_delete_sqlserver_fr.metadata.json b/prowler/providers/azure/services/monitor/monitor_alert_delete_sqlserver_fr/monitor_alert_delete_sqlserver_fr.metadata.json index b810934718..9a6debf62e 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_delete_sqlserver_fr/monitor_alert_delete_sqlserver_fr.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_alert_delete_sqlserver_fr/monitor_alert_delete_sqlserver_fr.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "monitor_alert_delete_sqlserver_fr", - "CheckTitle": "Ensure that Activity Log Alert exists for Delete SQL Server Firewall Rule", + "CheckTitle": "Subscription has an Activity Log Alert for SQL Server firewall rule deletions", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Monitor", + "Severity": "medium", + "ResourceType": "microsoft.insights/activitylogalerts", "ResourceGroup": "monitoring", - "Description": "Create an activity log alert for the 'Delete SQL Server Firewall Rule.'", - "Risk": "Monitoring for Delete SQL Server Firewall Rule events gives insight into SQL network access changes and may reduce the time it takes to detect suspicious activity.", - "RelatedUrl": "https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log", + "Description": "**Azure Monitor Activity log alerts** watch the admin operation `Microsoft.Sql/servers/firewallRules/delete`, indicating when an **Azure SQL firewall rule** is removed across a subscription.", + "Risk": "Without alerting on firewall rule deletions, unexpected changes to SQL network allowlists can go unnoticed, causing **availability** loss for apps and masking **unauthorized tampering**. A compromised admin could remove rules to disrupt service, erode control **integrity**, and delay response.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement", + "https://learn.microsoft.com/en-in/azure/azure-monitor/alerts/alerts-create-activity-log-alert-rule?tabs=activity-log", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/create-or-update-or-delete-sql-server-firewall-rule-alert.html#trendmicro" + ], "Remediation": { "Code": { - "CLI": "az monitor activity-log alert create --resource-group '' --condition category=Administrative and operationName=Microsoft.Sql/servers/firewallRules/delete and level=--scope '/subscriptions/' --name '' -- subscription --action-group --location global", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/ActivityLog/create-or-update-or-delete-sql-server-firewall-rule-alert.html#trendmicro", - "Terraform": "" + "CLI": "az monitor activity-log alert create --resource-group --name --scopes /subscriptions/ --condition \"category=Administrative and operationName=Microsoft.Sql/servers/firewallRules/delete\" --location global", + "NativeIaC": "```bicep\n// Activity Log Alert for SQL Server firewall rule deletions\nresource activityLogAlert 'Microsoft.Insights/activityLogAlerts@2020-10-01' = {\n name: ''\n location: 'Global'\n properties: {\n scopes: [\n '/subscriptions/'\n ]\n enabled: true\n condition: {\n allOf: [\n {\n field: 'category' // Critical: filter Activity Log category\n equals: 'Administrative' // Ensures Administrative events are matched\n }\n {\n field: 'operationName' // Critical: target deletion of SQL Server firewall rules\n equals: 'Microsoft.Sql/servers/firewallRules/delete' // This makes the check PASS\n }\n ]\n }\n }\n}\n```", + "Other": "1. In the Azure Portal, go to Monitor > Alerts > + Create > Alert rule\n2. Scope: Select the target Subscription and click Done\n3. Condition: Click Add condition, choose Signal type = Activity log, search for and select the operation with type \"Microsoft.Sql/servers/firewallRules/delete\" (display name like \"Delete Server Firewall Rule\"), then Click Apply\n4. Actions: Skip (optional)\n5. Details: Enter an Alert rule name and ensure Enable upon creation is selected\n6. Click Create", + "Terraform": "```hcl\n# Activity Log Alert for SQL Server firewall rule deletions\nresource \"azurerm_monitor_activity_log_alert\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n scopes = [\"/subscriptions/\"]\n\n criteria {\n category = \"Administrative\" # Critical: filter Activity Log category\n operation_name = \"Microsoft.Sql/servers/firewallRules/delete\" # Critical: match deletion of SQL Server firewall rules\n }\n}\n```" }, "Recommendation": { - "Text": "1. Navigate to the Monitor blade. 2. Select Alerts. 3. Select Create. 4. Select Alert rule. 5. Under Filter by subscription, choose a subscription. 6. Under Filter by resource type, select Server Firewall Rule (servers/firewallRules). 7. Under Filter by location, select All. 8. From the results, select the subscription. 9. Select Done. 10. Select the Condition tab. 11. Under Signal name, click Delete server firewall rule (Microsoft.Sql/servers/firewallRules). 12. Select the Actions tab. 13. To use an existing action group, click Select action groups. To create a new action group, click Create action group. Fill out the appropriate details for the selection. 14. Select the Details tab. 15. Select a Resource group, provide an Alert rule name and an optional Alert rule description. 16. Click Review + create. 17. Click Create.", - "Url": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement" + "Text": "Create an **activity log alert** for `Microsoft.Sql/servers/firewallRules/delete` and route it via an **action group** for rapid triage.\n\nEnforce **least privilege** and **separation of duties** on SQL admins, add alerts for related create/update operations, integrate with **SIEM**, and require *change approval* to strengthen defense in depth.", + "Url": "https://hub.prowler.com/check/monitor_alert_delete_sqlserver_fr" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, no monitoring alerts are created." diff --git a/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.metadata.json b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.metadata.json index 87b5afe627..9f96bcc322 100644 --- a/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.metadata.json @@ -1,30 +1,39 @@ { "Provider": "azure", "CheckID": "monitor_alert_service_health_exists", - "CheckTitle": "Ensure that an Activity Log Alert exists for Service Health", + "CheckTitle": "Azure subscription has an enabled Activity Log alert for Service Health incidents", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Monitor", + "Severity": "medium", + "ResourceType": "microsoft.insights/activitylogalerts", "ResourceGroup": "monitoring", - "Description": "Ensure that an Azure activity log alert is configured to trigger when Service Health events occur within your Microsoft Azure cloud account. The alert should activate when new events match the specified conditions in the alert rule configuration.", - "Risk": "Lack of monitoring for Service Health events may result in missing critical service issues, planned maintenance, security advisories, or other changes that could impact Azure services and regions in use.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/service-health/overview", + "Description": "**Azure Monitor Activity Log alert** is configured for **Service Health** notifications where `category` is `ServiceHealth` and `properties.incidentType` is `Incident`, with the rule enabled.", + "Risk": "Without alerts for **Service Health incidents**, teams may miss Azure outages or degradations, harming **availability** and delaying failover. Unseen incidents can cause cascading errors, timeouts, deployment failures, and SLA breaches across dependent workloads.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/service-health/service-health-notifications-properties", + "https://learn.microsoft.com/en-us/azure/service-health/alerts-activity-log-service-notifications-portal", + "https://learn.microsoft.com/en-us/azure/service-health/overview", + "https://learn.microsoft.com/en-us/azure/azure-monitor/platform/activity-log-schema", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/service-health-alert.html" + ], "Remediation": { "Code": { - "CLI": "az monitor activity-log alert create --subscription --resource-group --name --condition category=ServiceHealth and properties.incidentType=Incident --scope /subscriptions/ --action-group ", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/service-health-alert.html", - "Terraform": "" + "CLI": "az monitor activity-log alert create --resource-group --name --scopes /subscriptions/ --condition \"category=ServiceHealth and properties.incidentType=Incident\"", + "NativeIaC": "```bicep\n// Activity Log Alert for Service Health Incidents\nresource alert 'Microsoft.Insights/activityLogAlerts@2020-10-01' = {\n name: ''\n location: 'Global'\n properties: {\n enabled: true\n scopes: [ subscription().id ]\n condition: {\n allOf: [\n { field: 'category', equals: 'ServiceHealth' } // Critical: match Service Health category\n { field: 'properties.incidentType', equals: 'Incident' } // Critical: alert only on Incident events\n ]\n }\n }\n}\n```", + "Other": "1. In the Azure portal, go to Service Health > Health alerts > Create service health alert\n2. Scope: select your Subscription and choose the Resource group to save the alert\n3. Event types: select only Service issues (Incidents)\n4. Leave other filters as default, ensure Enable rule is On, then click Create", + "Terraform": "```hcl\n# Activity Log Alert for Service Health Incidents\nresource \"azurerm_monitor_activity_log_alert\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n scopes = [\"/subscriptions/\"]\n\n criteria {\n category = \"ServiceHealth\" # Critical: Service Health category\n service_health {\n events = [\"Incident\"] # Critical: alert only on Incident type\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Create an activity log alert for Service Health events and configure an action group to notify appropriate personnel.", - "Url": "https://learn.microsoft.com/en-us/azure/service-health/alerts-activity-log-service-notifications-portal" + "Text": "Create and maintain an enabled **Activity Log alert** for **Service Health Incident** events.\n- Route via **Action Groups** to on-call channels\n- Filter to critical services/regions\n- Test routing and refine recipients regularly\n- Integrate with **incident response** and **defense-in-depth** monitoring", + "Url": "https://hub.prowler.com/check/monitor_alert_service_health_exists" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, in your Azure subscription there will not be any activity log alerts configured for Service Health events." diff --git a/prowler/providers/azure/services/monitor/monitor_diagnostic_setting_with_appropriate_categories/monitor_diagnostic_setting_with_appropriate_categories.metadata.json b/prowler/providers/azure/services/monitor/monitor_diagnostic_setting_with_appropriate_categories/monitor_diagnostic_setting_with_appropriate_categories.metadata.json index e577e0925a..af122f3206 100644 --- a/prowler/providers/azure/services/monitor/monitor_diagnostic_setting_with_appropriate_categories/monitor_diagnostic_setting_with_appropriate_categories.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_diagnostic_setting_with_appropriate_categories/monitor_diagnostic_setting_with_appropriate_categories.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "monitor_diagnostic_setting_with_appropriate_categories", - "CheckTitle": "Ensure Diagnostic Setting captures appropriate categories", + "CheckTitle": "Subscription has a diagnostic setting capturing Administrative, Security, Alert, and Policy categories", "CheckType": [], "ServiceName": "monitor", - "SubServiceName": "Configuring Diagnostic Settings", + "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "Monitor", + "Severity": "high", + "ResourceType": "microsoft.resources/subscriptions", "ResourceGroup": "monitoring", - "Description": "Prerequisite: A Diagnostic Setting must exist. If a Diagnostic Setting does not exist, the navigation and options within this recommendation will not be available. Please review the recommendation at the beginning of this subsection titled: 'Ensure that a 'Diagnostic Setting' exists.' The diagnostic setting should be configured to log the appropriate activities from the control/management plane.", - "Risk": "A diagnostic setting controls how the diagnostic log is exported. Capturing the diagnostic setting categories for appropriate control/management plane activities allows proper alerting.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/diagnostic-settings", + "Description": "**Azure Monitor Diagnostic Settings** capture **control-plane events** at the subscription level. This evaluates whether at least one setting collects the categories: `Administrative`, `Security`, `Policy`, and `Alert`.", + "Risk": "Without these categories, critical control-plane actions may go unrecorded. Attackers could change policies, roles, or alerts unnoticed, enabling privilege escalation and resource tampering. This erodes **integrity**, threatens **confidentiality**, and weakens **availability** and **incident response**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/diagnostic-settings", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Monitor/diagnostic-setting-categories.html", + "https://learn.microsoft.com/en-us/azure/storage/common/manage-storage-analytics-logs?toc=%2Fazure%2Fstorage%2Fblobs%2Ftoc.json&bc=%2Fazure%2Fstorage%2Fblobs%2Fbreadcrumb%2Ftoc.json&tabs=azure-portal" + ], "Remediation": { "Code": { - "CLI": "az monitor diagnostic-settings subscription create --subscription --name --location <[- -event-hub --event-hub-auth-rule ] [-- storage-account ] [--workspace ] --logs '[{category:Security,enabled:true},{category:Administrative,enabled:true},{ca tegory:Alert,enabled:true},{category:Policy,enabled:true}]'>", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Monitor/diagnostic-setting-categories.html", - "Terraform": "" + "CLI": "az monitor diagnostic-settings subscription create --name --workspace --logs '[{\"category\":\"Administrative\",\"enabled\":true},{\"category\":\"Security\",\"enabled\":true},{\"category\":\"Alert\",\"enabled\":true},{\"category\":\"Policy\",\"enabled\":true}]'", + "NativeIaC": "```bicep\n// Create a subscription-level diagnostic setting capturing required categories\ntargetScope = 'subscription'\n\nresource diag 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {\n name: ''\n properties: {\n workspaceId: '' // Critical: send Activity Log to this Log Analytics workspace\n logs: [\n { category: 'Administrative', enabled: true } // Critical: required category\n { category: 'Security', enabled: true } // Critical: required category\n { category: 'Alert', enabled: true } // Critical: required category\n { category: 'Policy', enabled: true } // Critical: required category\n ]\n }\n}\n```", + "Other": "1. In Azure portal, go to Monitor > Activity log\n2. Click Diagnostic settings > Add diagnostic setting\n3. Name the setting\n4. Under Categories, check: Administrative, Security, Alert, Policy\n5. Under Destination, select Send to Log Analytics workspace and choose your workspace\n6. Click Save", + "Terraform": "```hcl\n# Subscription Activity Log diagnostic setting capturing required categories\nresource \"azurerm_monitor_diagnostic_setting\" \"example\" {\n name = \"\"\n target_resource_id = \"/subscriptions/\" # Critical: scope set to the subscription\n log_analytics_workspace_id = \"\" # Critical: destination workspace\n\n enabled_log { category = \"Administrative\" } # Critical: required category\n enabled_log { category = \"Security\" } # Critical: required category\n enabled_log { category = \"Alert\" } # Critical: required category\n enabled_log { category = \"Policy\" } # Critical: required category\n}\n```" }, "Recommendation": { - "Text": "1. Go to Azure Monitor 2. Click Activity log 3. Click on Export Activity Logs 4. Select the Subscription from the drop down menu 5. Click on Add diagnostic setting 6. Enter a name for your new Diagnostic Setting 7. Check the following categories: Administrative, Alert, Policy, and Security 8. Choose the destination details according to your organization's needs.", - "Url": "https://learn.microsoft.com/en-us/azure/storage/common/manage-storage-analytics-logs?toc=%2Fazure%2Fstorage%2Fblobs%2Ftoc.json&bc=%2Fazure%2Fstorage%2Fblobs%2Fbreadcrumb%2Ftoc.json&tabs=azure-portal" + "Text": "Collect `Administrative`, `Security`, `Policy`, and `Alert` via a subscription diagnostic setting and route them to a centralized, tamper-resistant destination. Enforce **least privilege** on log access, set retention, and create **alerts** for high-risk changes as part of **defense in depth**.", + "Url": "https://hub.prowler.com/check/monitor_diagnostic_setting_with_appropriate_categories" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "When the diagnostic setting is created using Azure Portal, by default no categories are selected." diff --git a/prowler/providers/azure/services/monitor/monitor_diagnostic_settings_exists/monitor_diagnostic_settings_exists.metadata.json b/prowler/providers/azure/services/monitor/monitor_diagnostic_settings_exists/monitor_diagnostic_settings_exists.metadata.json index 1e9ac7891c..21462248d7 100644 --- a/prowler/providers/azure/services/monitor/monitor_diagnostic_settings_exists/monitor_diagnostic_settings_exists.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_diagnostic_settings_exists/monitor_diagnostic_settings_exists.metadata.json @@ -1,30 +1,38 @@ { "Provider": "azure", "CheckID": "monitor_diagnostic_settings_exists", - "CheckTitle": "Ensure that a 'Diagnostic Setting' exists for Subscription Activity Logs ", + "CheckTitle": "Subscription has an Activity Log diagnostic setting", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "Monitor", + "Severity": "high", + "ResourceType": "microsoft.resources/subscriptions", "ResourceGroup": "monitoring", - "Description": "Enable Diagnostic settings for exporting activity logs. Diagnostic settings are available for each individual resource within a subscription. Settings should be configured for all appropriate resources for your environment.", - "Risk": "A diagnostic setting controls how a diagnostic log is exported. By default, logs are retained only for 90 days. Diagnostic settings should be defined so that logs can be exported and stored for a longer duration in order to analyze security activities within an Azure subscription.", - "RelatedUrl": "https://learn.microsoft.com/en-us/cli/azure/monitor/diagnostic-settings?view=azure-cli-latest", + "Description": "**Azure Monitor Diagnostic Settings** are configured to export the **Activity Log** to an external destination (Log Analytics, Storage, Event Hub, or partner).", + "Risk": "Without exporting the **Activity Log**, control-plane events lack **centralization and retention**.\n\nUndetected RBAC changes, policy updates, and resource deletions reduce **detectability**, hinder **forensics**, and weaken incident response and audit evidence.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/diagnostic-settings?WT.mc_id=AZ-MVP-5003450&tabs=portal", + "https://learn.microsoft.com/en-us/azure/azure-monitor/fundamentals/data-sources#export-the-activity-log-with-a-log-profile", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Monitor/subscription-activity-log-diagnostic-settings.html", + "https://learn.microsoft.com/en-us/cli/azure/monitor/diagnostic-settings?view=azure-cli-latest" + ], "Remediation": { "Code": { - "CLI": "az monitor diagnostic-settings subscription create --subscription --name --location <[- -event-hub --event-hub-auth-rule ] [-- storage-account ] [--workspace ] --logs '' (e.g. [{category:Security,enabled:true},{category:Administrative,enabled:true},{cat egory:Alert,enabled:true},{category:Policy,enabled:true}])", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/Monitor/subscription-activity-log-diagnostic-settings.html#trendmicro", - "Terraform": "" + "CLI": "az monitor diagnostic-settings subscription create --subscription --name --workspace --logs '[{\"category\":\"Administrative\",\"enabled\":true}]'", + "NativeIaC": "```bicep\n// Subscription-level Activity Log diagnostic setting\nresource diag 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {\n name: ''\n scope: subscription() // CRITICAL: targets the subscription Activity Log\n properties: {\n workspaceId: '' // CRITICAL: sends logs to this Log Analytics workspace\n logs: [\n { category: 'Administrative', enabled: true } // CRITICAL: enables at least one Activity Log category\n ]\n }\n}\n```", + "Other": "1. In the Azure portal, go to Subscriptions and select your subscription\n2. Open Monitoring > Activity log, then click Diagnostic settings\n3. Click + Add diagnostic setting and enter a name\n4. Under Destination details, select Send to Log Analytics workspace and choose your workspace\n5. Under Categories, select Administrative\n6. Click Save", + "Terraform": "```hcl\n# Subscription-level Activity Log diagnostic setting\nresource \"azurerm_monitor_diagnostic_setting\" \"\" {\n name = \"\"\n target_resource_id = \"/subscriptions/\" # CRITICAL: subscription scope\n log_analytics_workspace_id = \"\" # CRITICAL: destination workspace\n\n log {\n category = \"Administrative\" # CRITICAL: enable at least one Activity Log category\n enabled = true\n }\n}\n```" }, "Recommendation": { - "Text": "To enable Diagnostic Settings on a Subscription: 1. Go to Monitor 2. Click on Activity Log 3. Click on Export Activity Logs 4. Click + Add diagnostic setting 5. Enter a Diagnostic setting name 6. Select Categories for the diagnostic settings 7. Select the appropriate Destination details (this may be Log Analytics, Storage Account, Event Hub, or Partner solution) 8. Click Save To enable Diagnostic Settings on a specific resource: 1. Go to Monitor 2. Click Diagnostic settings 3. Click on the resource that has a diagnostics status of disabled 4. Select Add Diagnostic Setting 5. Enter a Diagnostic setting name 6. Select the appropriate log, metric, and destination. (this may be Log Analytics, Storage Account, Event Hub, or Partner solution) 7. Click save Repeat these step for all resources as needed.", - "Url": "https://docs.microsoft.com/en-us/azure/monitoring-and-diagnostics/monitoring-overview-activity-logs#export-the-activity-log-with-a-log-profile" + "Text": "Enable **subscription Diagnostic Settings** to send the **Activity Log** to a trusted destination.\n\nUse **immutable storage** or a **SIEM**, enforce coverage with **Azure Policy**, apply **least privilege** to log access, include essential categories, and set retention aligned to regulatory needs.", + "Url": "https://hub.prowler.com/check/monitor_diagnostic_settings_exists" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, diagnostic setting is not set." diff --git a/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_cmk_encrypted/monitor_storage_account_with_activity_logs_cmk_encrypted.metadata.json b/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_cmk_encrypted/monitor_storage_account_with_activity_logs_cmk_encrypted.metadata.json index 3ea864bc52..f5fd1d23b3 100644 --- a/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_cmk_encrypted/monitor_storage_account_with_activity_logs_cmk_encrypted.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_cmk_encrypted/monitor_storage_account_with_activity_logs_cmk_encrypted.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "monitor_storage_account_with_activity_logs_cmk_encrypted", - "CheckTitle": "Ensure the storage account containing the container with activity logs is encrypted with Customer Managed Key", + "CheckTitle": "Storage account storing Activity Log data is encrypted with a customer-managed key", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Monitor", + "ResourceType": "microsoft.storage/storageaccounts", "ResourceGroup": "monitoring", - "Description": "Storage accounts with the activity log exports can be configured to use CustomerManaged Keys (CMK).", - "Risk": "Configuring the storage account with the activity log export container to use CMKs provides additional confidentiality controls on log data, as a given user must have read permission on the corresponding storage account and must be granted decrypt permission by the CMK.", - "RelatedUrl": "https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-5-encrypt-sensitive-data-at-rest", + "Description": "**Azure Monitor Activity Logs** sent to a **Storage account** are evaluated to confirm encryption with **Customer-Managed Keys** (`CMK`) instead of **Microsoft-managed keys**.", + "Risk": "Storing activity logs without **CMK** weakens confidentiality and control of audit data. You lose independent key ownership, limiting rapid rotation/revocation and separation of duties. If storage credentials are compromised, attackers can exfiltrate logs that map resources and changes, aiding targeted attacks and hindering effective incident response.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/activity-log?tabs=cli#managing-legacy-log-profiles", + "https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-5-encrypt-sensitive-data-at-rest", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Monitor/use-cmk-for-activity-log-storage-container-encryption.html" + ], "Remediation": { "Code": { - "CLI": "az storage account update --name --resource-group --encryption-key-source=Microsoft.Keyvault --encryption-key-vault --encryption-key-name --encryption-key-version ", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/Monitor/use-cmk-for-activity-log-storage-container-encryption.html", - "Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/ensure-that-storage-accounts-use-customer-managed-key-for-encryption#terraform" + "CLI": "az storage account update --name --resource-group --assign-identity --encryption-key-source Microsoft.Keyvault --encryption-key-vault --encryption-key-name ", + "NativeIaC": "```bicep\n// Storage account encrypted with a customer-managed key (CMK)\nresource stg 'Microsoft.Storage/storageAccounts@2023-01-01' = {\n name: ''\n location: resourceGroup().location\n kind: 'StorageV2'\n sku: { name: 'Standard_LRS' }\n identity: { type: 'SystemAssigned' } // Required for Storage to access the Key Vault key\n properties: {\n encryption: {\n keySource: 'Microsoft.Keyvault' // CRITICAL: switches encryption from Microsoft.Storage to CMK\n keyVaultProperties: {\n keyName: ''\n keyVaultUri: '' // Uses latest key version if not specified\n }\n }\n }\n}\n```", + "Other": "1. In the Azure portal, go to Storage accounts and open the account used by your Activity Log diagnostic setting\n2. Select Identity > System assigned > set Status to On > Save\n3. Go to Settings > Encryption\n4. Select Customer-managed keys, choose your Key vault and Key, then click Save\n5. Ensure the storage account's identity has Get, Wrap Key, and Unwrap Key permissions on the key in Key Vault", + "Terraform": "```hcl\n# Storage account encrypted with a customer-managed key (CMK)\nresource \"azurerm_storage_account\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n location = \"\"\n account_tier = \"Standard\"\n account_replication_type = \"LRS\"\n\n identity {\n type = \"SystemAssigned\" # Required for Storage to access the Key Vault key\n }\n\n customer_managed_key {\n key_vault_key_id = \"\" # CRITICAL: enables CMK by pointing to the Key Vault key\n }\n}\n```" }, "Recommendation": { - "Text": "1. Go to Activity log 2. Select Export 3. Select Subscription 4. In section Storage Account, note the name of the Storage account 5. Close the Export Audit Logs blade. Close the Monitor - Activity Log blade. 6. In right column, Click service Storage Accounts to access Storage account blade 7. Click on the storage account name noted in step 4. This will open blade specific to that storage account 8. Under Security + networking, click Encryption. 9. Ensure Customer-managed keys is selected and Key URI is set.", - "Url": "https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/activity-log?tabs=cli#managing-legacy-log-profiles" + "Text": "Encrypt the storage account that holds exported **Activity Logs** with **Customer-Managed Keys** via Azure Key Vault or Managed HSM. Apply **least privilege** to key usage, enforce regular rotation and revocation, and enable soft delete and purge protection. Complement with network isolation and immutable retention for **defense in depth**.", + "Url": "https://hub.prowler.com/check/monitor_storage_account_with_activity_logs_cmk_encrypted" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "NOTE: You must have your key vault setup to utilize this. All Audit Logs will be encrypted with a key you provide. You will need to set up customer managed keys separately, and you will select which key to use via the instructions here. You will be responsible for the lifecycle of the keys, and will need to manually replace them at your own determined intervals to keep the data secure." diff --git a/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_is_private/monitor_storage_account_with_activity_logs_is_private.metadata.json b/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_is_private/monitor_storage_account_with_activity_logs_is_private.metadata.json index e91472c240..6d44bef7d5 100644 --- a/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_is_private/monitor_storage_account_with_activity_logs_is_private.metadata.json +++ b/prowler/providers/azure/services/monitor/monitor_storage_account_with_activity_logs_is_private/monitor_storage_account_with_activity_logs_is_private.metadata.json @@ -1,30 +1,38 @@ { "Provider": "azure", "CheckID": "monitor_storage_account_with_activity_logs_is_private", - "CheckTitle": "Ensure the Storage Container Storing the Activity Logs is not Publicly Accessible", + "CheckTitle": "Storage account storing activity logs does not allow public blob access", "CheckType": [], "ServiceName": "monitor", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Monitor", + "ResourceType": "microsoft.storage/storageaccounts", "ResourceGroup": "monitoring", - "Description": "The storage account container containing the activity log export should not be publicly accessible.", - "Risk": "Allowing public access to activity log content may aid an adversary in identifying weaknesses in the affected account's use or configuration.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/diagnostic-settings", + "Description": "**Azure Monitor Activity Logs** sent to a **Storage account** are evaluated for **Blob public access**. The finding identifies whether the account that stores the logs has `AllowBlobPublicAccess` turned on.", + "Risk": "Exposed log data undermines **confidentiality** by revealing operations, resource IDs, IPs, and identities.\n\nAdversaries gain **reconnaissance** to map controls, craft targeted attacks, and time actions to avoid detection, enabling **lateral movement** and broader compromise.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/diagnostic-settings", + "https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-network-security#ns-2-secure-cloud-services-with-network-controls", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/Monitor/check-for-publicly-accessible-activity-log-storage-container.html" + ], "Remediation": { "Code": { - "CLI": "az storage container set-permission --name insights-activity-logs --account-name --public-access off", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/Monitor/check-for-publicly-accessible-activity-log-storage-container.html", - "Terraform": "https://docs.prowler.com/checks/azure/azure-logging-policies/ensure-the-storage-container-storing-the-activity-logs-is-not-publicly-accessible#terraform" + "CLI": "az storage account update --name --resource-group --allow-blob-public-access false", + "NativeIaC": "```bicep\n// Set storage account to disallow public blob access\nresource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {\n name: ''\n location: resourceGroup().location\n sku: { name: 'Standard_LRS' }\n kind: 'StorageV2'\n properties: {\n allowBlobPublicAccess: false // Critical: disables public access at the account level\n }\n}\n```", + "Other": "1. In Azure Portal, go to the storage account used by the diagnostic/Activity Log export\n2. Under Settings, select Configuration\n3. Set \"Allow Blob public access\" to Disabled\n4. Click Save", + "Terraform": "```hcl\n# Disable public blob access on the storage account\nresource \"azurerm_storage_account\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n location = \"\"\n account_tier = \"Standard\"\n account_replication_type = \"LRS\"\n\n allow_blob_public_access = false # Critical: disables public access at the account level\n}\n```" }, "Recommendation": { - "Text": "1. From Azure Home select the Portal Menu 2. Search for Storage Accounts to access Storage account blade 3. Click on the storage account name 4. Click on Configuration under settings 5. Select Enabled under 'Allow Blob public access'", - "Url": "https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-network-security#ns-2-secure-cloud-services-with-network-controls" + "Text": "Set `AllowBlobPublicAccess=false` on the storage account holding logs. Enforce **least privilege** via RBAC or scoped SAS, use **private endpoints** and network restrictions, and enable **immutability** for log containers to add **defense in depth** and prevent unauthorized access.", + "Url": "https://hub.prowler.com/check/monitor_storage_account_with_activity_logs_is_private" } }, - "Categories": [], + "Categories": [ + "internet-exposed", + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Configuring container Access policy to private will remove access from the container for everyone except owners of the storage account. Access policy needs to be set explicitly in order to allow access to other desired users." diff --git a/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.metadata.json b/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.metadata.json index 8c37cd0997..a251539887 100644 --- a/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.metadata.json +++ b/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.metadata.json @@ -1,30 +1,37 @@ { "Provider": "azure", "CheckID": "vm_backup_enabled", - "CheckTitle": "Ensure Backups are enabled for Azure Virtual Machines", + "CheckTitle": "Virtual Machine is protected by Azure Backup", "CheckType": [], "ServiceName": "vm", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Microsoft.Compute/virtualMachines", + "ResourceType": "microsoft.compute/virtualmachines", "ResourceGroup": "compute", - "Description": "Ensure that Microsoft Azure Backup service is in use for your Azure virtual machines (VMs) to protect against accidental deletion or corruption.", - "Risk": "Without Azure Backup enabled, VMs are at risk of data loss due to accidental deletion, corruption, or other failures, and recovery options are limited.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/backup/backup-overview", + "Description": "**Azure VMs** are evaluated for protection by **Azure Backup** by confirming they exist as VM backup items in a **Recovery Services vault**.\n\nVMs absent from any vault item indicate no configured backup coverage.", + "Risk": "Unprotected VMs jeopardize **availability** and **integrity**. Deletion, corruption, or ransomware can wipe data, and without recovery points recovery is slow or impossible, causing extended outages, missed `RPO/RTO`, and cascading impact on dependent services.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/backup/backup-azure-arm-vms-prepare", + "https://learn.microsoft.com/en-us/azure/backup/quick-backup-vm-portal", + "https://learn.microsoft.com/en-us/azure/backup/backup-overview" + ], "Remediation": { "Code": { - "CLI": "az backup protection enable-for-vm --resource-group --vm --vault-name --policy-name DefaultPolicy", - "NativeIaC": "", - "Other": "https://learn.microsoft.com/en-us/azure/backup/quick-backup-vm-portal", - "Terraform": "" + "CLI": "az backup protection enable-for-vm --resource-group --vault-name --vm --vm-resource-group --policy-name DefaultPolicy", + "NativeIaC": "```bicep\n// Enable Azure Backup protection for an existing VM in an existing Recovery Services vault\nparam vmId string // e.g., /subscriptions//resourceGroups//providers/Microsoft.Compute/virtualMachines/\nparam vaultName string\nparam vaultRg string\nparam policyId string // e.g., /subscriptions//resourceGroups//providers/Microsoft.RecoveryServices/vaults//backupPolicies/\n\nresource vault 'Microsoft.RecoveryServices/vaults@2023-04-01' existing = {\n name: vaultName\n scope: resourceGroup(vaultRg)\n}\n\nvar vmRg = split(split(vmId, '/resourceGroups/')[1], '/')[0]\nvar vmName = split(vmId, '/virtualMachines/')[1]\n\nresource protect 'Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems@2023-02-01' = {\n // critical: this resource creates the protected item, enabling backup for the VM\n name: '${vault.name}/Azure/protectionContainers/iaasvmcontainer;iaasvmcontainerv2;${vmRg};${vmName}/protectedItems/VM;iaasvmcontainerv2;${vmRg};${vmName}'\n properties: {\n protectedItemType: 'Microsoft.Compute/virtualMachines' // critical: VM backup item type\n sourceResourceId: vmId // critical: target VM to protect\n policyId: policyId // critical: associates the VM with a backup policy\n }\n}\n```", + "Other": "1. In Azure Portal, go to Virtual machines > > Backup\n2. Select a Recovery Services vault in the same region (or create one if prompted)\n3. Choose the DefaultPolicy (or an existing VM backup policy)\n4. Click Enable backup", + "Terraform": "```hcl\n# Protect an existing VM with Azure Backup\nresource \"azurerm_backup_protected_vm\" \"\" {\n resource_group_name = \"\" # vault's resource group\n recovery_vault_name = \"\"\n source_vm_id = \"\" # critical: VM to protect\n backup_policy_id = \"\" # critical: policy that enables backup\n}\n```" }, "Recommendation": { - "Text": "Enable Azure Backup for each VM by associating it with a Recovery Services vault and a backup policy.", - "Url": "https://docs.microsoft.com/en-us/azure/backup/quick-backup-vm-portal" + "Text": "Protect all VMs with **Azure Backup** in a **Recovery Services vault** under a standardized policy. Align schedules and retention to `RPO/RTO`, use `GRS`/`ZRS` and `immutable` vault features, enforce least privilege on backup operations, automate enrollment at provisioning, and regularly test restores.", + "Url": "https://hub.prowler.com/check/vm_backup_enabled" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/azure/services/vm/vm_desired_sku_size/vm_desired_sku_size.metadata.json b/prowler/providers/azure/services/vm/vm_desired_sku_size/vm_desired_sku_size.metadata.json index 98124cfb36..77b06c905e 100644 --- a/prowler/providers/azure/services/vm/vm_desired_sku_size/vm_desired_sku_size.metadata.json +++ b/prowler/providers/azure/services/vm/vm_desired_sku_size/vm_desired_sku_size.metadata.json @@ -1,27 +1,31 @@ { "Provider": "azure", "CheckID": "vm_desired_sku_size", - "CheckTitle": "Ensure that your virtual machine instances are using SKU sizes that are approved by your organization", + "CheckTitle": "Virtual Machine uses an organization-approved SKU size", "CheckType": [], "ServiceName": "vm", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Microsoft.Compute/virtualMachines", + "Severity": "medium", + "ResourceType": "microsoft.compute/virtualmachines", "ResourceGroup": "compute", - "Description": "Ensure that your virtual machine instances are using SKU sizes that are approved by your organization. This check requires configuration of the desired VM SKU sizes in the Prowler configuration file.", - "Risk": "Setting limits for the SKU size(s) of the virtual machine instances provisioned in your Microsoft Azure account can help you to manage better your cloud compute power, address internal compliance requirements and prevent unexpected charges on your Azure monthly bill. Without proper SKU size controls, organizations may face cost overruns and compliance violations.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/overview", + "Description": "**Azure virtual machines** are compared against an organization **allowlist of VM size SKUs** defined in `desired_vm_sku_sizes`.\n\nInstances using sizes outside this list are flagged as non-standard.", + "Risk": "Unrestricted VM sizes enable over-provisioned or exotic SKUs, leading to:\n- Sudden cost spikes and quota exhaustion (availability)\n- Use of hardware lacking required security features (confidentiality/integrity)\n- Abuse by compromised accounts for cryptomining at scale", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/resize-vm", + "https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/overview" + ], "Remediation": { "Code": { - "CLI": "az policy assignment create --display-name 'Allowed VM SKU Sizes' --policy cccc23c7-8427-4f53-ad12-b6a63eb452b3 -p '{\"listOfAllowedSKUs\": {\"value\": [\"\", \"\"]}}' --scope /subscriptions/", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "az vm resize --resource-group --name --size ", + "NativeIaC": "```bicep\n// Resize VM to an approved SKU\nresource vm 'Microsoft.Compute/virtualMachines@2023-09-01' = {\n name: ''\n location: ''\n properties: {\n hardwareProfile: {\n vmSize: '' // CRITICAL: sets VM to an approved size so the check passes\n }\n }\n}\n```", + "Other": "1. In Azure Portal, go to Virtual machines and select the VM\n2. Under Availability + scale, select Size\n3. Choose an approved size (e.g., ) and click Resize\n4. If the size isn't listed, Stop (deallocate) the VM, then retry Resize", + "Terraform": "```hcl\n# Enforce allowed VM SKUs via Azure Policy\nresource \"azurerm_policy_assignment\" \"\" {\n name = \"\"\n scope = \"/subscriptions/\"\n policy_definition_id = \"/providers/Microsoft.Authorization/policyDefinitions/\"\n\n parameters = jsonencode({\n listOfAllowedSKUs = { value = [\"\", \"\"] } # CRITICAL: only these SKUs are allowed\n })\n}\n```" }, "Recommendation": { - "Text": "1. Define and document your organization's approved VM SKU sizes based on workload requirements, cost constraints, and compliance needs. 2. Implement Azure Policy to enforce VM size restrictions across your subscriptions. 3. Use the 'Allowed virtual machine size SKUs' built-in policy to restrict VM creation to approved sizes. 4. Regularly review and update your approved SKU list based on changing business requirements and cost optimization goals. 5. Monitor VM usage and costs to ensure compliance with your SKU size policies.", - "Url": "https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/resize-vm" + "Text": "Adopt a **least privilege, policy-enforced allowlist** of VM size SKUs per workload tier and region.\n- Deny creation of non-approved sizes across scopes\n- Require exception reviews with time-bound overrides\n- Reassess the list regularly for cost, performance, and compliance\n- Monitor provisioning and cost anomalies for drift", + "Url": "https://hub.prowler.com/check/vm_desired_sku_size" } }, "Categories": [], diff --git a/prowler/providers/azure/services/vm/vm_ensure_attached_disks_encrypted_with_cmk/vm_ensure_attached_disks_encrypted_with_cmk.metadata.json b/prowler/providers/azure/services/vm/vm_ensure_attached_disks_encrypted_with_cmk/vm_ensure_attached_disks_encrypted_with_cmk.metadata.json index d880672dcb..7887e4ffae 100644 --- a/prowler/providers/azure/services/vm/vm_ensure_attached_disks_encrypted_with_cmk/vm_ensure_attached_disks_encrypted_with_cmk.metadata.json +++ b/prowler/providers/azure/services/vm/vm_ensure_attached_disks_encrypted_with_cmk/vm_ensure_attached_disks_encrypted_with_cmk.metadata.json @@ -1,30 +1,38 @@ { "Provider": "azure", "CheckID": "vm_ensure_attached_disks_encrypted_with_cmk", - "CheckTitle": "Ensure that 'OS and Data' disks are encrypted with Customer Managed Key (CMK)", + "CheckTitle": "Virtual Machine OS or data disk is encrypted with a customer-managed key (CMK)", "CheckType": [], "ServiceName": "vm", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Microsoft.Compute/virtualMachines", + "Severity": "medium", + "ResourceType": "microsoft.compute/disks", "ResourceGroup": "compute", - "Description": "Ensure that OS disks (boot volumes) and data disks (non-boot volumes) are encrypted with CMK (Customer Managed Keys). Customer Managed keys can be either ADE or Server Side Encryption (SSE).", - "Risk": "Encrypting the IaaS VM's OS disk (boot volume) and Data disks (non-boot volume) ensures that the entire content is fully unrecoverable without a key, thus protecting the volume from unwanted reads. PMK (Platform Managed Keys) are enabled by default in Azure-managed disks and allow encryption at rest. CMK is recommended because it gives the customer the option to control which specific keys are used for the encryption and decryption of the disk. The customer can then change keys and increase security by disabling them instead of relying on the PMK key that remains unchanging. There is also the option to increase security further by using automatically rotating keys so that access to disk is ensured to be limited. Organizations should evaluate what their security requirements are, however, for the data stored on the disk. For high-risk data using CMK is a must, as it provides extra steps of security. If the data is low risk, PMK is enabled by default and provides sufficient data security.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machines/disk-encryption-overview", + "Description": "**Attached Azure managed disks** (OS and data) are assessed to confirm encryption uses **customer-managed keys** (`CMK`) via disk encryption sets rather than platform-managed keys. Scope includes disks currently attached to VMs, evaluating their encryption type to verify CMK is applied.", + "Risk": "Without **CMK**, you lose control over key lifecycle and access. This weakens confidentiality and compliance: you can't enforce independent rotation, promptly revoke keys to crypto-lock stolen copies/snapshots, or separate duties. Misuse or compromise may keep data readable beyond your trust boundary.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/virtual-machines/disk-encryption-overview", + "https://support.icompaas.com/support/solutions/articles/62000229895-ensure-that-os-and-data-disks-are-encrypted-with-customer-managed-key-cmk-", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/sse-boot-disk-cmk.html#", + "https://learn.microsoft.com/en-us/azure/security/fundamentals/data-encryption-best-practices#protect-data-at-rest" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/sse-boot-disk-cmk.html#", - "Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/bc_azr_general_1#terraform" + "CLI": "az disk update -g -n --encryption-type EncryptionAtRestWithCustomerKey --disk-encryption-set ", + "NativeIaC": "```bicep\n// Encrypt a managed disk with a customer-managed key via Disk Encryption Set (DES)\nresource disk 'Microsoft.Compute/disks@2023-10-02' = {\n name: ''\n location: ''\n sku: {\n name: 'Standard_LRS'\n }\n properties: {\n creationData: {\n createOption: 'Empty'\n }\n diskSizeGB: 32\n\n // CRITICAL: Use CMK by attaching a Disk Encryption Set (DES)\n // This switches encryption from platform-managed to customer-managed\n encryption: {\n type: 'EncryptionAtRestWithCustomerKey' // critical: CMK\n diskEncryptionSetId: '' // critical: DES resource ID\n }\n }\n}\n```", + "Other": "1. In Azure Portal, open the target VM and click Stop to deallocate it.\n2. For each data disk: Go to VM > Disks > select the data disk > Detach > Save.\n3. In the portal search box, open Disks, select the detached disk > Encryption.\n4. Set Encryption type to Customer-managed key (Disk encryption set), select your Disk Encryption Set, then Save.\n5. Reattach the disk: VM > Disks > Add data disk (select the same disk) > Save.\n6. For OS disk, use Swap OS disk: create a new managed disk from a snapshot/image with Encryption type = Customer-managed key (select the same DES), then VM > Disks > Swap OS disk and choose that disk.\n7. Start the VM.", + "Terraform": "```hcl\n# Encrypt a managed disk with a customer-managed key via Disk Encryption Set (DES)\nresource \"azurerm_managed_disk\" \"\" {\n name = \"\"\n location = \"\"\n resource_group_name = \"\"\n storage_account_type = \"Standard_LRS\"\n create_option = \"Empty\"\n disk_size_gb = 32\n\n # CRITICAL: Attach DES to enable SSE with CMK and pass the check\n disk_encryption_set_id = \"\" # critical: DES ID\n}\n```" }, "Recommendation": { - "Text": "Note: Disks must be detached from VMs to have encryption changed. 1. Go to Virtual machines 2. For each virtual machine, go to Settings 3. Click on Disks 4. Click the ellipsis (...), then click Detach to detach the disk from the VM 5. Now search for Disks and locate the unattached disk 6. Click the disk then select Encryption 7. Change your encryption type, then select your encryption set 8. Click Save 9. Go back to the VM and re-attach the disk", - "Url": "https://learn.microsoft.com/en-us/azure/security/fundamentals/data-encryption-best-practices#protect-data-at-rest" + "Text": "Use **CMK** with disk encryption sets for all attached OS and data disks.\n- Enforce **least privilege** on key usage and scope\n- Enable periodic key rotation and auditing\n- Store keys in HSM-backed vaults; separate key and data admins\n- Combine with **encryption at host** to cover temp disks and caches", + "Url": "https://hub.prowler.com/check/vm_ensure_attached_disks_encrypted_with_cmk" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Using CMK/BYOK will entail additional management of keys." diff --git a/prowler/providers/azure/services/vm/vm_ensure_unattached_disks_encrypted_with_cmk/vm_ensure_unattached_disks_encrypted_with_cmk.metadata.json b/prowler/providers/azure/services/vm/vm_ensure_unattached_disks_encrypted_with_cmk/vm_ensure_unattached_disks_encrypted_with_cmk.metadata.json index 0bf4e96e91..8675865160 100644 --- a/prowler/providers/azure/services/vm/vm_ensure_unattached_disks_encrypted_with_cmk/vm_ensure_unattached_disks_encrypted_with_cmk.metadata.json +++ b/prowler/providers/azure/services/vm/vm_ensure_unattached_disks_encrypted_with_cmk/vm_ensure_unattached_disks_encrypted_with_cmk.metadata.json @@ -1,30 +1,38 @@ { "Provider": "azure", "CheckID": "vm_ensure_unattached_disks_encrypted_with_cmk", - "CheckTitle": "Ensure that 'Unattached disks' are encrypted with 'Customer Managed Key' (CMK)", + "CheckTitle": "Unattached disk is encrypted with a customer-managed key (CMK)", "CheckType": [], "ServiceName": "vm", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Microsoft.Compute/virtualMachines", + "Severity": "medium", + "ResourceType": "microsoft.compute/disks", "ResourceGroup": "compute", - "Description": "Ensure that unattached disks in a subscription are encrypted with a Customer Managed Key (CMK).", - "Risk": "Managed disks are encrypted by default with Platform-managed keys. Using Customer-managed keys may provide an additional level of security or meet an organization's regulatory requirements. Encrypting managed disks ensures that its entire content is fully unrecoverable without a key and thus protects the volume from unwarranted reads. Even if the disk is not attached to any of the VMs, there is always a risk where a compromised user account with administrative access to VM service can mount/attach these data disks, which may lead to sensitive information disclosure and tampering.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/security/fundamentals/azure-disk-encryption-vms-vmss", + "Description": "Unattached **Azure managed disks** use **Customer-Managed Keys** (`CMK`) for server-side encryption rather than platform-managed keys. Only disks not currently attached to a VM are in scope.", + "Risk": "Without **CMK**, you lack independent key control on unattached disks. A compromised admin could attach a disk to read or alter data before you can revoke access. Missing customer-controlled rotation and revocation weakens **confidentiality** and **integrity**, and can hinder data-sovereignty compliance.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/sse-unattached-disk-cmk.html#", + "https://learn.microsoft.com/en-us/azure/security/fundamentals/data-encryption-best-practices#protect-data-at-rest", + "https://learn.microsoft.com/en-us/rest/api/compute/disks/delete?view=rest-compute-2023-10-02&tabs=HTTP", + "https://learn.microsoft.com/en-us/azure/virtual-machines/disk-encryption-overview" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/sse-unattached-disk-cmk.html#", - "Terraform": "" + "CLI": "az disk update -g -n --encryption-type EncryptionAtRestWithCustomerKey --disk-encryption-set ", + "NativeIaC": "```bicep\n// Update an existing managed disk to use a customer-managed key via Disk Encryption Set\nresource example_disk 'Microsoft.Compute/disks@2023-08-01' = {\n name: ''\n location: ''\n properties: {\n encryption: {\n type: 'EncryptionAtRestWithCustomerKey' // CRITICAL: switch to CMK-based encryption\n diskEncryptionSetId: '' // CRITICAL: DES resource ID that holds the CMK\n }\n }\n}\n```", + "Other": "1. In Azure portal, go to Disks and open the unattached disk\n2. Select Encryption\n3. Set Encryption type to Customer-managed key\n4. Select the Disk encryption set to use\n5. Click Save", + "Terraform": "```hcl\n# Associate an unattached managed disk with a Disk Encryption Set (CMK)\nresource \"azurerm_managed_disk\" \"\" {\n name = \"\"\n location = \"\"\n resource_group_name = \"\"\n create_option = \"Empty\"\n disk_size_gb = 1\n\n disk_encryption_set_id = \"\" # CRITICAL: enables CMK by linking the Disk Encryption Set\n}\n```" }, "Recommendation": { - "Text": "If data stored in the disk is no longer useful, refer to Azure documentation to delete unattached data disks at: https://learn.microsoft.com/en-us/rest/api/compute/disks/delete?view=rest-compute-2023-10-02&tabs=HTTP", - "Url": "https://learn.microsoft.com/en-us/azure/security/fundamentals/data-encryption-best-practices#protect-data-at-rest" + "Text": "Encrypt unattached disks with **CMK** backed by a hardened key service. Enforce **least privilege** for disk attachment and key usage, enable key rotation and auditing, and restrict access to keys. Apply lifecycle governance-*remove stale disks*-to reduce exposure and support **defense in depth**.", + "Url": "https://hub.prowler.com/check/vm_ensure_unattached_disks_encrypted_with_cmk" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "You must have your key vault set up to utilize this. Encryption is available only on Standard tier VMs. This might cost you more. Utilizing and maintaining Customer-managed keys will require additional work to create, protect, and rotate keys." diff --git a/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.metadata.json b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.metadata.json index 572e2560b4..92c4853fcb 100644 --- a/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.metadata.json +++ b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "vm_ensure_using_approved_images", - "CheckTitle": "Ensure that Azure VMs are using an approved machine image.", + "CheckTitle": "Virtual Machine uses an approved custom machine image", "CheckType": [], "ServiceName": "vm", - "SubServiceName": "image", - "ResourceIdTemplate": "/subscriptions//resourceGroups//providers/Microsoft.Compute/images/", - "Severity": "medium", - "ResourceType": "Microsoft.Compute/images", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "microsoft.compute/virtualmachines", "ResourceGroup": "compute", - "Description": "Ensure that all your Azure virtual machine instances are launched from approved machine images only.", - "Risk": "An approved machine image is a custom virtual machine (VM) image that contains a pre-configured OS and a well-defined stack of server software approved by Azure, fully configured to run your application. Using approved (golden) machine images to launch new VM instances within your Azure cloud environment brings major benefits such as fast and stable application deployment and scaling, secure application stack upgrades, and versioning.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machines/windows/create-vm-generalized-managed", + "Description": "**Azure VMs** are evaluated for use of an **approved custom image** by inspecting the VM image reference. The expected format is a subscription-scoped ID like `/subscriptions/.../providers/Microsoft.Compute/images/`, not marketplace, gallery, or community sources.", + "Risk": "Using **unapproved images** undermines **integrity** and **confidentiality** by introducing unknown packages, misconfigurations, or malware. Attackers can implant backdoors, weaken hardening, and bypass baselines, enabling data exfiltration and lateral movement, and harming **availability** with unpatched software.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/approved-machine-image.html", + "https://learn.microsoft.com/en-us/azure/virtual-machines/windows/create-vm-generalized-managed" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/approved-machine-image.html", - "Terraform": "" + "CLI": "az vm create --resource-group --name --image /subscriptions//resourceGroups//providers/Microsoft.Compute/images/ --admin-username azureuser --generate-ssh-keys", + "NativeIaC": "```bicep\n// Create a VM using an approved custom managed image\nparam location string = resourceGroup().location\nparam nicId string\nparam adminUsername string\nparam adminPassword string\n\nresource vm 'Microsoft.Compute/virtualMachines@2023-09-01' = {\n name: ''\n location: location\n properties: {\n hardwareProfile: { vmSize: 'Standard_DS1_v2' }\n storageProfile: {\n imageReference: {\n id: '/subscriptions//resourceGroups//providers/Microsoft.Compute/images/' // CRITICAL: use managed image ID to pass check\n }\n osDisk: { createOption: 'FromImage' }\n }\n osProfile: {\n computerName: ''\n adminUsername: adminUsername\n adminPassword: adminPassword\n }\n networkProfile: {\n networkInterfaces: [{ id: nicId }]\n }\n }\n}\n```", + "Other": "1. In Azure Portal, go to Virtual machines > Create > Azure virtual machine\n2. Under Image, click See all images, then select the My Images tab\n3. Choose the approved managed image (type: Microsoft.Compute/images)\n4. Complete required basics and create the VM\n5. If replacing a non-compliant VM, migrate workload and delete the old VM", + "Terraform": "```hcl\n# VM created from an approved custom managed image\nresource \"azurerm_windows_virtual_machine\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n location = \"\"\n size = \"Standard_DS1_v2\"\n admin_username = \"\"\n admin_password = \"\"\n network_interface_ids = [\"\"]\n\n source_image_id = \"/subscriptions//resourceGroups//providers/Microsoft.Compute/images/\" # CRITICAL: managed image ID ensures PASS\n\n os_disk {\n caching = \"ReadWrite\"\n storage_account_type = \"Standard_LRS\"\n }\n}\n```" }, "Recommendation": { - "Text": "Re-create the required VM instances using the approved (golden) machine image.", - "Url": "https://docs.microsoft.com/en-us/azure/virtual-machines/windows/create-vm-generalized-managed" + "Text": "Standardize on **golden images** maintained in an Azure Compute Gallery or managed images.\n- Harden and patch each release; scan for vulnerabilities\n- Restrict who can create/publish images (**least privilege**)\n- Enforce deployments only from approved images via policy\n- Version, sign, and retire images regularly", + "Url": "https://hub.prowler.com/check/vm_ensure_using_approved_images" } }, - "Categories": [], + "Categories": [ + "software-supply-chain" + ], "DependsOn": [], "RelatedTo": [], "Notes": "This check only validates if the VM was launched from a custom image. It does not validate the image content or security baseline." diff --git a/prowler/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks.metadata.json b/prowler/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks.metadata.json index acd819e71b..4d43280e71 100644 --- a/prowler/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks.metadata.json +++ b/prowler/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks.metadata.json @@ -1,30 +1,38 @@ { "Provider": "azure", "CheckID": "vm_ensure_using_managed_disks", - "CheckTitle": "Ensure Virtual Machines are utilizing Managed Disks", + "CheckTitle": "Virtual Machine uses managed disks for OS and data disks", "CheckType": [], "ServiceName": "vm", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "Microsoft.Compute/virtualMachines", + "Severity": "high", + "ResourceType": "microsoft.compute/virtualmachines", "ResourceGroup": "compute", - "Description": "Migrate blob-based VHDs to Managed Disks on Virtual Machines to exploit the default features of this configuration. The features include: 1. Default Disk Encryption 2. Resilience, as Microsoft will managed the disk storage and move around if underlying hardware goes faulty 3. Reduction of costs over storage accounts", - "Risk": "Managed disks are by default encrypted on the underlying hardware, so no additional encryption is required for basic protection. It is available if additional encryption is required. Managed disks are by design more resilient that storage accounts. For ARM-deployed Virtual Machines, Azure Adviser will at some point recommend moving VHDs to managed disks both from a security and cost management perspective.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machines/unmanaged-disks-deprecation", + "Description": "**Azure virtual machines** use **managed disks** for the OS disk and every data disk, rather than page blob VHDs in storage accounts.\n\nThe evaluation confirms that all attached VM disks are managed.", + "Risk": "Using **unmanaged disks** ties VM data to storage account keys and SAS, increasing exposure of disk contents if those secrets leak (**confidentiality/integrity**). Limited platform resiliency and quotas increase **availability** risk. With Azure retiring unmanaged disks on `2026-03-31`, such VMs may be stopped and unable to start.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/virtual-machines/unmanaged-disks-deprecation", + "https://learn.microsoft.com/en-us/azure/virtual-machines/windows/convert-unmanaged-to-managed-disks", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/managed-disks-in-use.html", + "https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-4-enable-data-at-rest-encryption-by-default" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/managed-disks-in-use.html", - "Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/ensure-virtual-machines-are-utilizing-managed-disks#terraform" + "CLI": "az vm convert --resource-group --name ", + "NativeIaC": "```bicep\n// VM configured to use a managed OS disk\nresource vm 'Microsoft.Compute/virtualMachines@2023-09-01' = {\n name: ''\n location: resourceGroup().location\n properties: {\n hardwareProfile: { vmSize: 'Standard_DS1_v2' }\n storageProfile: {\n osDisk: {\n name: 'osdisk'\n osType: 'Linux'\n createOption: 'Attach'\n managedDisk: {\n id: '' // CRITICAL: attaching a managed disk makes the VM use managed disks\n }\n }\n }\n networkProfile: { networkInterfaces: [ { id: '' } ] }\n }\n}\n```", + "Other": "1. In Azure Portal, go to Virtual machines > select your VM\n2. Click Stop to deallocate the VM\n3. In the VM menu, select Disks\n4. Click Migrate to managed disks, then click Migrate to start\n5. After migration completes, click Start to power on the VM\n6. If the VM is in an availability set: first go to Availability sets > select the set > Convert to managed (SKU: Aligned), then repeat steps 1-5", + "Terraform": "```hcl\n# VM attached to a managed OS disk\nresource \"azurerm_virtual_machine\" \"\" {\n name = \"\"\n location = \"\"\n resource_group_name = \"\"\n network_interface_ids = [\"\"]\n vm_size = \"Standard_DS1_v2\"\n\n storage_os_disk {\n name = \"osdisk\"\n create_option = \"Attach\"\n managed_disk_id = \"\" # CRITICAL: use a managed disk for the OS disk\n }\n}\n```" }, "Recommendation": { - "Text": "1. Using the search feature, go to Virtual Machines 2. Select the virtual machine you would like to convert 3. Select Disks in the menu for the VM 4. At the top select Migrate to managed disks 5. You may follow the prompts to convert the disk and finish by selecting Migrate to start the process", - "Url": "https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-4-enable-data-at-rest-encryption-by-default" + "Text": "Adopt **managed disks** for all VM OS and data volumes.\n- Enforce **least privilege** via RBAC at disk scope\n- Use encryption at rest; prefer `CMEK` when mandated\n- Apply backups/snapshots and zone-aware designs for **defense in depth**\n- After migration, delete orphaned VHD blobs to avoid data exposure and cost.", + "Url": "https://hub.prowler.com/check/vm_ensure_using_managed_disks" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "There are additional costs for managed disks based off of disk space allocated. When converting to managed disks, VMs will be powered off and back on." diff --git a/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.metadata.json b/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.metadata.json index a08397637d..490cfa7d2e 100644 --- a/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.metadata.json +++ b/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "azure", "CheckID": "vm_jit_access_enabled", - "CheckTitle": "Enable Just-In-Time Access for Virtual Machines", + "CheckTitle": "Virtual Machine has Just-in-Time (JIT) access enabled", "CheckType": [], "ServiceName": "vm", "SubServiceName": "", - "ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}", - "Severity": "high", - "ResourceType": "Microsoft.Compute/virtualMachines", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "microsoft.compute/virtualmachines", "ResourceGroup": "compute", - "Description": "Ensure that Microsoft Azure virtual machines are configured to use Just-in-Time (JIT) access.", - "Risk": "Without JIT access, management ports such as 22 (SSH) and 3389 (RDP) may be exposed, increasing the risk of brute-force and DDoS attacks.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/security-center/security-center-just-in-time?tabs=jit-config-asc%2Cjit-request-asc", + "Description": "**Azure virtual machines** are associated with a **Just-in-Time (JIT) network access policy** that opens management ports only for approved, time-bound requests from specified source IPs.", + "Risk": "Without **JIT**, management ports like `22`/`3389` may stay reachable, enabling:\n- brute-force and password-spray attempts\n- exploitation of remote access flaws or stolen keys\nThis threatens **confidentiality** (data exfiltration), **integrity** (unauthorized changes), and **availability** (service disruption).", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/defender-for-cloud/enable-just-in-time-access?tabs=jit-config-asc%2Cjit-request-asc" + ], "Remediation": { "Code": { - "CLI": "az security jit-policy list --query '[*].virtualMachines[*].id | []'", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "az rest --method PUT --url \"https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.Security/locations//jitNetworkAccessPolicies/default?api-version=2020-01-01\" --body '{\"kind\":\"Basic\",\"properties\":{\"virtualMachines\":[{\"id\":\"/subscriptions//resourceGroups//providers/Microsoft.Compute/virtualMachines/\",\"ports\":[{\"number\":22,\"protocol\":\"*\",\"allowedSourceAddressPrefix\":[\"*\"],\"maxRequestAccessDuration\":\"PT3H\"}]}]}}'", + "NativeIaC": "```bicep\n// Bicep: Enable JIT for a VM by creating a JIT policy that references the VM\nparam location string = \"\"\nparam vmId string = \"/subscriptions//resourceGroups//providers/Microsoft.Compute/virtualMachines/\"\n\nresource jit 'Microsoft.Security/locations/jitNetworkAccessPolicies@2020-01-01' = {\n name: '${location}/default'\n properties: {\n virtualMachines: [\n {\n id: vmId // Critical: Adding the VM ID enables JIT for this VM\n ports: [\n {\n number: 22\n protocol: '*'\n allowedSourceAddressPrefix: ['*']\n maxRequestAccessDuration: 'PT3H' // Critical: Minimal required port configuration for JIT\n }\n ]\n }\n ]\n }\n}\n```", + "Other": "1. In the Azure portal, go to Microsoft Defender for Cloud\n2. Select Workload protections > Just-in-time VM access\n3. Open the Not configured tab, select the VM, and click Enable JIT on VMs\n4. Keep the default port (22 for Linux or 3389 for Windows) and click Save", + "Terraform": "```hcl\n# Enable JIT by creating a Security Center JIT policy for the VM\nresource \"azurerm_security_center_jit_network_access_policy\" \"\" {\n name = \"default\"\n location = \"\"\n resource_group_name = \"\"\n\n virtual_machines {\n id = \"\" # Critical: VM ID included in the JIT policy enables JIT for this VM\n\n ports {\n number = 22\n protocol = \"*\"\n allowed_source_address_prefixes = [\"*\"]\n max_request_access_duration = \"PT3H\" # Critical: Minimal port config required by JIT\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Enable Just-in-Time (JIT) network access for your Microsoft Azure virtual machines using the Azure Portal under Security Center > Just-in-time VM access.", - "Url": "https://docs.microsoft.com/en-us/azure/security-center/security-center-just-in-time?tabs=jit-config-asc%2Cjit-request-asc" + "Text": "Enable **JIT network access** and apply **least privilege** and **zero trust**:\n- keep admin ports closed by default\n- approve only specific IPs, minimal ports (e.g., `22`, `3389`), and short windows\n- favor **private access** (VPN, Bastion)\n- layer controls (**defense in depth**) and audit access requests", + "Url": "https://hub.prowler.com/check/vm_jit_access_enabled" } }, - "Categories": [], + "Categories": [ + "internet-exposed" + ], "DependsOn": [], "RelatedTo": [], "Notes": "JIT access can only be enabled via the Azure Portal. Ensure Security Center standard pricing tier for servers is enabled." diff --git a/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.metadata.json b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.metadata.json index 75dcfe9d28..e2ec0ba32c 100644 --- a/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.metadata.json +++ b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "vm_linux_enforce_ssh_authentication", - "CheckTitle": "Ensure SSH key authentication is enforced on Linux-based Virtual Machines", + "CheckTitle": "Linux Virtual Machine has password authentication disabled (SSH key authentication enforced)", "CheckType": [], "ServiceName": "vm", "SubServiceName": "", - "ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Microsoft.Compute/virtualMachines", + "ResourceType": "microsoft.compute/virtualmachines", "ResourceGroup": "compute", - "Description": "Ensure that Azure Linux-based virtual machines are configured to use SSH keys by disabling password authentication.", - "Risk": "Allowing password-based SSH authentication increases the risk of brute-force attacks and unauthorized access. Enforcing SSH key authentication ensures only users with the private key can access the VM.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/virtual-machines/linux/create-ssh-keys-detailed", + "Description": "**Azure Linux virtual machines** are assessed for SSH configuration where `disablePasswordAuthentication` is set to `true`, allowing only **public key authentication** and disallowing interactive passwords.", + "Risk": "With **password-based SSH**, attackers can run brute-force or password-spray campaigns. A successful login grants remote shell, enabling command execution, data exfiltration, and lateral movement, degrading **confidentiality** and **integrity**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/ssh-authentication-type.html", + "https://learn.microsoft.com/en-us/azure/virtual-machines/linux/create-ssh-keys-detailed" + ], "Remediation": { "Code": { "CLI": "", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/ssh-authentication-type.html", - "Terraform": "" + "NativeIaC": "```bicep\n// Bicep snippet to enforce SSH key authentication on a Linux VM\nresource linuxVm 'Microsoft.Compute/virtualMachines@2024-03-01' = {\n name: ''\n location: ''\n properties: {\n osProfile: {\n linuxConfiguration: {\n disablePasswordAuthentication: true // CRITICAL: Disables password-based SSH; enforces SSH key authentication\n }\n }\n }\n}\n```", + "Other": "1. In Azure Portal, go to your Linux VM > Settings > Export template\n2. Click Edit template\n3. Find properties.osProfile.linuxConfiguration and set disablePasswordAuthentication to true\n4. Ensure an SSH public key is provided (adminPassword must be absent)\n5. Click Save, then Review + create to deploy the update", + "Terraform": "```hcl\n# Minimal Terraform to enforce SSH key authentication on a Linux VM\nresource \"azurerm_linux_virtual_machine\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n location = \"\"\n size = \"Standard_B1s\"\n network_interface_ids = [\"\"]\n admin_username = \"azureuser\"\n\n disable_password_authentication = true # CRITICAL: Disables password-based SSH; enforces SSH key authentication\n\n admin_ssh_key { # Required when password auth is disabled\n username = \"azureuser\"\n public_key = \"\"\n }\n\n os_disk {\n caching = \"ReadWrite\"\n storage_account_type = \"Standard_LRS\"\n }\n\n source_image_reference {\n publisher = \"Canonical\"\n offer = \"0001-com-ubuntu-server-focal\"\n sku = \"20_04-lts\"\n version = \"latest\"\n }\n}\n```" }, "Recommendation": { - "Text": "Recreate Linux VMs with SSH key authentication enabled and password authentication disabled.", - "Url": "https://docs.microsoft.com/en-us/azure/virtual-machines/linux/create-ssh-keys-detailed" + "Text": "Enforce **key-only SSH** by setting `disablePasswordAuthentication` to `true` and disallowing OS password logins. Use strong, passphrase-protected keys with rotation; restrict SSH via NSGs and private access or **Azure Bastion**; enable JIT; and apply **least privilege**.", + "Url": "https://hub.prowler.com/check/vm_linux_enforce_ssh_authentication" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer.metadata.json b/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer.metadata.json index 9e4b4ed95b..2a5f197734 100644 --- a/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer.metadata.json +++ b/prowler/providers/azure/services/vm/vm_scaleset_associated_with_load_balancer/vm_scaleset_associated_with_load_balancer.metadata.json @@ -1,30 +1,38 @@ { "Provider": "azure", "CheckID": "vm_scaleset_associated_with_load_balancer", - "CheckTitle": "VM Scale Set Is Associated With Load Balancer", + "CheckTitle": "Virtual Machine Scale Set is associated with a load balancer backend pool", "CheckType": [], "ServiceName": "vm", - "SubServiceName": "scaleset", - "ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachineScaleSets/{vmScaleSetName}", + "SubServiceName": "", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Microsoft.Compute/virtualMachineScaleSets", + "ResourceType": "microsoft.compute/virtualmachinescalesets", "ResourceGroup": "compute", - "Description": "Ensure that your Azure virtual machine scale sets are using load balancers for traffic distribution.", - "Risk": "Without load balancer integration, Azure virtual machine scale sets may experience reduced availability and potential service disruptions during traffic spikes or instance failures, leading to degraded user experience and potential business impact.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-network/network-overview", + "Description": "**Azure VM scale sets** are associated with at least one **load balancer backend pool**.\n\nThe evaluation looks for a backend pool link on each scale set's network configuration.", + "Risk": "Without a load balancer, traffic targets instances directly, bypassing health-based distribution. This degrades **availability** (overloads, no failover) and **reliability** (dropped sessions, uneven scaling) and can amplify **DoS** impact by concentrating flows on fewer nodes, increasing outage risk.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/virtual-network/network-overview", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/associated-load-balancers.html", + "https://learn.microsoft.com/ms-my/azure/load-balancer/load-balancer-multiple-virtual-machine-scale-set?tabs=azureportal", + "https://learn.microsoft.com/en-us/azure/load-balancer/load-balancer-overview" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/associated-load-balancers.html", - "Terraform": "" + "CLI": "az vmss update --resource-group --name --add virtualMachineProfile.networkProfile.networkInterfaceConfigurations[0].ipConfigurations[0].loadBalancerBackendAddressPools '{\"id\":\"/subscriptions//resourceGroups//providers/Microsoft.Network/loadBalancers//backendAddressPools/\"}'", + "NativeIaC": "```bicep\n// Associate VMSS with a Load Balancer backend pool\nresource vmss 'Microsoft.Compute/virtualMachineScaleSets@2023-09-01' = {\n name: ''\n location: resourceGroup().location\n properties: {\n virtualMachineProfile: {\n networkProfile: {\n networkInterfaceConfigurations: [\n {\n name: 'nic'\n properties: {\n ipConfigurations: [\n {\n name: 'ipcfg'\n properties: {\n loadBalancerBackendAddressPools: [\n { id: '' } // CRITICAL: attach VMSS NIC IP config to LB backend pool\n ]\n }\n }\n ]\n }\n }\n ]\n }\n }\n }\n}\n```", + "Other": "1. In Azure Portal, go to Load balancers > select \n2. Under Settings, open Backend pools and select the target backend pool\n3. Click Add > IP configurations\n4. Choose Virtual machine scale set, select and its IP configuration\n5. Click Add, then Save to attach the scale set to the backend pool", + "Terraform": "```hcl\n# Attach VMSS to an existing Load Balancer backend pool\nresource \"azurerm_linux_virtual_machine_scale_set\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n location = \"\"\n sku = \"Standard_DS1_v2\"\n instances = 1\n\n admin_username = \"azureuser\"\n disable_password_authentication = true\n admin_ssh_key {\n username = \"azureuser\"\n public_key = \"\"\n }\n\n source_image_reference {\n publisher = \"Canonical\"\n offer = \"0001-com-ubuntu-server-jammy\"\n sku = \"22_04-lts\"\n version = \"latest\"\n }\n\n network_interface {\n name = \"nic\"\n primary = true\n ip_configuration {\n name = \"ipcfg\"\n primary = true\n subnet_id = \"\"\n load_balancer_backend_address_pool_ids = [\"\"] # CRITICAL: associates VMSS with LB backend pool\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Attach a load balancer to your Azure virtual machine scale set to ensure high availability and optimal traffic distribution.", - "Url": "https://docs.microsoft.com/en-us/azure/load-balancer/load-balancer-overview" + "Text": "Associate VM scale sets with a **Standard Load Balancer** backend pool to enforce health-probed, even distribution and seamless failover.\n\nDesign for **high availability**: run multiple instances across zones, enable autoscale, and apply **defense in depth** with limited exposure and NSG controls. *Prefer SKU `Standard` over legacy Basic.*", + "Url": "https://hub.prowler.com/check/vm_scaleset_associated_with_load_balancer" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/azure/services/vm/vm_scaleset_not_empty/vm_scaleset_not_empty.metadata.json b/prowler/providers/azure/services/vm/vm_scaleset_not_empty/vm_scaleset_not_empty.metadata.json index a2012c7696..af45f81ae0 100644 --- a/prowler/providers/azure/services/vm/vm_scaleset_not_empty/vm_scaleset_not_empty.metadata.json +++ b/prowler/providers/azure/services/vm/vm_scaleset_not_empty/vm_scaleset_not_empty.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "vm_scaleset_not_empty", - "CheckTitle": "Check for Empty Virtual Machine Scale Sets", + "CheckTitle": "Virtual Machine Scale Set has at least one VM instance", "CheckType": [], "ServiceName": "vm", - "SubServiceName": "scaleset", - "ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachineScaleSets/{vmScaleSetName}", - "Severity": "low", - "ResourceType": "Microsoft.Compute/virtualMachineScaleSets", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "microsoft.compute/virtualmachinescalesets", "ResourceGroup": "compute", - "Description": "Identify and remove empty virtual machine scale sets from your Azure cloud account.", - "Risk": "Empty virtual machine scale sets may incur unnecessary costs and complicate cloud resource management, impacting cost optimization and compliance.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/overview", + "Description": "**Azure VM scale sets** with `0` VM instances are identified as **empty**.", + "Risk": "Orphaned scale sets degrade governance and can hide **stale autoscale** or permissive identities. If reactivated, they may launch VMs from **outdated images**, exposing vulnerable services and enabling lateral movement, impacting **confidentiality** and **availability**. They also create inventory sprawl and unnecessary spend.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/overview", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/empty-vm-scale-sets.html" + ], "Remediation": { "Code": { - "CLI": "az vmss delete --name --resource-group ", + "CLI": "az vmss scale --resource-group --name --new-capacity 1", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/empty-vm-scale-sets.html", - "Terraform": "" + "Other": "1. In Azure Portal, go to Virtual machine scale sets and select \n2. Under Settings, click Scaling\n3. Set Instance count to 1\n4. Click Save", + "Terraform": "```hcl\n# Patch the scale set to have at least one instance\nresource \"azapi_update_resource\" \"\" {\n type = \"Microsoft.Compute/virtualMachineScaleSets@2024-03-01\"\n resource_id = \"\"\n\n body = jsonencode({\n sku = {\n capacity = 1 # Critical: ensures the scale set has at least one VM instance\n }\n })\n}\n```" }, "Recommendation": { - "Text": "Remove empty Azure virtual machine scale sets to optimize costs and simplify management.", - "Url": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/empty-vm-scale-sets.html" + "Text": "Decommission empty scale sets or consolidate capacity. Apply **resource lifecycle** and **governance policies** to prevent drift; require tagging and owners, review regularly. Manage capacity through **IaC**, enforce **least privilege** on scale set identities, and use change control to avoid unintended autoscale activations. *If retained temporarily*, document purpose and review date.", + "Url": "https://hub.prowler.com/check/vm_scaleset_not_empty" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.metadata.json b/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.metadata.json index 63809ce7ae..b668c8fdfc 100644 --- a/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.metadata.json +++ b/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.metadata.json @@ -1,30 +1,36 @@ { "Provider": "azure", "CheckID": "vm_sufficient_daily_backup_retention_period", - "CheckTitle": "Ensure there is a sufficient daily backup retention period configured for Azure virtual machines.", + "CheckTitle": "Virtual Machine has a backup policy with a daily retention period meeting the configured minimum", "CheckType": [], "ServiceName": "vm", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Microsoft.Compute/virtualMachines", + "ResourceType": "microsoft.compute/virtualmachines", "ResourceGroup": "compute", - "Description": "Ensure there is a sufficient daily backup retention period configured for Azure virtual machines.", - "Risk": "Having an optimal daily backup retention period for your Azure virtual machines will enforce your backup strategy to follow the best practices as specified in the compliance regulations promoted by your organization. Retaining VM backups for a longer period of time will allow you to handle more efficiently your data restoration process in the event of a failure.", - "RelatedUrl": "https://docs.microsoft.com/en-us/azure/backup/backup-azure-vms-introduction", + "Description": "**Azure virtual machines** are evaluated for a backup policy in a Recovery Services vault with a **daily retention** period that meets the configured minimum. VMs lacking protection or using a shorter retention window are identified for review.", + "Risk": "**Insufficient or missing VM backups** weaken **availability** and **integrity**. Short retention reduces recovery points, limiting rollback after **ransomware**, accidental deletion, or faulty changes. This increases RPO, extends RTO, and can force rebuilds, causing data loss and downtime.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/sufficient-backup-retention-period.html", + "https://learn.microsoft.com/en-us/azure/backup/backup-azure-vms-introduction" + ], "Remediation": { "Code": { "CLI": "", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/sufficient-backup-retention-period.html", - "Terraform": "" + "NativeIaC": "```bicep\n// Create/ensure a VM backup policy with daily retention >= minimum\nresource rv 'Microsoft.RecoveryServices/vaults@2023-01-01' existing = {\n name: ''\n}\n\nresource vmPolicy 'Microsoft.RecoveryServices/vaults/backupPolicies@2023-02-01' = {\n name: '${rv.name}/'\n properties: {\n backupManagementType: 'AzureIaasVM'\n schedulePolicy: {\n schedulePolicyType: 'SimpleSchedulePolicyV2'\n scheduleRunFrequency: 'Daily'\n scheduleRunTimes: ['2020-01-01T23:00:00Z']\n }\n retentionPolicy: {\n retentionPolicyType: 'LongTermRetentionPolicy'\n dailySchedule: {\n retentionTimes: ['2020-01-01T23:00:00Z']\n retentionDuration: {\n count: 7 // CRITICAL: sets daily retention to at least the minimum required days\n durationType: 'Days' // explains the unit\n }\n }\n }\n }\n}\n```", + "Other": "1. In Azure portal, go to Recovery Services vault \n2. Select Backup policies > Azure Virtual Machine > Edit (or Create new)\n3. Set Daily retention to at least 7 days and Save\n4. Go to Backup items > Azure Virtual Machine\n5. If the VM is unprotected: click Backup, select the policy from step 3, and Enable\n6. If the VM is already protected with an insufficient policy: select the VM > Change Policy > choose the policy from step 3 > Save", + "Terraform": "```hcl\n# Backup policy with sufficient daily retention\nresource \"azurerm_backup_policy_vm\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n recovery_vault_name = \"\"\n\n backup {\n frequency = \"Daily\"\n time = \"23:00\"\n }\n\n retention_daily {\n count = 7 # CRITICAL: ensures daily retention meets minimum required days\n }\n}\n\n# Protect the VM with the compliant policy\nresource \"azurerm_backup_protected_vm\" \"\" {\n resource_group_name = \"\"\n recovery_vault_name = \"\"\n source_vm_id = \"\"\n backup_policy_id = azurerm_backup_policy_vm..id # CRITICAL: applies the policy to the VM\n}\n```" }, "Recommendation": { - "Text": "Set the daily backup retention period for each VM's backup policy to meet or exceed your organization's minimum requirement.", - "Url": "https://docs.microsoft.com/en-us/azure/backup/backup-azure-vms-introduction" + "Text": "Enforce backup policies that provide **daily retention** aligned to business RPO/RTO for every VM. Apply **defense in depth**: isolate backups in a vault, enable immutability/soft delete, limit changes with **least privilege** and MFA, and regularly test restores. Use tiered retention (daily/weekly/monthly/yearly) based on data criticality.", + "Url": "https://hub.prowler.com/check/vm_sufficient_daily_backup_retention_period" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/azure/services/vm/vm_trusted_launch_enabled/vm_trusted_launch_enabled.metadata.json b/prowler/providers/azure/services/vm/vm_trusted_launch_enabled/vm_trusted_launch_enabled.metadata.json index c68c4ec6ee..b53ac0ac78 100644 --- a/prowler/providers/azure/services/vm/vm_trusted_launch_enabled/vm_trusted_launch_enabled.metadata.json +++ b/prowler/providers/azure/services/vm/vm_trusted_launch_enabled/vm_trusted_launch_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "azure", "CheckID": "vm_trusted_launch_enabled", - "CheckTitle": "Ensure Trusted Launch is enabled on Virtual Machines", + "CheckTitle": "Virtual Machine has Trusted Launch with Secure Boot and vTPM enabled", "CheckType": [], "ServiceName": "vm", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Microsoft.Compute/virtualMachines", + "Severity": "medium", + "ResourceType": "microsoft.compute/virtualmachines", "ResourceGroup": "compute", - "Description": "When Secure Boot and vTPM are enabled together, they provide a strong foundation for protecting your VM from boot attacks. For example, if an attacker attempts to replace the bootloader with a malicious version, Secure Boot will prevent the VM from booting. If the attacker is able to bypass Secure Boot and install a malicious bootloader, vTPM can be used to detect the intrusion and alert you.", - "Risk": "Secure Boot and vTPM work together to protect your VM from a variety of boot attacks, including bootkits, rootkits, and firmware rootkits. Not enabling Trusted Launch in Azure VM can lead to increased vulnerability to rootkits and boot-level malware, reduced ability to detect and prevent unauthorized changes to the boot process, and a potential compromise of system integrity and data security.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-existing-vm?tabs=portal", + "Description": "**Azure VMs** are evaluated for **Trusted Launch** with both **Secure Boot** and **vTPM** enabled.\n\nIdentifies VMs not set to `TrustedLaunch` or missing `secureBootEnabled` and `vTpmEnabled` together.", + "Risk": "Missing **Trusted Launch** weakens boot-chain integrity. Attackers can persist via bootkits/rootkits, bypass OS controls, steal secrets, and tamper with drivers. Loss of attestation reduces detection, risking **integrity**, **confidentiality**, and **availability** through stealthy, hard-to-remediate compromises.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "az vm update --resource-group --name --security-type TrustedLaunch --enable-secure-boot true --enable-vtpm true", + "NativeIaC": "```bicep\n// Enables Trusted Launch with Secure Boot and vTPM on the VM\nresource vm 'Microsoft.Compute/virtualMachines@2022-11-01' = {\n name: ''\n location: ''\n properties: {\n securityProfile: {\n securityType: 'TrustedLaunch' // Critical: sets VM security type to Trusted Launch\n uefiSettings: {\n secureBootEnabled: true // Critical: enables Secure Boot\n vTpmEnabled: true // Critical: enables vTPM\n }\n }\n }\n}\n```", + "Other": "1. In Azure Portal, open the VM and click Stop to deallocate it\n2. Go to Settings > Configuration\n3. Under Security type, select Trusted launch\n4. Check Secure Boot and vTPM\n5. Click Save\n6. Start the VM from the Overview page", + "Terraform": "```hcl\n# Patch the existing VM to enable Trusted Launch with Secure Boot and vTPM\nresource \"azapi_update_resource\" \"\" {\n type = \"Microsoft.Compute/virtualMachines@2022-11-01\"\n resource_id = \"\"\n\n body = jsonencode({\n properties = {\n securityProfile = {\n securityType = \"TrustedLaunch\" # Critical: sets VM security type to Trusted Launch\n uefiSettings = {\n secureBootEnabled = true # Critical: enables Secure Boot\n vTpmEnabled = true # Critical: enables vTPM\n }\n }\n }\n })\n}\n```" }, "Recommendation": { - "Text": "1. Go to Virtual Machines 2. For each VM, under Settings, click on Configuration on the left blade 3. Under Security Type, select 'Trusted Launch Virtual Machines' 4. Make sure Enable Secure Boot & Enable vTPM are checked 5. Click on Apply.", - "Url": "https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-existing-vm?tabs=portal#enable-trusted-launch-on-existing-vm" + "Text": "Adopt **defense in depth**: enable **Trusted Launch** with **Secure Boot** and **vTPM** on Gen2 VMs. Standardize on images with signed boot components and use supported sizes/OS. Enforce **least privilege** for administrators and enable attestation monitoring to prevent and detect boot-level tampering.", + "Url": "https://hub.prowler.com/check/vm_trusted_launch_enabled" } }, - "Categories": [], + "Categories": [ + "node-security" + ], "DependsOn": [], "RelatedTo": [], "Notes": "Secure Boot and vTPM are not currently supported for Azure Generation 1 VMs. IMPORTANT: Before enabling Secure Boot and vTPM on a Generation 2 VM which does not already have both enabled, it is highly recommended to create a restore point of the VM prior to remediation." diff --git a/prowler/providers/cloudflare/cloudflare_provider.py b/prowler/providers/cloudflare/cloudflare_provider.py index 689d2da2b1..4773501ce3 100644 --- a/prowler/providers/cloudflare/cloudflare_provider.py +++ b/prowler/providers/cloudflare/cloudflare_provider.py @@ -1,3 +1,4 @@ +import logging import os from typing import Iterable @@ -55,6 +56,9 @@ class CloudflareProvider(Provider): ): logger.info("Instantiating Cloudflare provider...") + # Mute HPACK library logs to prevent token leakage in debug mode + logging.getLogger("hpack").setLevel(logging.CRITICAL) + if config_content: self._audit_config = config_content else: diff --git a/prowler/providers/cloudflare/lib/service/service.py b/prowler/providers/cloudflare/lib/service/service.py index 8a1ea7484e..9eceef3592 100644 --- a/prowler/providers/cloudflare/lib/service/service.py +++ b/prowler/providers/cloudflare/lib/service/service.py @@ -1,5 +1,10 @@ +from concurrent.futures import ThreadPoolExecutor, as_completed + +from prowler.lib.logger import logger from prowler.providers.cloudflare.cloudflare_provider import CloudflareProvider +MAX_WORKERS = 10 + class CloudflareService: """Base class for Cloudflare services to share provider context.""" @@ -10,3 +15,29 @@ class CloudflareService: self.audit_config = provider.audit_config self.fixer_config = provider.fixer_config self.service = service.lower() if not service.islower() else service + + # Thread pool for __threading_call__ + self.thread_pool = ThreadPoolExecutor(max_workers=MAX_WORKERS) + + def __threading_call__(self, call, iterator): + """Execute a function across multiple items using threading.""" + items = list(iterator) if not isinstance(iterator, list) else iterator + + futures = {self.thread_pool.submit(call, item): item for item in items} + results = [] + + for future in as_completed(futures): + try: + result = future.result() + if result is not None: + results.append(result) + except Exception as error: + # Log unhandled exceptions from threaded calls + item = futures[future] + item_id = getattr(item, "id", str(item)) + logger.error( + f"{self.service} - Threading error processing {item_id}: " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return results diff --git a/prowler/providers/cloudflare/services/zone/zone_service.py b/prowler/providers/cloudflare/services/zone/zone_service.py index 1fdb133e68..040bd59e92 100644 --- a/prowler/providers/cloudflare/services/zone/zone_service.py +++ b/prowler/providers/cloudflare/services/zone/zone_service.py @@ -49,13 +49,13 @@ class Zone(CloudflareService): super().__init__(__class__.__name__, provider) self.zones: dict[str, "CloudflareZone"] = {} self._list_zones() - self._get_zones_settings() - self._get_zones_dnssec() - self._get_zones_universal_ssl() - self._get_zones_rate_limit_rules() - self._get_zones_bot_management() - self._get_zones_firewall_rules() - self._get_zones_waf_rulesets() + self.__threading_call__(self._get_zone_settings_threaded, self.zones.values()) + self.__threading_call__(self._get_zone_dnssec, self.zones.values()) + self.__threading_call__(self._get_zone_universal_ssl, self.zones.values()) + self.__threading_call__(self._get_zone_rate_limit_rules, self.zones.values()) + self.__threading_call__(self._get_zone_bot_management, self.zones.values()) + self.__threading_call__(self._get_zone_firewall_rules, self.zones.values()) + self.__threading_call__(self._get_zone_waf_rulesets, self.zones.values()) def _list_zones(self) -> None: """List all Cloudflare zones with their basic information.""" @@ -124,110 +124,89 @@ class Zone(CloudflareService): f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - def _get_zones_settings(self) -> None: - """Get settings for all zones.""" - logger.info("Zone - Getting zone settings...") - for zone in self.zones.values(): - try: - zone.settings = self._get_zone_settings(zone.id) - except Exception as error: - logger.error( - f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) + def _get_zone_settings_threaded(self, zone: "CloudflareZone") -> None: + """Get settings for a single zone (thread-safe).""" + try: + zone.settings = self._get_zone_settings(zone.id) + except Exception as error: + logger.error( + f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) - def _get_zones_dnssec(self) -> None: - """Get DNSSEC status for all zones.""" - logger.info("Zone - Getting DNSSEC status...") - for zone in self.zones.values(): - try: - dnssec = self.client.dns.dnssec.get(zone_id=zone.id) - zone.dnssec_status = getattr(dnssec, "status", None) - except Exception as error: - logger.error( - f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) + def _get_zone_dnssec(self, zone: "CloudflareZone") -> None: + """Get DNSSEC status for a single zone.""" + try: + dnssec = self.client.dns.dnssec.get(zone_id=zone.id) + zone.dnssec_status = getattr(dnssec, "status", None) + except Exception as error: + logger.error( + f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) - def _get_zones_universal_ssl(self) -> None: - """Get Universal SSL settings for all zones.""" - logger.info("Zone - Getting Universal SSL settings...") - for zone in self.zones.values(): - try: - universal_ssl = self.client.ssl.universal.settings.get(zone_id=zone.id) - zone.settings.universal_ssl_enabled = getattr( - universal_ssl, "enabled", False - ) - except Exception as error: - logger.error( - f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) + def _get_zone_universal_ssl(self, zone: "CloudflareZone") -> None: + """Get Universal SSL settings for a single zone.""" + try: + universal_ssl = self.client.ssl.universal.settings.get(zone_id=zone.id) + zone.settings.universal_ssl_enabled = getattr( + universal_ssl, "enabled", False + ) + except Exception as error: + logger.error( + f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) - def _get_zones_rate_limit_rules(self) -> None: - """Get rate limiting rules for all zones.""" - logger.info("Zone - Getting rate limit rules...") - for zone in self.zones.values(): - try: - seen_ruleset_ids: set[str] = set() - for ruleset in self.client.rulesets.list(zone_id=zone.id): - ruleset_id = getattr(ruleset, "id", "") - if ruleset_id in seen_ruleset_ids: - break - seen_ruleset_ids.add(ruleset_id) + def _get_zone_rate_limit_rules(self, zone: "CloudflareZone") -> None: + """Get rate limiting rules for a single zone.""" + try: + seen_ruleset_ids: set[str] = set() + for ruleset in self.client.rulesets.list(zone_id=zone.id): + ruleset_id = getattr(ruleset, "id", "") + if ruleset_id in seen_ruleset_ids: + break + seen_ruleset_ids.add(ruleset_id) - phase = getattr(ruleset, "phase", "") - if phase == "http_ratelimit": - try: - ruleset_detail = self.client.rulesets.get( - ruleset_id=ruleset_id, zone_id=zone.id - ) - rules = getattr(ruleset_detail, "rules", []) or [] - seen_rule_ids: set[str] = set() - for rule in rules: - rule_id = getattr(rule, "id", "") - if rule_id in seen_rule_ids: - break - seen_rule_ids.add(rule_id) - zone.rate_limit_rules.append( - CloudflareRateLimitRule( - id=rule_id, - description=getattr(rule, "description", None), - action=getattr(rule, "action", None), - enabled=getattr(rule, "enabled", True), - expression=getattr(rule, "expression", None), - ) + phase = getattr(ruleset, "phase", "") + if phase == "http_ratelimit": + try: + ruleset_detail = self.client.rulesets.get( + ruleset_id=ruleset_id, zone_id=zone.id + ) + rules = getattr(ruleset_detail, "rules", []) or [] + seen_rule_ids: set[str] = set() + for rule in rules: + rule_id = getattr(rule, "id", "") + if rule_id in seen_rule_ids: + break + seen_rule_ids.add(rule_id) + zone.rate_limit_rules.append( + CloudflareRateLimitRule( + id=rule_id, + description=getattr(rule, "description", None), + action=getattr(rule, "action", None), + enabled=getattr(rule, "enabled", True), + expression=getattr(rule, "expression", None), ) - except Exception as error: - logger.debug( - f"{zone.id} ruleset {ruleset_id} -- {error.__class__.__name__}: {error}" ) - except Exception as error: - logger.error( - f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) + except Exception as error: + logger.debug( + f"{zone.id} ruleset {ruleset_id} -- {error.__class__.__name__}: {error}" + ) + except Exception as error: + logger.error( + f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) - def _get_zones_bot_management(self) -> None: - """Get Bot Management settings for all zones.""" - logger.info("Zone - Getting Bot Management settings...") - for zone in self.zones.values(): - try: - bot_management = self.client.bot_management.get(zone_id=zone.id) - zone.settings.bot_fight_mode_enabled = getattr( - bot_management, "fight_mode", False - ) - except Exception as error: - logger.error( - f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) - - def _get_zones_firewall_rules(self) -> None: - """Get firewall rules for all zones.""" - logger.info("Zone - Getting firewall rules...") - for zone in self.zones.values(): - try: - self._get_zone_firewall_rules(zone) - except Exception as error: - logger.error( - f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) + def _get_zone_bot_management(self, zone: "CloudflareZone") -> None: + """Get Bot Management settings for a single zone.""" + try: + bot_management = self.client.bot_management.get(zone_id=zone.id) + zone.settings.bot_fight_mode_enabled = getattr( + bot_management, "fight_mode", False + ) + except Exception as error: + logger.error( + f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) def _get_zone_firewall_rules(self, zone: "CloudflareZone") -> None: """List firewall rules from custom rulesets for a zone.""" @@ -282,17 +261,6 @@ class Zone(CloudflareService): f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - def _get_zones_waf_rulesets(self) -> None: - """Get WAF rulesets for all zones.""" - logger.info("Zone - Getting WAF rulesets...") - for zone in self.zones.values(): - try: - self._get_zone_waf_rulesets(zone) - except Exception as error: - logger.error( - f"{zone.id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) - def _get_zone_waf_rulesets(self, zone: "CloudflareZone") -> None: """List WAF rulesets for a zone using the rulesets API.""" seen_ids: set[str] = set() diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 0ffe15e825..6f29c0b800 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -274,6 +274,18 @@ class Provider(ABC): config_path=arguments.config_file, fixer_config=fixer_config, ) + elif "image" in provider_class_name.lower(): + provider_class( + images=arguments.images, + image_list_file=arguments.image_list_file, + scanners=arguments.scanners, + image_config_scanners=arguments.image_config_scanners, + trivy_severity=arguments.trivy_severity, + ignore_unfixed=arguments.ignore_unfixed, + timeout=arguments.timeout, + config_path=arguments.config_file, + fixer_config=fixer_config, + ) elif "mongodbatlas" in provider_class_name.lower(): provider_class( atlas_public_key=arguments.atlas_public_key, @@ -297,6 +309,9 @@ class Provider(ABC): elif "openstack" in provider_class_name.lower(): provider_class( clouds_yaml_file=getattr(arguments, "clouds_yaml_file", None), + clouds_yaml_content=getattr( + arguments, "clouds_yaml_content", None + ), clouds_yaml_cloud=getattr(arguments, "clouds_yaml_cloud", None), auth_url=getattr(arguments, "os_auth_url", None), identity_api_version=getattr( diff --git a/prowler/providers/gcp/services/apikeys/apikeys_api_restrictions_configured/apikeys_api_restrictions_configured.metadata.json b/prowler/providers/gcp/services/apikeys/apikeys_api_restrictions_configured/apikeys_api_restrictions_configured.metadata.json index 499a660014..dcc4145c91 100644 --- a/prowler/providers/gcp/services/apikeys/apikeys_api_restrictions_configured/apikeys_api_restrictions_configured.metadata.json +++ b/prowler/providers/gcp/services/apikeys/apikeys_api_restrictions_configured/apikeys_api_restrictions_configured.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "apikeys_api_restrictions_configured", - "CheckTitle": "Ensure API Keys Are Restricted to Only APIs That Application Needs Access", + "CheckTitle": "API key is restricted to specific Google APIs", "CheckType": [], "ServiceName": "apikeys", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "API Key", - "ResourceGroup": "IAM", - "Description": "API Keys should only be used for services in cases where other authentication methods are unavailable. If they are in use it is recommended to rotate API keys every 90 days.", - "Risk": "Google Cloud Platform (GCP) API keys are simple encrypted strings that don't identify the user or the application that performs the API request. GCP API keys are typically accessible to clients, as they can be viewed publicly from within a browser, making it easy to discover and capture API keys.", + "Severity": "high", + "ResourceType": "apikeys.googleapis.com/Key", + "Description": "Google Cloud API keys have **API restrictions** limiting calls to specific services. The finding checks that keys are restricted to named Google APIs and do not include the broad `cloudapis.googleapis.com`, indicating keys are scoped only to intended use.", + "Risk": "Unrestricted keys-or ones allowing `cloudapis.googleapis.com`-expand attack surface. A leaked key can call many APIs without identity, enabling data exposure, unintended changes on permissive endpoints, and **quota/billing exhaustion**, impacting confidentiality, integrity, and availability.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/docs/authentication/api-keys", + "https://cloud.google.com/docs/authentication/api-keys-best-practices" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud services api-keys update --api-target=service=", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudAPI/check-for-api-key-api-restrictions.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to APIs & Services > Credentials\n2. Click the API key name to edit it\n3. In API restrictions, select \"Restrict key\"\n4. Choose only the specific API(s) needed (do not select \"All Google APIs\")\n5. Click Save", + "Terraform": "```hcl\nresource \"google_apikeys_key\" \"key\" {\n display_name = \"\"\n\n restrictions {\n api_targets {\n service = \"\" # Critical: restricts the key to a specific API, removing any \"All Google APIs\" (cloudapis.googleapis.com)\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that the usage of your Google Cloud API keys is restricted to specific APIs such as Cloud Key Management Service (KMS) API, Cloud Storage API, Cloud Monitoring API and/or Cloud Logging API. All Google Cloud API keys that are being used for production applications should use API restrictions. In order to follow cloud security best practices and reduce the attack surface, Google Cloud API keys should be restricted to call only those APIs required by your application.", - "Url": "https://cloud.google.com/docs/authentication/api-keys" + "Text": "Apply **least privilege**: restrict each API key to only the specific APIs it must access and never include `cloudapis.googleapis.com`. Add **application restrictions** (referrers, IPs, app IDs), rotate keys, and monitor usage. Prefer **service accounts** or short-lived tokens for production as part of **defense in depth**.", + "Url": "https://hub.prowler.com/check/apikeys_api_restrictions_configured" } }, - "Categories": [], + "Categories": [ + "secrets", + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/apikeys/apikeys_key_exists/apikeys_key_exists.metadata.json b/prowler/providers/gcp/services/apikeys/apikeys_key_exists/apikeys_key_exists.metadata.json index 1623abb17c..883b71f294 100644 --- a/prowler/providers/gcp/services/apikeys/apikeys_key_exists/apikeys_key_exists.metadata.json +++ b/prowler/providers/gcp/services/apikeys/apikeys_key_exists/apikeys_key_exists.metadata.json @@ -1,30 +1,34 @@ { "Provider": "gcp", "CheckID": "apikeys_key_exists", - "CheckTitle": "Ensure API Keys Only Exist for Active Services", + "CheckTitle": "Project has no active API keys", "CheckType": [], "ServiceName": "apikeys", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "API Key", - "ResourceGroup": "IAM", - "Description": "API Keys should only be used for services in cases where other authentication methods are unavailable. Unused keys with their permissions in tact may still exist within a project. Keys are insecure because they can be viewed publicly, such as from within a browser, or they can be accessed on a device where the key resides. It is recommended to use standard authentication flow instead.", - "Risk": "Security risks involved in using API-Keys appear below: API keys are simple encrypted strings, API keys do not identify the user or the application making the API request, API keys are typically accessible to clients, making it easy to discover and steal an API key.", + "ResourceType": "apikeys.googleapis.com/Key", + "Description": "Google Cloud projects are evaluated for **active API keys**. The finding indicates whether any keys exist and are enabled in the project, regardless of restrictions or usage.", + "Risk": "Active API keys are **bearer tokens** often exposed in clients and lack **user identity**. Compromise enables unauthorized API calls causing data exposure (C), unauthorized changes (I), quota/service exhaustion (A), and **billing abuse**. Keys can be harvested from code, logs, or intercepted requests.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/docs/authentication/api-keys" + ], "Remediation": { "Code": { - "CLI": "gcloud alpha services api-keys delete", + "CLI": "", "NativeIaC": "", - "Other": "", + "Other": "1. In the Google Cloud Console, go to APIs & Services > Credentials\n2. In the API keys section, for each key with state Active, click its row > Delete > Confirm\n3. Repeat until no API keys remain in the project\n4. Refresh the page to verify the API keys list is empty", "Terraform": "" }, "Recommendation": { - "Text": "To avoid the security risk in using API keys, it is recommended to use standard authentication flow instead.", - "Url": "https://cloud.google.com/docs/authentication/api-keys" + "Text": "Prefer **service accounts** with short-lived credentials or **OAuth 2.0**, enforcing **least privilege**. *If keys are necessary*, restrict by API and application, store them in a secrets manager, rotate and revoke promptly, monitor with alerts as **defense in depth**, and remove unused keys.", + "Url": "https://hub.prowler.com/check/apikeys_key_exists" } }, - "Categories": [], + "Categories": [ + "secrets" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/apikeys/apikeys_key_rotated_in_90_days/apikeys_key_rotated_in_90_days.metadata.json b/prowler/providers/gcp/services/apikeys/apikeys_key_rotated_in_90_days/apikeys_key_rotated_in_90_days.metadata.json index 057661e946..bfabd2c012 100644 --- a/prowler/providers/gcp/services/apikeys/apikeys_key_rotated_in_90_days/apikeys_key_rotated_in_90_days.metadata.json +++ b/prowler/providers/gcp/services/apikeys/apikeys_key_rotated_in_90_days/apikeys_key_rotated_in_90_days.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "apikeys_key_rotated_in_90_days", - "CheckTitle": "Ensure API Keys Are Rotated Every 90 Days", + "CheckTitle": "API key was created within the last 90 days", "CheckType": [], "ServiceName": "apikeys", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "API Key", - "ResourceGroup": "IAM", - "Description": "API Keys should only be used for services in cases where other authentication methods are unavailable. If they are in use it is recommended to rotate API keys every 90 days.", - "Risk": "Once a Google Cloud API key is compromised, it can be used indefinitely unless the project owner revokes or regenerates that key.", + "ResourceType": "apikeys.googleapis.com/Key", + "Description": "**Google Cloud API keys** are evaluated for **rotation age** using their `creation_time`. Keys created within the last `90` days are treated as recently rotated; keys older than `90` days are treated as overdue.", + "Risk": "Stale, long-lived **API keys** are **bearer tokens** without IAM context. If exposed, attackers can invoke allowed APIs, enabling data exfiltration (**confidentiality**), unauthorized changes (**integrity**), and quota/billing abuse or disruption (**availability**). Lack of rotation prolongs misuse.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/docs/authentication/api-keys", + "https://cloud.google.com/docs/authentication/api-keys-best-practices" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudAPI/rotate-api-keys.html", + "Other": "1. In the Google Cloud Console, go to APIs & Services > Credentials\n2. Under API keys, click the key older than 90 days\n3. Click Rotate key, then click Create to generate the new key\n4. Copy the new key string and update your applications to use it\n5. In the Previous key section, click Delete the previous key\n6. Repeat for any remaining API keys older than 90 days until only keys created within 90 days remain", "Terraform": "" }, "Recommendation": { - "Text": "Ensure that all your Google Cloud API keys are regularly regenerated (rotated) in order to meet security and compliance requirements. By default, it is recommended to rotate keys every 90 days. Google Cloud Platform (GCP) API keys are simple, encrypted strings that can be used when calling specific APIs that don't need to access private user data. API keys are typically used to track API requests associated with your GCP project for quota and billing. Rotating GCP API keys will substantially reduce the window of opportunity for exploits and ensure that data can't be accessed with an outdated key that might have been lost, cracked, or stolen.", - "Url": "https://cloud.google.com/docs/authentication/api-keys" + "Text": "Rotate API keys at least every `90` days. Prefer **service accounts** or OAuth with short-lived credentials. Enforce **API** and **application/IP restrictions**. Store keys in a **secrets manager**, avoid client-side embedding, and monitor usage. Apply **least privilege** and **defense in depth**.", + "Url": "https://hub.prowler.com/check/apikeys_key_rotated_in_90_days" } }, - "Categories": [], + "Categories": [ + "secrets" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/bigquery/bigquery_dataset_cmk_encryption/bigquery_dataset_cmk_encryption.metadata.json b/prowler/providers/gcp/services/bigquery/bigquery_dataset_cmk_encryption/bigquery_dataset_cmk_encryption.metadata.json index e1aeaf70aa..014ef01f1e 100644 --- a/prowler/providers/gcp/services/bigquery/bigquery_dataset_cmk_encryption/bigquery_dataset_cmk_encryption.metadata.json +++ b/prowler/providers/gcp/services/bigquery/bigquery_dataset_cmk_encryption/bigquery_dataset_cmk_encryption.metadata.json @@ -1,27 +1,29 @@ { "Provider": "gcp", "CheckID": "bigquery_dataset_cmk_encryption", - "CheckTitle": "Ensure BigQuery datasets are encrypted with Customer-Managed Keys (CMKs).", + "CheckTitle": "BigQuery dataset is encrypted with Customer-Managed Keys (CMKs)", "CheckType": [], "ServiceName": "bigquery", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Dataset", - "ResourceGroup": "analytics", - "Description": "Ensure BigQuery datasets are encrypted with Customer-Managed Keys (CMKs) in order to have a more granular control over data encryption/decryption process.", - "Risk": "If you want to have greater control, Customer-managed encryption keys (CMEK) can be used as encryption key management solution for BigQuery Data Sets.", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/BigQuery/enable-table-encryption-with-cmks.html", + "ResourceType": "bigquery.googleapis.com/Dataset", + "Description": "**BigQuery datasets** use **Customer-Managed Encryption Keys** (`CMEK`) rather than Google-managed encryption. The evaluation identifies datasets configured to use a customer-managed key for data-at-rest protection.", + "Risk": "Without **CMEK**, organizations lose **cryptographic control** of data at rest, weakening **confidentiality** and **access governance**. Lack of custom **key rotation**, **revocation** (kill switch), and location control hinders incident response and may conflict with data-sovereignty requirements.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/bigquery/docs/customer-managed-encryption" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "bq update --default_kms_key projects//locations//keyRings//cryptoKeys/ --dataset ", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_11", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/ensure-gcp-big-query-tables-are-encrypted-with-customer-supplied-encryption-keys-csek-1#terraform" + "Other": "1. In Google Cloud Console, go to BigQuery\n2. In Explorer, select your project and click the dataset \n3. Click Edit details\n4. Under Encryption, select Customer-managed key and choose or paste the key: projects//locations//keyRings//cryptoKeys/\n5. Click Save", + "Terraform": "```hcl\nresource \"google_bigquery_dataset\" \"\" {\n dataset_id = \"\"\n location = \"\"\n\n # Critical: apply CMEK at the dataset level so the check passes\n default_encryption_configuration {\n kms_key_name = \"projects//locations//keyRings//cryptoKeys/\" # Critical: sets CMEK\n }\n}\n```" }, "Recommendation": { - "Text": "Encrypting datasets with Cloud KMS Customer-Managed Keys (CMKs) will allow for a more granular control over data encryption/decryption process.", - "Url": "https://cloud.google.com/bigquery/docs/customer-managed-encryption" + "Text": "Protect datasets with **CMEK** via Cloud KMS:\n- Set a dataset or project default key\n- Align key location with dataset region\n- Enforce **least privilege** and separation of duties on key usage\n- Rotate keys and define revocation procedures\n- Audit key usage and use org policies to require CMEK", + "Url": "https://hub.prowler.com/check/bigquery_dataset_cmk_encryption" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/bigquery/bigquery_dataset_public_access/bigquery_dataset_public_access.metadata.json b/prowler/providers/gcp/services/bigquery/bigquery_dataset_public_access/bigquery_dataset_public_access.metadata.json index e01d2bde27..b67e87c65b 100644 --- a/prowler/providers/gcp/services/bigquery/bigquery_dataset_public_access/bigquery_dataset_public_access.metadata.json +++ b/prowler/providers/gcp/services/bigquery/bigquery_dataset_public_access/bigquery_dataset_public_access.metadata.json @@ -1,30 +1,39 @@ { "Provider": "gcp", "CheckID": "bigquery_dataset_public_access", - "CheckTitle": "Ensure That BigQuery Datasets Are Not Anonymously or Publicly Accessible.", + "CheckTitle": "BigQuery dataset is not publicly accessible", "CheckType": [], "ServiceName": "bigquery", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Dataset", - "ResourceGroup": "analytics", - "Description": "Ensure That BigQuery Datasets Are Not Anonymously or Publicly Accessible.", - "Risk": "Granting permissions to allUsers or allAuthenticatedUsers allows anyone to access the dataset. Such access might not be desirable if sensitive data is being stored in the dataset. Therefore, ensure that anonymous and/or public access to a dataset is not allowed.", + "ResourceType": "bigquery.googleapis.com/Dataset", + "Description": "BigQuery datasets are evaluated for exposure to **public identities**. The finding highlights datasets that grant any role to `allUsers` or `allAuthenticatedUsers`, which makes the dataset publicly accessible beyond intended principals.", + "Risk": "Public dataset access erodes **confidentiality** by enabling unrestricted reads and metadata discovery. Attackers can query and copy data for **exfiltration** and use schema details for **lateral movement**. It can also trigger unexpected **costs** from abusive or high-volume queries.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/bigquery/docs/dataset-access-controls", + "https://github.com/forseti-security/forseti-security/issues/3406", + "https://support.icompaas.com/support/solutions/articles/62000233051-7-1-ensure-bigquery-datasets-are-not-anonymously-or-publicly-accessible-automated-", + "https://securitylabs.datadoghq.com/cloud-security-atlas/vulnerabilities/bigquery-publicly-accessible-dataset/", + "https://cloud.google.com/blog/products/identity-security/automatic-dlp-for-bigquery" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/BigQuery/publicly-accessible-big-query-datasets.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/bc_gcp_general_3#terraform" + "Other": "1. Open Google Cloud Console and go to BigQuery\n2. In Explorer, select the dataset\n3. Click SHARING > Permissions\n4. Remove principals named AllUsers and AllAuthenticatedUsers\n5. Click Save", + "Terraform": "```hcl\n# Ensure the dataset is not public by removing public principals from IAM (authoritative for this role)\nresource \"google_bigquery_dataset_iam_binding\" \"\" {\n dataset_id = \"\"\n role = \"roles/bigquery.dataViewer\"\n members = [\n \"user:\" # CRITICAL: exclude allUsers/allAuthenticatedUsers to remove public access\n ]\n}\n```" }, "Recommendation": { - "Text": "It is recommended that the IAM policy on BigQuery datasets does not allow anonymous and/or public access.", - "Url": "https://cloud.google.com/bigquery/docs/customer-managed-encryption" + "Text": "Apply **least privilege** on datasets:\n- Remove `allUsers` and `allAuthenticatedUsers`\n- Grant access only to required groups or service accounts\n- Use **authorized views** or controlled listings for sharing\n- Review access regularly and enforce org policies that block public identities for **defense in depth**", + "Url": "https://hub.prowler.com/check/bigquery_dataset_public_access" } }, - "Categories": [], + "Categories": [ + "internet-exposed", + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/bigquery/bigquery_table_cmk_encryption/bigquery_table_cmk_encryption.metadata.json b/prowler/providers/gcp/services/bigquery/bigquery_table_cmk_encryption/bigquery_table_cmk_encryption.metadata.json index 9a3fbfc8be..8af8af36ef 100644 --- a/prowler/providers/gcp/services/bigquery/bigquery_table_cmk_encryption/bigquery_table_cmk_encryption.metadata.json +++ b/prowler/providers/gcp/services/bigquery/bigquery_table_cmk_encryption/bigquery_table_cmk_encryption.metadata.json @@ -1,30 +1,34 @@ { "Provider": "gcp", "CheckID": "bigquery_table_cmk_encryption", - "CheckTitle": "Ensure BigQuery tables are encrypted with Customer-Managed Keys (CMKs).", + "CheckTitle": "BigQuery table is encrypted with a Customer-Managed Key (CMK)", "CheckType": [], "ServiceName": "bigquery", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Table", - "ResourceGroup": "analytics", - "Description": "Ensure BigQuery tables are encrypted with Customer-Managed Keys (CMKs) in order to have a more granular control over data encryption/decryption process.", - "Risk": "If you want to have greater control, Customer-managed encryption keys (CMEK) can be used as encryption key management solution for BigQuery Tables.", + "ResourceType": "bigquery.googleapis.com/Table", + "Description": "**BigQuery tables** use **customer-managed encryption keys** (`CMEK`) for at-rest encryption. The evaluation identifies tables lacking a configured **Cloud KMS key**, indicating use of default Google-managed encryption instead.", + "Risk": "Without `CMEK`, encryption keys are not under your control, reducing **key custody** and auditability. You can't enforce **region-bound keys**, custom **rotation**, or revoke access by disabling a key, weakening **confidentiality** and **compliance**. A compromised account may retain data access without an external KMS gate.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/bigquery/docs/customer-managed-encryption" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "bq cp -f --destination_kms_key projects//locations//keyRings//cryptoKeys/ . .", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/BigQuery/enable-table-encryption-with-cmks.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/ensure-gcp-big-query-tables-are-encrypted-with-customer-supplied-encryption-keys-csek#terraform" + "Other": "1. In Google Cloud Console, go to BigQuery and open the Query editor\n2. Run:\n ```sql\n ALTER TABLE .\n SET OPTIONS (kms_key_name = 'projects//locations//keyRings//cryptoKeys/');\n ```\n3. Click Run\n4. Open the table Details and verify Customer-managed key shows the specified key", + "Terraform": "```hcl\nresource \"google_bigquery_table\" \"\" {\n dataset_id = \"\"\n table_id = \"\"\n\n schema = jsonencode([\n { name = \"id\", type = \"INT64\", mode = \"NULLABLE\" }\n ])\n\n # Critical: enforce CMEK on the table using the specified KMS key\n encryption_configuration {\n kms_key_name = \"projects//locations//keyRings//cryptoKeys/\" # Enables CMK encryption\n }\n}\n```" }, "Recommendation": { - "Text": "Encrypting tables with Cloud KMS Customer-Managed Keys (CMKs) will allow for a more granular control over data encryption/decryption process.", - "Url": "https://cloud.google.com/bigquery/docs/customer-managed-encryption" + "Text": "Protect BigQuery tables with **Cloud KMS CMEK** to retain key ownership and control.\n- Set dataset/project default keys\n- Match key location to dataset region\n- Enforce least-privilege on KMS and monitor usage\n- Rotate keys and define revocation procedures\n- Apply org policies to require CMEK and restrict permissible key projects", + "Url": "https://hub.prowler.com/check/bigquery_table_cmk_encryption" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_automated_backups/cloudsql_instance_automated_backups.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_automated_backups/cloudsql_instance_automated_backups.metadata.json index a450f39a7e..318b018df6 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_automated_backups/cloudsql_instance_automated_backups.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_automated_backups/cloudsql_instance_automated_backups.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_automated_backups", - "CheckTitle": "Ensure That Cloud SQL Database Instances Are Configured With Automated Backups", + "CheckTitle": "Cloud SQL database instance has automated backups configured", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That Cloud SQL Database Instances Are Configured With Automated Backups", - "Risk": "Backups provide a way to restore a Cloud SQL instance to recover lost data or recover from a problem with that instance. Automated backups need to be set for any instance that contains data that should be protected from loss or damage. This recommendation is applicable for SQL Server, PostgreSql, MySql generation 1 and MySql generation 2 instances.", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL instances** are checked for **automated backups** being configured to run on a schedule and support point-in-time recovery.", + "Risk": "Absent **automated backups**, unintended deletes, corruption, or ransomware can become irreversible. This degrades data **integrity** and **availability**, removes point-in-time recovery options, and widens `RPO`/`RTO`, causing prolonged outages and incomplete restoration after incidents or schema changes.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/enable-automated-backups.html", + "https://cloud.google.com/sql/docs/mysql/backup-recovery/backups", + "https://cloud.google.com/sql/docs/postgres/configure-ssl-instance/" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch --backup-start-time <[HH:MM]>", + "CLI": "gcloud sql instances patch --backup-start-time ", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/enable-automated-backups.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Click your instance name, then click Edit\n3. In the Backups section, enable Automated backups and set a Start time\n4. Click Save to apply", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"POSTGRES_14\"\n region = \"\"\n\n settings {\n tier = \"db-custom-1-3840\"\n\n backup_configuration {\n enabled = true # Critical: turns on automated backups\n start_time = \"02:00\" # Critical: required to enable backups and set start time\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to have all SQL database instances set to enable automated backups.", - "Url": "https://cloud.google.com/sql/docs/postgres/configure-ssl-instance/" + "Text": "Enable **automated backups** on all Cloud SQL instances holding important data. Set retention and schedules to meet `RPO`/`RTO`, and enable point-in-time recovery. Apply **least privilege** to backup access, use **separation of duties**, consider cross-region resilience, and regularly test restores with monitoring and alerts for failures.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_automated_backups" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_local_infile_flag/cloudsql_instance_mysql_local_infile_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_local_infile_flag/cloudsql_instance_mysql_local_infile_flag.metadata.json index 6a3779e604..79b7614822 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_local_infile_flag/cloudsql_instance_mysql_local_infile_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_local_infile_flag/cloudsql_instance_mysql_local_infile_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_mysql_local_infile_flag", - "CheckTitle": "Ensure That the Local_infile Database Flag for a Cloud SQL MySQL Instance Is Set to Off", + "CheckTitle": "Cloud SQL MySQL instance has the local_infile database flag set to off", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That the Local_infile Database Flag for a Cloud SQL MySQL Instance Is Set to Off", - "Risk": "The local_infile flag controls the server-side LOCAL capability for LOAD DATA statements. Depending on the local_infile setting, the server refuses or permits local data loading by clients that have LOCAL enabled on the client side.", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for MySQL** instances are evaluated for the `local_infile` database flag being explicitly set to `off`, disabling use of `LOAD DATA LOCAL`.\n\nInstances where `local_infile` is absent or not `off` are identified.", + "Risk": "With `local_infile` enabled, clients can send local files via `LOAD DATA LOCAL`. A stolen credential or SQL injection can coerce clients to leak files and mass-ingest unvetted data, compromising **confidentiality** and **integrity**, and aiding lateral movement through secrets imported into the database.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/disable-local-infile-flag.html", + "https://cloud.google.com/sql/docs/mysql/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags local_infile=off", + "CLI": "gcloud sql instances patch --database-flags=local_infile=off", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/disable-local-infile-flag.html", - "Terraform": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_1#terraform" + "Other": "1. In Google Cloud Console, go to SQL\n2. Select the MySQL instance and click Edit\n3. In Database flags, add or locate \"local_infile\" and set it to Off\n4. Click Save to apply changes", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"MYSQL_8_0\"\n region = \"\"\n\n settings {\n tier = \"\"\n # Critical: disables LOCAL INFILE to pass the check\n database_flags {\n name = \"local_infile\" # sets the specific flag\n value = \"off\" # required value for compliance\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set the local_infile database flag for a Cloud SQL MySQL instance to off.", - "Url": "https://cloud.google.com/sql/docs/mysql/flags" + "Text": "Keep `local_infile` set to `off`. Use governed import channels (e.g., controlled object storage imports) and enforce **least privilege** for bulk-loading. Apply **separation of duties** between ingestion and admin roles, validate file sources and formats, and monitor high-volume loads. *If ever needed, enable only briefly for vetted tasks.*", + "Url": "https://hub.prowler.com/check/cloudsql_instance_mysql_local_infile_flag" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_skip_show_database_flag/cloudsql_instance_mysql_skip_show_database_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_skip_show_database_flag/cloudsql_instance_mysql_skip_show_database_flag.metadata.json index 5b5dda6342..d50c19d28b 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_skip_show_database_flag/cloudsql_instance_mysql_skip_show_database_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_skip_show_database_flag/cloudsql_instance_mysql_skip_show_database_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_mysql_skip_show_database_flag", - "CheckTitle": "Ensure Skip_show_database Database Flag for Cloud SQL MySQL Instance Is Set to On", + "CheckTitle": "Cloud SQL MySQL instance has skip_show_database flag set to on", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure Skip_show_database Database Flag for Cloud SQL MySQL Instance Is Set to On", - "Risk": "'skip_show_database' database flag prevents people from using the SHOW DATABASES statement if they do not have the SHOW DATABASES privilege.", + "Severity": "low", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL MySQL** instances configure the `skip_show_database` database flag to `on`, limiting use of `SHOW DATABASES` to accounts with the `SHOW DATABASES` privilege.", + "Risk": "Without `skip_show_database` set to `on`, database names can be exposed to unprivileged users, reducing **confidentiality**. Attackers can perform schema **enumeration** and targeted probing, enabling **lateral movement** and privilege escalation against specific datasets.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/enable-skip-show-database-flag.html", + "https://cloud.google.com/sql/docs/mysql/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags skip_show_database=on", + "CLI": "gcloud sql instances patch --database-flags=skip_show_database=on", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/enable-skip-show-database-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Open your MySQL instance and click Edit\n3. Under Flags, click Add item, select skip_show_database, set value to ON\n4. Click Save", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"MYSQL_8_0\"\n region = \"\"\n\n settings {\n tier = \"db-custom-1-3840\"\n\n database_flags {\n name = \"skip_show_database\" # Critical: enforce hiding databases from users without SHOW DATABASES privilege\n value = \"on\" # Critical: set flag to 'on' to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set skip_show_database database flag for Cloud SQL Mysql instance to on.", - "Url": "https://cloud.google.com/sql/docs/mysql/flags" + "Text": "Set `skip_show_database` to `on` for all Cloud SQL MySQL instances. Enforce **least privilege** by granting `SHOW DATABASES` only when necessary and reviewing roles regularly. Use **defense in depth**: monitor access and admin actions, and plan changes in maintenance windows as flag updates may trigger restarts.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_mysql_skip_show_database_flag" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_enable_pgaudit_flag/cloudsql_instance_postgres_enable_pgaudit_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_enable_pgaudit_flag/cloudsql_instance_postgres_enable_pgaudit_flag.metadata.json index 922d5136fc..6791e595a3 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_enable_pgaudit_flag/cloudsql_instance_postgres_enable_pgaudit_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_enable_pgaudit_flag/cloudsql_instance_postgres_enable_pgaudit_flag.metadata.json @@ -1,30 +1,37 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_enable_pgaudit_flag", - "CheckTitle": "Ensure That 'cloudsql.enable_pgaudit' Database Flag for each Cloud Sql Postgresql Instance Is Set to 'on' For Centralized Logging", + "CheckTitle": "Cloud SQL PostgreSQL instance has 'cloudsql.enable_pgaudit' flag set to 'on'", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That 'cloudsql.enable_pgaudit' Database Flag for each Cloud Sql Postgresql Instance Is Set to 'on' For Centralized Logging", - "Risk": "Ensure cloudsql.enable_pgaudit database flag for Cloud SQL PostgreSQL instance is set to on to allow for centralized logging.", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** instances are evaluated for the database flag `cloudsql.enable_pgaudit` being set to `on`", + "Risk": "Without `cloudsql.enable_pgaudit`, **database activity** lacks granular audit trails. Undetected reads/writes enable insider abuse, credential reuse, or SQL injection without evidence, harming **confidentiality** and **integrity**. Poor traceability slows incident response, forensics, and undermines compliance.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/postgre-sql-audit-flag.html", + "https://cloud.google.com/sql/docs/postgres/flags", + "https://cloud.google.com/sql/docs/postgres/pg-audit" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags cloudsql.enable_pgaudit=On", + "CLI": "gcloud sql instances patch --database-flags cloudsql.enable_pgaudit=on", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/postgre-sql-audit-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to SQL\n2. Select your PostgreSQL instance and click Edit\n3. In Database flags, click Add item\n4. Set Flag to cloudsql.enable_pgaudit and Value to on\n5. Click Save and restart if prompted", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"us-central1\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"db-custom-1-3840\"\n database_flags {\n name = \"cloudsql.enable_pgaudit\" # Critical: enable pgAudit\n value = \"on\" # Critical: set flag to 'on' to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "As numerous other recommendations in this section consist of turning on flags for logging purposes, your organization will need a way to manage these logs. You may have a solution already in place. If you do not, consider installing and enabling the open source pgaudit extension within PostgreSQL and enabling its corresponding flag of cloudsql.enable_pgaudit. This flag and installing the extension enables database auditing in PostgreSQL through the open-source pgAudit extension. This extension provides detailed session and object logging to comply with government, financial, & ISO standards and provides auditing capabilities to mitigate threats by monitoring security events on the instance. Enabling the flag and settings later in this recommendation will send these logs to Google Logs Explorer so that you can access them in a central location.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Enable `cloudsql.enable_pgaudit` and configure **pgAudit** to log required classes (e.g., `read`, `write`, `ddl`) under least privilege. Centralize logs, enforce retention and RBAC, and monitor with alerts. *Scope auditing to sensitive data to reduce noise and overhead, and review coverage regularly.*", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_enable_pgaudit_flag" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_connections_flag/cloudsql_instance_postgres_log_connections_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_connections_flag/cloudsql_instance_postgres_log_connections_flag.metadata.json index 95f4d094a0..8c7ea9968f 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_connections_flag/cloudsql_instance_postgres_log_connections_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_connections_flag/cloudsql_instance_postgres_log_connections_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_connections_flag", - "CheckTitle": "Ensure That the Log_connections Database Flag for Cloud SQL PostgreSQL Instance Is Set to On", + "CheckTitle": "Cloud SQL PostgreSQL instance has log_connections flag set to on", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That the Log_connections Database Flag for Cloud SQL PostgreSQL Instance Is Set to On", - "Risk": "Enabling the log_connections setting causes each attempted connection to the server to be logged, along with successful completion of client authentication. This parameter cannot be changed after the session starts.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** instances have the `log_connections` flag set to `on`, causing the server to record every connection attempt and the result of client authentication.", + "Risk": "Without connection logs, unauthorized access attempts can go unnoticed. Attackers may brute-force or reuse credentials without audit evidence, enabling stealthy data access (**confidentiality**), changes via compromised accounts (**integrity**), and connection floods that impact service (**availability**).", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/enable-log-connections-flag.html", + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_connections=on", + "CLI": "gcloud sql instances patch --database-flags=log_connections=on", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/enable-log-connections-flag.html", - "Terraform": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_3#terraform" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Open your PostgreSQL instance and click Edit\n3. In Flags, click Add item, select log_connections, set value to on\n4. Click Save and confirm the restart", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"POSTGRES_14\"\n region = \"\"\n\n settings {\n tier = \"db-f1-micro\"\n\n # Critical: enables connection logging to pass the check\n database_flags {\n name = \"log_connections\" # critical\n value = \"on\" # critical\n }\n }\n}\n```" }, "Recommendation": { - "Text": "PostgreSQL does not log attempted connections by default. Enabling the log_connections setting will create log entries for each attempted connection as well as successful completion of client authentication which can be useful in troubleshooting issues and to determine any unusual connection attempts to the server.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Enable `log_connections`=`on` for all PostgreSQL instances.\n- Apply **defense in depth**: also capture disconnects and audit events\n- Centralize logs, retain them, and alert on anomalies\n- Enforce **least privilege** and strong authentication to reduce exposure and improve detection", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_connections_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_disconnections_flag/cloudsql_instance_postgres_log_disconnections_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_disconnections_flag/cloudsql_instance_postgres_log_disconnections_flag.metadata.json index b80ebe79c5..b6fd6835b3 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_disconnections_flag/cloudsql_instance_postgres_log_disconnections_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_disconnections_flag/cloudsql_instance_postgres_log_disconnections_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_disconnections_flag", - "CheckTitle": "Ensure That the log_disconnections Database Flag for Cloud SQL PostgreSQL Instance Is Set to On", + "CheckTitle": "Cloud SQL PostgreSQL instance has log_disconnections flag set to on", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That the log_disconnections Database Flag for Cloud SQL PostgreSQL Instance Is Set to On", - "Risk": "PostgreSQL does not log session details such as duration and session end by default. Enabling the log_disconnections setting will create log entries at the end of each session which can be useful in troubleshooting issues and determine any unusual activity across a time period.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** instances have the `log_disconnections` flag set to `on`, creating a record each time a client session ends, including its duration and status", + "Risk": "Without **disconnection logs**, session lifecycles lack visibility, obscuring **credential misuse**, **session hijacking**, and short-lived data exfiltration.\n\nWeak audit trails hinder correlation and forensics, undermining confidentiality and integrity and slowing incident response.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/enable-log-connections-flag.html", + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_disconnections=on", + "CLI": "gcloud sql instances patch --database-flags=log_disconnections=on", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/enable-log-connections-flag.html", - "Terraform": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_4#terraform" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Click your PostgreSQL instance\n3. Click Edit\n4. In Database flags, click Add item\n5. Select log_disconnections and set value to on\n6. Click Save and confirm the restart", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"\"\n\n # Critical: enable disconnect logging\n database_flags {\n name = \"log_disconnections\" # sets the required flag\n value = \"on\" # ensures the check passes\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Enabling the log_disconnections setting logs the end of each session, including the session duration.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Enable `log_disconnections=on` to ensure complete session auditing.\n- Pair with `log_connections` and a consistent `log_line_prefix`\n- Centralize and retain logs; alert on anomalies\n- Apply **defense in depth** with routine review of access and audit events", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_disconnections_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_error_verbosity_flag/cloudsql_instance_postgres_log_error_verbosity_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_error_verbosity_flag/cloudsql_instance_postgres_log_error_verbosity_flag.metadata.json index ceedd32501..4bc0693679 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_error_verbosity_flag/cloudsql_instance_postgres_log_error_verbosity_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_error_verbosity_flag/cloudsql_instance_postgres_log_error_verbosity_flag.metadata.json @@ -1,30 +1,34 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_error_verbosity_flag", - "CheckTitle": "Ensure Log_error_verbosity Database Flag for Cloud SQL PostgreSQL Instance Is Set to DEFAULT or Stricter", + "CheckTitle": "Cloud SQL PostgreSQL instance has log_error_verbosity flag set to default", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure Log_error_verbosity Database Flag for Cloud SQL PostgreSQL Instance Is Set to DEFAULT or Stricter", - "Risk": "The log_error_verbosity flag controls the verbosity/details of messages logged.TERSE excludes the logging of DETAIL, HINT, QUERY, and CONTEXT error information. VERBOSE output includes the SQLSTATE error code, source code file name, function name, and line number that generated the error. Ensure an appropriate value is set to 'DEFAULT' or stricter.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** evaluates the `log_error_verbosity` database flag and expects the value `default`.\n\nConfigurations using `terse` or `verbose` are flagged as deviations.", + "Risk": "With `verbose`, logs may reveal SQLSTATE, code paths, and function details, aiding recon and leaking metadata (**confidentiality**). With `terse`, missing DETAIL/HINT/CONTEXT hinders detection and forensics, reducing **integrity** of investigations and **availability** of operational insight.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_error_verbosity=default", + "CLI": "gcloud sql instances patch --database-flags=log_error_verbosity=default", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Open the PostgreSQL instance\n3. Click Edit\n4. In Database flags, set log_error_verbosity to default (or remove the custom value)\n5. Click Save (the instance may restart)", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"main\" {\n name = \"\"\n region = \"\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"\"\n\n database_flags {\n name = \"log_error_verbosity\"\n value = \"default\" # Critical: sets the flag to default to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Auditing helps in troubleshooting operational problems and also permits forensic analysis. If log_error_verbosity is not set to the correct value, too many details or too few details may be logged. This flag should be configured with a value of 'DEFAULT' or stricter. This recommendation is applicable to PostgreSQL database instances.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Set `log_error_verbosity` to `default` to balance **data minimization** and **observability**.\n\n- Avoid `verbose` in production; restrict log access (least privilege)\n- Avoid `terse` except briefly to curb noise\n- Centralize logs with retention and tamper protection for **defense in depth**", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_error_verbosity_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_duration_statement_flag/cloudsql_instance_postgres_log_min_duration_statement_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_duration_statement_flag/cloudsql_instance_postgres_log_min_duration_statement_flag.metadata.json index da6af1ee58..856f7e4163 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_duration_statement_flag/cloudsql_instance_postgres_log_min_duration_statement_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_duration_statement_flag/cloudsql_instance_postgres_log_min_duration_statement_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_min_duration_statement_flag", - "CheckTitle": "Ensure that the Log_min_duration_statement Flag for a Cloud SQL PostgreSQL Instance Is Set to -1", + "CheckTitle": "Cloud SQL PostgreSQL instance has the log_min_duration_statement flag set to -1", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure that the Log_min_duration_statement Flag for a Cloud SQL PostgreSQL Instance Is Set to -1", - "Risk": "The log_min_duration_statement flag defines the minimum amount of execution time of a statement in milliseconds where the total duration of the statement is logged. Ensure that log_min_duration_statement is disabled, i.e., a value of -1 is set.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** evaluates whether `log_min_duration_statement` is set to `-1`, disabling **statement duration logging**.", + "Risk": "When duration-based statement logging is enabled, logs can capture full SQL with literals, exposing **confidential data** in log stores. Adversaries or over-privileged users could harvest secrets/PII, profile schemas, and support **lateral movement**. Heavy logging can also raise costs and impact availability under load.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/configure-log-min-error-statement-flag.html", + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_min_duration_statement=-1", + "CLI": "gcloud sql instances patch --database-flags=log_min_duration_statement=-1", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/configure-log-min-error-statement-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to SQL > Instances\n2. Select your PostgreSQL instance\n3. Click Edit\n4. In Database flags, add or edit log_min_duration_statement and set it to -1\n5. Click Save (the instance may restart)", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"\"\n # Critical: set to -1 to pass the check\n database_flags {\n name = \"log_min_duration_statement\" # Critical\n value = \"-1\" # Critical\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Logging SQL statements may include sensitive information that should not be recorded in logs. This recommendation is applicable to PostgreSQL database instances.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Keep `log_min_duration_statement` at `-1` in production to avoid writing sensitive query text to logs. Apply **least privilege** to log access, enforce **data minimization** with redaction and short retention. *If troubleshooting is required*, enable narrowly and temporarily, prefer non-prod, and monitor access.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_min_duration_statement_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_error_statement_flag/cloudsql_instance_postgres_log_min_error_statement_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_error_statement_flag/cloudsql_instance_postgres_log_min_error_statement_flag.metadata.json index e70bfb9c9f..cf57b9250e 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_error_statement_flag/cloudsql_instance_postgres_log_min_error_statement_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_error_statement_flag/cloudsql_instance_postgres_log_min_error_statement_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_min_error_statement_flag", - "CheckTitle": "Ensure that the Log_min_error_statement Flag for a Cloud SQL PostgreSQL Instance Is Set Appropriately", + "CheckTitle": "Cloud SQL for PostgreSQL instance has log_min_error_statement set to error", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure that the Log_min_error_statement Flag for a Cloud SQL PostgreSQL Instance Is Set Appropriately", - "Risk": "The log_min_error_statement flag defines the minimum message severity level that are considered as an error statement. Messages for error statements are logged with the SQL statement. Valid values include DEBUG5, DEBUG4, DEBUG3, DEBUG2, DEBUG1, INFO, NOTICE, WARNING, ERROR, LOG, FATAL, and PANIC. Each severity level includes the subsequent levels mentioned above. Ensure a value of ERROR or stricter is set.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** uses the `log_min_error_statement` flag and expects it set to `error`, the severity threshold that controls when SQL text is logged with error messages.", + "Risk": "An incorrect threshold skews visibility and exposure:\n- Lower than `error`: logs excessive SQL, risking **confidentiality** loss and alert noise (monitoring availability).\n- Higher than `error`: omits query context for real errors, weakening audit trail **integrity** and incident response.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/configure-log-min-error-statement-flag.html", + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_min_error_statement=error", + "CLI": "gcloud sql instances patch --database-flags=log_min_error_statement=error", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/configure-log-min-error-statement-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances and open your PostgreSQL instance\n2. Click Edit\n3. In Database flags, click Add item, select log_min_error_statement, set value to error\n4. Click Save (the instance will restart)", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"POSTGRES_14\"\n region = \"us-central1\"\n\n settings {\n tier = \"db-custom-2-7680\"\n database_flags {\n name = \"log_min_error_statement\" # critical: requires minimum 'error' level\n value = \"error\" # sets the flag to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Auditing helps in troubleshooting operational problems and also permits forensic analysis. If log_min_error_statement is not set to the correct value, messages may not be classified as error messages appropriately. Considering general log messages as error messages would make is difficult to find actual errors and considering only stricter severity levels as error messages may skip actual errors to log their SQL statements. The log_min_error_statement flag should be set to ERROR or stricter.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Set `log_min_error_statement` to `error` to balance insight and exposure. Enforce a **logging policy** that limits sensitive data in queries and supports **defense in depth**. Periodically review severity and retention to match workload and compliance needs and maintain reliable forensic readiness.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_min_error_statement_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_messages_flag/cloudsql_instance_postgres_log_min_messages_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_messages_flag/cloudsql_instance_postgres_log_min_messages_flag.metadata.json index b54b9ee8c3..acedebebd9 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_messages_flag/cloudsql_instance_postgres_log_min_messages_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_messages_flag/cloudsql_instance_postgres_log_min_messages_flag.metadata.json @@ -1,30 +1,34 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_min_messages_flag", - "CheckTitle": "Ensure that the Log_min_messages Flag for a Cloud SQL PostgreSQL Instance Is Set Appropriately", + "CheckTitle": "Cloud SQL PostgreSQL instance has the log_min_messages flag set to WARNING or higher", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure that the Log_min_messages Flag for a Cloud SQL PostgreSQL Instance Is Set Appropriately", - "Risk": "Auditing helps in troubleshooting operational problems and also permits forensic analysis. If log_min_messages is not set to the correct value, messages may not be classified as error messages appropriately. An organization will need to decide their own threshold for logging log_min_messages flag.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** instances are evaluated for the `log_min_messages` flag being set to a sufficiently high severity. Instances with the flag missing or set below `ERROR` (e.g., `DEBUG*`, `INFO`, `NOTICE`) are identified.", + "Risk": "Insufficient `log_min_messages` severity degrades **audit log integrity**, causing real failures to be treated as non-errors or lack context. This delays detection and impairs **forensics**, enabling unnoticed data tampering or repeated faulty operations, impacting the **integrity** and **availability** of the service.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_min_messages=warning", + "CLI": "gcloud sql instances patch --database-flags=log_min_messages=warning", "NativeIaC": "", - "Other": "", - "Terraform": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_4#terraform" + "Other": "1. In Google Cloud Console, go to SQL and select your PostgreSQL instance\n2. Click Edit\n3. In Database flags, click Add item\n4. Choose log_min_messages and set value to warning\n5. Click Save and confirm the restart", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"db-f1-micro\"\n\n # Critical: sets the minimum PostgreSQL log level to WARNING (or higher) to pass the check\n database_flags {\n name = \"log_min_messages\" # sets the flag\n value = \"warning\" # acceptable level (WARNING or higher)\n }\n }\n}\n```" }, "Recommendation": { - "Text": "The log_min_messages flag defines the minimum message severity level that is considered as an error statement. Messages for error statements are logged with the SQL statement. Valid values include DEBUG5, DEBUG4, DEBUG3, DEBUG2, DEBUG1, INFO, NOTICE, WARNING, ERROR, LOG, FATAL, and PANIC. Each severity level includes the subsequent levels mentioned above. ERROR is considered the best practice setting. Changes should only be made in accordance with the organization's logging policy.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Set `log_min_messages` to **ERROR or stricter** to ensure error statements are captured with context. Align with centralized logging, retention, and review processes. Prefer **defense in depth** by preserving actionable error telemetry, while balancing verbosity and cost per your logging policy.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_min_messages_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_statement_flag/cloudsql_instance_postgres_log_statement_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_statement_flag/cloudsql_instance_postgres_log_statement_flag.metadata.json index 9f178ba2d5..96b9f7de55 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_statement_flag/cloudsql_instance_postgres_log_statement_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_statement_flag/cloudsql_instance_postgres_log_statement_flag.metadata.json @@ -1,30 +1,34 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_statement_flag", - "CheckTitle": "Ensure That the Log_statement Database Flag for Cloud SQL PostgreSQL Instance Is Set Appropriately", + "CheckTitle": "Cloud SQL PostgreSQL instance has 'log_statement' flag set to 'ddl'", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That the Log_statement Database Flag for Cloud SQL PostgreSQL Instance Is Set Appropriately", - "Risk": "Auditing helps in forensic analysis. If log_statement is not set to the correct value, too many statements may be logged leading to issues in finding the relevant information from the logs, or too few statements may be logged with relevant information missing from the logs.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** instances have `log_statement` set to `ddl`, recording only data definition statements in server logs", + "Risk": "Missing `ddl` logging leaves schema changes untracked, undermining **integrity** and hindering investigations.\n\nExcessive logging (e.g., `all`) can inflate volumes, impair **availability**, raise costs, and leak sensitive values, harming **confidentiality**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_statement=ddl", + "CLI": "gcloud sql instances patch --database-flags=log_statement=ddl", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to SQL > Instances and open \n2. Click Edit\n3. In Database flags, click Add item\n4. Select log_statement and set value to ddl\n5. Click Save", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"db-custom-2-7680\"\n\n # Critical: sets PostgreSQL 'log_statement' to 'ddl' to pass the check\n database_flags {\n name = \"log_statement\" # required flag name\n value = \"ddl\" # required value\n }\n }\n}\n```" }, "Recommendation": { - "Text": "The value ddl logs all data definition statements. A value of 'ddl' is recommended unless otherwise directed by your organization's logging policy.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Configure `log_statement` to `ddl` to capture schema changes without excessive noise. Apply **defense in depth**: use targeted auditing for data access, restrict and monitor log access with **least privilege**, and enforce log retention and rotation to protect availability.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_statement_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_private_ip_assignment/cloudsql_instance_private_ip_assignment.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_private_ip_assignment/cloudsql_instance_private_ip_assignment.metadata.json index 0d2bab9aec..35aad807ec 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_private_ip_assignment/cloudsql_instance_private_ip_assignment.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_private_ip_assignment/cloudsql_instance_private_ip_assignment.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_private_ip_assignment", - "CheckTitle": "Ensure Instance IP assignment is set to private", + "CheckTitle": "Cloud SQL instance has no public IP addresses", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure Instance IP assignment is set to private", - "Risk": "Instance addresses can be public IP or private IP. Public IP means that the instance is accessible through the public internet. In contrast, instances using only private IP are not accessible through the public internet, but are accessible through a Virtual Private Cloud (VPC). Limiting network access to your database will limit potential attacks.", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "Cloud SQL instances are evaluated for IP assignment, highlighting instances that have any **public IP** instead of being restricted to **private IP** only.", + "Risk": "**Public database endpoints** expose services to Internet scanning, brute-force logins, and exploit attempts. A compromise can cause data exfiltration (**confidentiality**), unauthorized changes (**integrity**), and outages or DDoS impact (**availability**), while bypassing VPC isolation and enabling lateral movement.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/sql/docs/mysql/configure-private-ip", + "https://docs.cloud.google.com/sql/docs/sqlserver/recommender-disable-public-ip" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud beta sql instances patch --project= --network=projects//global/networks/ --no-assign-ip", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL and open \n2. Click Connections > Networking\n3. Uncheck Public IP\n4. Check Private IP and select your VPC network\n5. If prompted, click Set up connection to create private services access, then continue\n6. Click Save and wait for the instance to restart", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"db-f1-micro\"\n\n ip_configuration {\n ipv4_enabled = false # Critical: disables public IP\n private_network = \"projects//global/networks/\" # Critical: enables private IP on the specified VPC\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Setting databases access only to private will reduce attack surface.", - "Url": "https://cloud.google.com/sql/docs/mysql/configure-private-ip" + "Text": "Use **private IP-only** connectivity for databases. Remove public IPs, segment access to required VPCs/subnets, and enforce **least privilege** with strong auth and TLS. For external access, use secure private channels (VPN/Interconnect) or a hardened bastion. Monitor connections as part of **defense in depth**.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_private_ip_assignment" } }, - "Categories": [], + "Categories": [ + "internet-exposed" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_access/cloudsql_instance_public_access.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_access/cloudsql_instance_public_access.metadata.json index c1d2963974..516042bb12 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_access/cloudsql_instance_public_access.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_access/cloudsql_instance_public_access.metadata.json @@ -1,27 +1,30 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_public_access", - "CheckTitle": "Ensure That Cloud SQL Database Instances Do Not Implicitly Whitelist All Public IP Addresses ", + "CheckTitle": "Cloud SQL instance does not allow 0.0.0.0/0 in authorized networks", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That Cloud SQL Database Instances Do Not Implicitly Whitelist All Public IP Addresses ", - "Risk": "To minimize attack surface on a Database server instance, only trusted/known and required IP(s) should be white-listed to connect to it. An authorized network should not have IPs/networks configured to 0.0.0.0/0 which will allow access to the instance from anywhere in the world. Note that authorized networks apply only to instances with public IPs.", + "Severity": "critical", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL authorized networks** are checked for the open CIDR `0.0.0.0/0` on instances using a public IP.\n\nThe finding flags configurations where a catch-all entry exists instead of specific client ranges.", + "Risk": "Allowing `0.0.0.0/0` makes the database reachable from the Internet, degrading **confidentiality** and **availability**. Attackers can brute-force credentials, probe for vulnerable endpoints, exfiltrate data via unauthorized queries, and trigger resource exhaustion through automated scanning.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/publicly-accessible-cloud-sql-instances.html", + "https://cloud.google.com/sql/docs/mysql/connection-org-policy" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch --authorized-networks=IP_ADDR1,IP_ADDR2...", + "CLI": "gcloud sql instances patch --authorized-networks=\"\"", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/publicly-accessible-cloud-sql-instances.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to SQL > Instances and select your instance\n2. Open the Connections tab\n3. Under Authorized networks, delete the entry 0.0.0.0/0\n4. Click Save", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"\"\n\n settings {\n tier = \"\"\n\n ip_configuration {\n authorized_networks {\n value = \"\" # Critical: remove 0.0.0.0/0; allow only specific CIDR to pass the check\n }\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Database Server should accept connections only from trusted Network(s)/IP(s) and restrict access from public IP addresses.", - "Url": "https://cloud.google.com/sql/docs/mysql/connection-org-policy" + "Text": "Enforce **least privilege** network access:\n- Remove `0.0.0.0/0`; allow only trusted, fixed IP ranges\n- Prefer **private IP** or **Private Service Connect** with VPC controls\n- Use proxied access (Cloud SQL Auth Proxy) over direct public connections\n- Apply org policies to prevent broad allowlists", + "Url": "https://hub.prowler.com/check/cloudsql_instance_public_access" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_ip/cloudsql_instance_public_ip.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_ip/cloudsql_instance_public_ip.metadata.json index cfdf4e1198..e6b70e9737 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_ip/cloudsql_instance_public_ip.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_ip/cloudsql_instance_public_ip.metadata.json @@ -1,27 +1,30 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_public_ip", - "CheckTitle": "Check for Cloud SQL Database Instances with Public IPs", + "CheckTitle": "Cloud SQL database instance does not have a public IP address", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Check for Cloud SQL Database Instances with Public IPs", - "Risk": "To lower the organization's attack surface, Cloud SQL databases should not have public IPs. Private IPs provide improved network security and lower latency for your application.", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/sql-database-instances-with-public-ips.html", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL instances** are evaluated for exposure via **public IP addresses** instead of `private IP` connectivity within a VPC.\n\nInstances with an externally routable database endpoint are surfaced.", + "Risk": "**Public DB endpoints** expand attack surface:\n- Credential brute force and SQL injection threaten **confidentiality** and **integrity**\n- Internet DDoS reduces **availability**\n- Exposure bypasses VPC controls, easing **lateral movement** and data exfiltration", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/sql/docs/mysql/configure-private-ip", + "https://cloud.google.com/sql/docs/mysql/recommender-disable-public-ip" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch --project --network= --no-assign-ip", + "CLI": "gcloud beta sql instances patch --project= --network=projects//global/networks/ --no-assign-ip", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_11", - "Terraform": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_11#terraform" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances and select \n2. Open Connections > Networking\n3. Check Private IP and select the VPC network; if prompted, click Set up connection to create the private service connection\n4. Uncheck Public IP\n5. Click Save", + "Terraform": "```hcl\n# Cloud SQL instance without public IP\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"MYSQL_8_0\"\n\n settings {\n tier = \"db-f1-micro\"\n ip_configuration {\n ipv4_enabled = false # Critical: disables public (IPv4) IP\n private_network = \"projects//global/networks/\" # Critical: ensures private IP via specified VPC\n }\n }\n}\n```" }, "Recommendation": { - "Text": "To lower the organization's attack surface, Cloud SQL databases should not have public IPs. Private IPs provide improved network security and lower latency for your application.", - "Url": "https://cloud.google.com/sql/docs/mysql/configure-private-ip" + "Text": "Prefer **private IP** and disable public endpoints. Access databases over VPC, VPN/Interconnect, or **Private Service Connect**. If `authorized networks` are required, restrict to specific sources-never `0.0.0.0/0`. Enforce **least privilege** IAM, use Cloud SQL connectors/proxy, and layer **defense in depth** with network controls and monitoring.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_public_ip" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_contained_database_authentication_flag/cloudsql_instance_sqlserver_contained_database_authentication_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_contained_database_authentication_flag/cloudsql_instance_sqlserver_contained_database_authentication_flag.metadata.json index 20b338e3cc..67a2316db0 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_contained_database_authentication_flag/cloudsql_instance_sqlserver_contained_database_authentication_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_contained_database_authentication_flag/cloudsql_instance_sqlserver_contained_database_authentication_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_contained_database_authentication_flag", - "CheckTitle": "Ensure that the 'contained database authentication' database flag for Cloud SQL on the SQL Server instance is set to 'off' ", + "CheckTitle": "Cloud SQL for SQL Server instance has 'contained database authentication' flag set to off", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure that the 'contained database authentication' database flag for Cloud SQL on the SQL Server instance is set to 'off' ", - "Risk": "A contained database includes all database settings and metadata required to define the database and has no configuration dependencies on the instance of the Database Engine where the database is installed. Users can connect to the database without authenticating a login at the Database Engine level. Isolating the database from the Database Engine makes it possible to easily move the database to another instance of SQL Server. Contained databases have some unique threats that should be understood and mitigated by SQL Server Database Engine administrators. Most of the threats are related to the USER WITH PASSWORD authentication process, which moves the authentication boundary from the Database Engine level to the database level, hence this is recommended to disable this flag. This recommendation is applicable to SQL Server database instances.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "Cloud SQL for SQL Server instances are evaluated for the **contained database authentication** setting. The check inspects the `contained database authentication` flag and expects its value to be `off`.", + "Risk": "Enabling contained authentication moves identity checks to the database, bypassing server-level logins and policies. This weakens centralized controls and auditing, enables password spraying on contained users, and can persist users across copies, increasing unauthorized data access and tampering risk to **confidentiality** and **integrity**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/disable-contained-database-authentication-flag.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch --database-flags contained database authentication=off", + "CLI": "gcloud sql instances patch --database-flags=\"contained database authentication\"=off", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/disable-contained-database-authentication-flag.html", - "Terraform": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_10#terraform" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Open the SQL Server instance\n3. Click Edit\n4. In Database flags, add or edit: contained database authentication = Off\n5. Click Save (the instance may restart)", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n\n settings {\n tier = \"\"\n database_flags {\n name = \"contained database authentication\" # critical: target flag\n value = \"off\" # critical: disable contained DB auth\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set contained database authentication database flag for Cloud SQL on the SQL Server instance to off.", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Keep `contained database authentication` set to `off`. Centralize authentication and auditing at the server layer or via directory integration, and apply **least privilege**. Avoid `USER WITH PASSWORD` contained users. If containment is unavoidable, tightly scope usage, enforce strong credentials, and monitor login activity.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_contained_database_authentication_flag" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag.metadata.json index 2d341c6893..ea03e35241 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag", - "CheckTitle": "Ensure that the 'cross db ownership chaining' database flag for Cloud SQL SQL Server instance is set to 'off'", + "CheckTitle": "Cloud SQL SQL Server instance has 'cross db ownership chaining' flag set to off", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure that the 'cross db ownership chaining' database flag for Cloud SQL SQL Server instance is set to 'off'", - "Risk": "Use the cross db ownership for chaining option to configure cross-database ownership chaining for an instance of Microsoft SQL Server. This server option allows you to control cross-database ownership chaining at the database level or to allow cross- database ownership chaining for all databases. Enabling cross db ownership is not recommended unless all of the databases hosted by the instance of SQL Server must participate in cross-database ownership chaining and you are aware of the security implications of this setting.", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL SQL Server** instances are evaluated for the `cross db ownership chaining` server flag. The finding identifies SQL Server instances where this flag isn't set to `off`, meaning cross-database ownership chaining is permitted.", + "Risk": "Allowing cross-database ownership chaining erodes database boundaries, impacting **confidentiality** and **integrity**. Users with privileges in one database can traverse ownership chains to access or modify objects in others, enabling **privilege escalation**, **lateral movement**, and unauthorized data exposure.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/disable-cross-db-ownership-chaining-flag.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags cross db ownership=off", + "CLI": "gcloud sql instances patch --database-flags '\"cross db ownership chaining\"=off'", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/disable-cross-db-ownership-chaining-flag.html", - "Terraform": "" + "Other": "1. In the Google Cloud Console, go to SQL > Instances\n2. Open the SQL Server instance () and click Edit\n3. Scroll to Flags and click Add item (or edit if present)\n4. Select cross db ownership chaining and set value to off\n5. Click Save and restart if prompted", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n\n settings {\n tier = \"\"\n\n # Critical: ensures the flag is OFF to pass the check\n database_flags {\n name = \"cross db ownership chaining\" # disables cross-database ownership chaining\n value = \"off\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set cross db ownership chaining database flag for Cloud SQL SQL Server instance to off.", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Keep `cross db ownership chaining` set to `off` to maintain database isolation. Enforce **least privilege** with explicit per-database permissions and **separation of duties**. Prefer controlled execution patterns (e.g., signed modules) over implicit trusts, and periodically review flags and access. *This flag is deprecated-do not enable it.*", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag" } }, - "Categories": [], + "Categories": [ + "identity-access", + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_external_scripts_enabled_flag/cloudsql_instance_sqlserver_external_scripts_enabled_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_external_scripts_enabled_flag/cloudsql_instance_sqlserver_external_scripts_enabled_flag.metadata.json index c7c36aab66..119a287573 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_external_scripts_enabled_flag/cloudsql_instance_sqlserver_external_scripts_enabled_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_external_scripts_enabled_flag/cloudsql_instance_sqlserver_external_scripts_enabled_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_external_scripts_enabled_flag", - "CheckTitle": "Ensure 'external scripts enabled' database flag for Cloud SQL SQL Server instance is set to 'off'", + "CheckTitle": "Cloud SQL SQL Server instance has 'external scripts enabled' flag set to 'off'", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure 'external scripts enabled' database flag for Cloud SQL SQL Server instance is set to 'off'", - "Risk": "external scripts enabled enable the execution of scripts with certain remote language extensions. This property is OFF by default. When Advanced Analytics Services is installed, setup can optionally set this property to true. As the External Scripts Enabled feature allows scripts external to SQL such as files located in an R library to be executed, which could adversely affect the security of the system, hence this should be disabled.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for SQL Server** instances have the `external scripts enabled` database flag set to `off`", + "Risk": "Allowing **external scripts** lets SQL invoke language extensions (e.g., R/Python), enabling arbitrary code execution. This can cause data exfiltration (**confidentiality**), tampered query results (**integrity**), and resource exhaustion or service degradation (**availability**), and may facilitate lateral movement from the database layer.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/disable-external-scripts-enabled-flag.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags external scripts enabled=off", + "CLI": "gcloud sql instances patch --database-flags=\"external scripts enabled\"=off", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/disable-external-scripts-enabled-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to SQL > Instances and select the SQL Server instance\n2. Click Edit\n3. In the Flags section, add or locate \"external scripts enabled\"\n4. Set its value to Off\n5. Click Save to apply (the instance may restart)", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n\n settings {\n tier = \"db-custom-2-7680\"\n\n # Critical: disables external scripts on the SQL Server instance\n database_flags {\n name = \"external scripts enabled\" # sets the flag\n value = \"off\" # required value to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set external scripts enabled database flag for Cloud SQL SQL Server instance to off", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Keep `external scripts enabled` set to `off`. Apply **least privilege** and **defense in depth** by disabling code-execution features in the database. If analytics are required, use isolated instances, restrict outbound network access, and enforce change control and auditing to prevent misuse.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_external_scripts_enabled_flag" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_remote_access_flag/cloudsql_instance_sqlserver_remote_access_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_remote_access_flag/cloudsql_instance_sqlserver_remote_access_flag.metadata.json index 77477646b7..95b80be870 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_remote_access_flag/cloudsql_instance_sqlserver_remote_access_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_remote_access_flag/cloudsql_instance_sqlserver_remote_access_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_remote_access_flag", - "CheckTitle": "Ensure 'remote access' database flag for Cloud SQL SQL Server instance is set to 'off'", + "CheckTitle": "Cloud SQL SQL Server instance has 'remote access' database flag set to 'off'", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure 'remote access' database flag for Cloud SQL SQL Server instance is set to 'off'", - "Risk": "The remote access option controls the execution of stored procedures from local or remote servers on which instances of SQL Server are running. This default value for this option is 1. This grants permission to run local stored procedures from remote servers or remote stored procedures from the local server. To prevent local stored procedures from being run from a remote server or remote stored procedures from being run on the local server, this must be disabled. The Remote Access option controls the execution of local stored procedures on remote servers or remote stored procedures on local server. 'Remote access' functionality can be abused to launch a Denial-of- Service (DoS) attack on remote servers by off-loading query processing to a target, hence this should be disabled. This recommendation is applicable to SQL Server database instances.", + "Severity": "medium", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for SQL Server** instances where the `remote access` database flag is `on`, allowing remote procedure calls between servers", + "Risk": "Enabling **remote procedure calls** expands exposure: untrusted servers can invoke stored procedures, leading to **data exfiltration** (confidentiality), unauthorized changes (**integrity**), and **DoS** via resource-heavy remote execution (**availability**). It can also enable lateral movement.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/disable-remote-access-flag.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud sql instances patch --database-flags=\"remote access\"=off", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/disable-remote-access-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Open the SQL Server instance () and click Edit\n3. Scroll to Database flags and click Add item\n4. Select \"remote access\" and set value to off\n5. Click Save and confirm the restart when prompted\n6. Verify under Overview > Database flags that \"remote access\" = off", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n region = \"\"\n\n settings {\n tier = \"\"\n\n # Critical: disables SQL Server remote access to pass the check\n database_flags {\n name = \"remote access\"\n value = \"off\" # sets the flag to off\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set remote access database flag for Cloud SQL SQL Server instance to off.", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Set `remote access` to `off` to reduce the attack surface. Apply **least privilege** and **defense in depth**: avoid remote stored procedures; if business-required, allow only trusted peers, enforce strong authentication, audit calls, and monitor for abuse.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_remote_access_flag" } }, - "Categories": [], + "Categories": [ + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_trace_flag/cloudsql_instance_sqlserver_trace_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_trace_flag/cloudsql_instance_sqlserver_trace_flag.metadata.json index 9df75263ba..4594aedad3 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_trace_flag/cloudsql_instance_sqlserver_trace_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_trace_flag/cloudsql_instance_sqlserver_trace_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_trace_flag", - "CheckTitle": "Ensure '3625 (trace flag)' database flag for all Cloud SQL Server instances is set to 'on' ", + "CheckTitle": "Cloud SQL for SQL Server instance has trace flag 3625 set to 'on'", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure '3625 (trace flag)' database flag for all Cloud SQL Server instances is set to 'on' ", - "Risk": "Microsoft SQL Trace Flags are frequently used to diagnose performance issues or to debug stored procedures or complex computer systems, but they may also be recommended by Microsoft Support to address behavior that is negatively impacting a specific workload. All documented trace flags and those recommended by Microsoft Support are fully supported in a production environment when used as directed. 3625(trace log) Limits the amount of information returned to users who are not members of the sysadmin fixed server role, by masking the parameters of some error messages using '******'. Setting this in a Google Cloud flag for the instance allows for security through obscurity and prevents the disclosure of sensitive information, hence this is recommended to set this flag globally to on to prevent the flag having been left off, or changed by bad actors. This recommendation is applicable to SQL Server database instances.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for SQL Server** instances have the `3625 (trace flag)` database flag set to `on`", + "Risk": "Without `3625` enabled, SQL errors can reveal parameters and object names to non-admins, weakening **confidentiality** and aiding targeted **injection**, account enumeration, and data discovery. Leaked context helps craft exploits and pivot attacks, ultimately risking data integrity and availability.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/disable-3625-trace-flag.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch --database-flags 3625=on", + "CLI": "gcloud sql instances patch --database-flags=3625=on", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/disable-3625-trace-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances and open \n2. Click Edit\n3. In Flags, click Add item\n4. Select 3625 (trace flag) and set value to on\n5. Click Save and confirm the restart", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n\n settings {\n tier = \"\"\n\n # Critical: enable SQL Server trace flag 3625\n # This sets the flag to 'on' so the check passes\n database_flags {\n name = \"3625\"\n value = \"on\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set 3625 (trace flag) database flag for Cloud SQL SQL Server instance to on.", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Set trace flag `3625` to `on` for all SQL Server instances in Cloud SQL to limit error details for non-admins. Apply **least privilege**, practice **defense in depth** with application-level error handling, and centralize diagnostics in logs rather than returning verbose messages to clients.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_trace_flag" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_connections_flag/cloudsql_instance_sqlserver_user_connections_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_connections_flag/cloudsql_instance_sqlserver_user_connections_flag.metadata.json index cce517561a..4d45efcdd9 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_connections_flag/cloudsql_instance_sqlserver_user_connections_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_connections_flag/cloudsql_instance_sqlserver_user_connections_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_user_connections_flag", - "CheckTitle": "Ensure 'user Connections' Database Flag for Cloud Sql Sql Server Instance Is Set to a Non-limiting Value", + "CheckTitle": "Cloud SQL SQL Server instance has the 'user connections' database flag set to 0", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure 'user Connections' Database Flag for Cloud Sql Sql Server Instance Is Set to a Non-limiting Value", - "Risk": "The user connections option specifies the maximum number of simultaneous user connections that are allowed on an instance of SQL Server. The actual number of user connections allowed also depends on the version of SQL Server that you are using, and also the limits of your application or applications and hardware. SQL Server allows a maximum of 32,767 user connections. Because user connections is by default a self- configuring value, with SQL Server adjusting the maximum number of user connections automatically as needed, up to the maximum value allowable. For example, if only 10 users are logged in, 10 user connection objects are allocated. In most cases, you do not have to change the value for this option. The default is 0, which means that the maximum (32,767) user connections are allowed. However if there is a number defined here that limits connections, SQL Server will not allow anymore above this limit. If the connections are at the limit, any new requests will be dropped, potentially causing lost data or outages for those using the database.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for SQL Server** instances are evaluated to ensure the `user connections` database flag is set to `0` (unlimited), avoiding any artificial cap on concurrent user sessions", + "Risk": "A capped `user connections` value can exhaust available sessions, causing login failures, aborted transactions, and timeouts. This reduces **availability**, can delay administrative access, and may lead to **integrity** issues from failed or inconsistent retries under load.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/configure-user-connection-flag.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags user connections=0", + "CLI": "gcloud sql instances patch --database-flags='\"user connections\"=0'", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/configure-user-connection-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Open the SQL Server instance and click Edit\n3. In Database flags, click Add item, select \"user connections\", set value to 0\n4. Click Save (the instance may restart)", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n\n settings {\n tier = \"db-custom-1-3840\"\n\n # Critical: ensure the 'user connections' flag is set to 0 to pass the check\n database_flags {\n name = \"user connections\" # Critical line: target flag\n value = \"0\" # Critical line: set to 0\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to check the user connections for a Cloud SQL SQL Server instance to ensure that it is not artificially limiting connections.", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Set `user connections` to `0` to prevent artificial limits. Preserve **availability** with **connection pooling**, controlled retries, and **capacity planning** based on peak usage. *If a cap is required*, size it with ample headroom, monitor connection counts, and review regularly.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_user_connections_flag" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_options_flag/cloudsql_instance_sqlserver_user_options_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_options_flag/cloudsql_instance_sqlserver_user_options_flag.metadata.json index 8b91366494..8027863415 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_options_flag/cloudsql_instance_sqlserver_user_options_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_options_flag/cloudsql_instance_sqlserver_user_options_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_user_options_flag", - "CheckTitle": "Ensure 'user options' database flag for Cloud SQL SQL Server instance is not configured", + "CheckTitle": "Cloud SQL for SQL Server instance does not have the 'user options' flag configured", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure 'user options' database flag for Cloud SQL SQL Server instance is not configured", - "Risk": "The user options option specifies global defaults for all users. A list of default query processing options is established for the duration of a user's work session. The user options option allows you to change the default values of the SET options (if the server's default settings are not appropriate). A user can override these defaults by using the SET statement. You can configure user options dynamically for new logins. After you change the setting of user options, new login sessions use the new setting, current login sessions are not affected.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for SQL Server** instances are evaluated for the `user options` database flag configured with any value.\n\nThis flag sets global defaults for session `SET` behaviors; the check identifies instances where this global override is present.", + "Risk": "Global `user options` changes affect all sessions, impacting **data integrity** and **availability**. Disabling safe **ANSI behaviors** or enabling **implicit transactions** can alter NULL comparisons and error handling, leading to inconsistent results, lock contention, and application failures, reducing predictability and complicating auditing.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/user-options-flag-not-configured.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud sql instances patch --clear-database-flags", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/user-options-flag-not-configured.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL and open your SQL Server instance\n2. Click Edit\n3. In Database flags, locate 'user options' and click the X to remove it\n4. Click Save\n5. Allow the instance to restart to apply the change", + "Terraform": "```hcl\n# Cloud SQL for SQL Server instance with no 'user options' flag set\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n\n settings {\n tier = \"db-custom-2-7680\"\n # Remediation: Do NOT set a database_flags block for 'user options'\n # This omission removes/unsets the 'user options' flag so the check passes.\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that, user options database flag for Cloud SQL SQL Server instance should not be configured.", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Leave `user options` unset at the instance level; keep default behavior. Control `SET` options explicitly at session or database scope.\n\n- Enforce **least privilege** for flag management\n- Use **change control** and testing before rollout\n- Monitor for configuration drift as part of **defense in depth**", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_user_options_flag" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections.metadata.json index bc1f402431..543081679e 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_ssl_connections", - "CheckTitle": "Ensure That the Cloud SQL Database Instance Requires All Incoming Connections To Use SSL", + "CheckTitle": "Cloud SQL database instance requires SSL for all incoming connections", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That the Cloud SQL Database Instance Requires All Incoming Connections To Use SSL", - "Risk": "SQL database connections if successfully trapped (MITM), can reveal sensitive data like credentials, database queries, query outputs etc. For security, it is recommended to always use SSL encryption when connecting to your instance. This recommendation is applicable for Postgresql, MySql generation 1, MySql generation 2 and SQL Server 2017 instances.", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "Cloud SQL instances enforce **SSL/TLS-only connections**, rejecting plaintext traffic. The connection policy requires encryption for all clients (e.g., `ENCRYPTED_ONLY` or `TRUSTED_CLIENT_CERTIFICATE_REQUIRED`) instead of allowing both encrypted and unencrypted connections.", + "Risk": "Without enforced TLS, database traffic is exposed to interception.\n- **MITM** can read creds and query results (**confidentiality**)\n- Inject/alter statements to corrupt data (**integrity**)\n\nMixed modes cause accidental plaintext use on public or untrusted networks.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/enable-ssl-for-incoming-connections.html", + "https://cloud.google.com/sql/docs/postgres/configure-ssl-instance/" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch --require-ssl", + "CLI": "gcloud sql instances patch --ssl-mode=ENCRYPTED_ONLY", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/enable-ssl-for-incoming-connections.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Click your instance name\n3. Open Connections > Security tab\n4. Select \"Allow only SSL connections\"\n5. Click Save", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"\"\n\n settings {\n tier = \"\"\n ip_configuration {\n ssl_mode = \"ENCRYPTED_ONLY\" # Critical: only allow SSL/TLS-encrypted connections\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to enforce all incoming connections to SQL database instance to use SSL.", - "Url": "https://cloud.google.com/sql/docs/postgres/configure-ssl-instance/" + "Text": "Require **TLS for all connections**. Prefer `TRUSTED_CLIENT_CERTIFICATE_REQUIRED` or use Cloud SQL Auth Proxy/Connectors for encrypted, authenticated channels.\n- Disallow mixed plaintext/SSL modes\n- Rotate and monitor certificates\n- Combine with **least privilege** and private access", + "Url": "https://hub.prowler.com/check/cloudsql_instance_ssl_connections" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_audit_logs_enabled/cloudstorage_audit_logs_enabled.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_audit_logs_enabled/cloudstorage_audit_logs_enabled.metadata.json index 511b4c7bbe..7776a3ac3b 100644 --- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_audit_logs_enabled/cloudstorage_audit_logs_enabled.metadata.json +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_audit_logs_enabled/cloudstorage_audit_logs_enabled.metadata.json @@ -13,7 +13,7 @@ "Risk": "Without Data Access audit logs, you cannot track who accessed or modified objects in your Cloud Storage buckets, making it difficult to detect unauthorized access, data exfiltration, or compliance violations.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/enable-data-access-audit-logs.html", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudStorage/enable-data-access-audit-logs.html", "https://cloud.google.com/storage/docs/audit-logging" ], "Remediation": { diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled.metadata.json index ffb2ad7110..33af3d20d9 100644 --- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled.metadata.json +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_lifecycle_management_enabled/cloudstorage_bucket_lifecycle_management_enabled.metadata.json @@ -1,30 +1,32 @@ { "Provider": "gcp", "CheckID": "cloudstorage_bucket_lifecycle_management_enabled", - "CheckTitle": "Cloud Storage buckets have lifecycle management enabled", + "CheckTitle": "Cloud Storage bucket has lifecycle management enabled with at least one valid rule", "CheckType": [], "ServiceName": "cloudstorage", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", + "Severity": "low", "ResourceType": "storage.googleapis.com/Bucket", "ResourceGroup": "storage", - "Description": "**Google Cloud Storage buckets** are evaluated for the presence of **lifecycle management** with at least one valid rule (supported action and non-empty condition) to automatically transition or delete objects and optimize storage costs.", - "Risk": "Buckets without lifecycle rules can accumulate stale data, increase storage costs, and fail to meet data retention and internal compliance requirements.", + "Description": "**Cloud Storage buckets** use **Object Lifecycle Management** with at least one valid rule (supported `action` and non-empty `condition`) to automatically transition storage class or delete objects.", + "Risk": "Without lifecycle rules, data and object versions persist indefinitely, expanding the attack surface and hindering mandated erasure. Stale data amplifies exfiltration impact (**confidentiality**) and complicates **integrity** controls, while also driving avoidable cost and retention noncompliance.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/enable-lifecycle-management.html", - "https://cloud.google.com/storage/docs/lifecycle" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudStorage/enable-lifecycle-management.html", + "https://docs.cloud.google.com/storage/docs/managing-lifecycles", + "https://docs.cloud.google.com/storage/docs/lifecycle", + "https://docs.cloud.google.com/storage/docs/samples/storage-enable-bucket-lifecycle-management" ], "Remediation": { "Code": { "CLI": "gcloud storage buckets update gs:// --lifecycle-file=", "NativeIaC": "", - "Other": "1) Open Google Cloud Console → Storage → Buckets → \n2) Tab 'Lifecycle'\n3) Add rule(s) to delete or transition objects (e.g., delete after 365 days; transition STANDARD→NEARLINE after 90 days)\n4) Save", - "Terraform": "```hcl\n# Example: enable lifecycle to transition and delete objects\nresource \"google_storage_bucket\" \"example\" {\n name = var.bucket_name\n location = var.location\n\n # Transition STANDARD → NEARLINE after 90 days\n lifecycle_rule {\n action {\n type = \"SetStorageClass\"\n storage_class = \"NEARLINE\"\n }\n condition {\n age = 90\n matches_storage_class = [\"STANDARD\"]\n }\n }\n\n # Delete objects after 365 days\n lifecycle_rule {\n action {\n type = \"Delete\"\n }\n condition {\n age = 365\n }\n }\n}\n```" + "Other": "1. In Google Cloud Console, go to Storage > Buckets and open \n2. Click the Lifecycle tab\n3. Click Add a rule\n4. Action: Delete\n5. Condition: Age = 1 day\n6. Click Create/Save", + "Terraform": "```hcl\nresource \"google_storage_bucket\" \"\" {\n name = \"\"\n location = \"US\"\n\n # Critical: add at least one lifecycle rule with a condition to pass the check\n lifecycle_rule {\n action { type = \"Delete\" } # Critical: defines a supported action\n condition { age = 1 } # Critical: ensures the rule has a valid condition\n }\n}\n```" }, "Recommendation": { - "Text": "Configure lifecycle rules to automatically delete stale objects or transition them to colder storage classes according to your organization's retention and cost-optimization policy.", + "Text": "Define lifecycle policies by data classification to enforce **least data retention**. Use `Delete` for TTL/age and `SetStorageClass` for archival, with version-aware conditions like `isLive=false` or `numNewerVersions`. Test on a limited dataset, review regularly, and align with **defense in depth**.", "Url": "https://hub.prowler.com/check/cloudstorage_bucket_lifecycle_management_enabled" } }, diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock.metadata.json index 679751f085..941afe8c18 100644 --- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock.metadata.json +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock.metadata.json @@ -1,33 +1,40 @@ { "Provider": "gcp", "CheckID": "cloudstorage_bucket_log_retention_policy_lock", - "CheckTitle": "Cloud Storage log bucket has a Retention Policy with Bucket Lock enabled", + "CheckTitle": "Cloud Storage log sink bucket has a retention policy with Bucket Lock enabled", "CheckType": [], "ServiceName": "cloudstorage", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", + "Severity": "high", "ResourceType": "storage.googleapis.com/Bucket", "ResourceGroup": "storage", - "Description": "**Google Cloud Storage buckets** used as **log sinks** are evaluated to ensure that a **Retention Policy** is configured and **Bucket Lock** is enabled. Enabling Bucket Lock permanently prevents the retention policy from being reduced or removed, protecting logs from modification or deletion.", - "Risk": "Log sink buckets without a locked retention policy are at risk of log tampering or accidental deletion. Without Bucket Lock, an attacker or user could remove or shorten the retention policy, compromising the integrity of audit logs required for forensics and compliance investigations.", + "Description": "**Cloud Storage log sink buckets** have a configured **retention period** with **Bucket Lock** applied, ensuring the retention policy cannot be shortened or removed.", + "Risk": "Without a locked retention policy, exported logs can be deleted early or retention reduced, undermining log **integrity** and **availability**. An attacker or malicious insider could purge evidence to evade detection, hindering **forensics** and weakening **non-repudiation** across the environment.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/retention-policies-with-bucket-lock.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudStorage/retention-policies-with-bucket-lock.html", + "https://docs.cloud.google.com/storage/docs/bucket-lock", + "https://docs.cloud.google.com/storage/docs/using-bucket-lock", + "https://docs.cloud.google.com/storage/docs/samples/storage-lock-retention-policy", + "https://docs.cloud.google.com/logging/docs/export/configure_export_v2" ], "Remediation": { "Code": { "CLI": "gcloud storage buckets lock-retention-policy gs://", "NativeIaC": "", - "Other": "1) Open Google Cloud Console → Storage → Buckets → \n2) Go to the **Configuration** tab\n3) Under **Retention policy**, ensure a retention duration is set\n4) Click **Lock** to enable Bucket Lock and confirm the operation", - "Terraform": "```hcl\nresource \"google_storage_bucket\" \"log_bucket\" {\n name = var.log_bucket_name\n location = var.location\n\n retention_policy {\n retention_period = 31536000 # 365 days in seconds\n is_locked = true\n }\n}\n```" + "Other": "1. In Google Cloud Console, go to Storage > Buckets and open the bucket used by your Logs Router sink\n2. Click the Configuration tab\n3. Under Retention policy, click Edit, set any required retention duration, and click Save\n4. Click Lock retention policy, type LOCK to confirm, and confirm to permanently lock it", + "Terraform": "```hcl\nresource \"google_storage_bucket\" \"\" {\n name = \"\"\n location = \"\"\n\n retention_policy {\n retention_period = 86400 # Required: enable a retention policy (1 day)\n is_locked = true # CRITICAL: locks the retention policy (Bucket Lock) to pass the check\n }\n}\n```" }, "Recommendation": { - "Text": "Configure a retention policy and enable Bucket Lock on all Cloud Storage buckets used as log sinks to ensure log integrity and immutability.", + "Text": "Set a **retention policy** on every log sink bucket and enable **Bucket Lock**. Choose durations that meet investigative and regulatory needs. Enforce **least privilege** and **separation of duties** for bucket and logging administration, and apply **defense in depth** so no single actor can weaken log retention.", "Url": "https://hub.prowler.com/check/cloudstorage_bucket_log_retention_policy_lock" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_logging_enabled/cloudstorage_bucket_logging_enabled.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_logging_enabled/cloudstorage_bucket_logging_enabled.metadata.json index e94769162c..2f31198774 100644 --- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_logging_enabled/cloudstorage_bucket_logging_enabled.metadata.json +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_logging_enabled/cloudstorage_bucket_logging_enabled.metadata.json @@ -13,7 +13,7 @@ "Risk": "Buckets without Usage and Storage Logs enabled lack visibility into access and storage activity, which increases the risk of undetected data exfiltration, misuse, or configuration errors.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/enable-usage-and-storage-logs.html", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudStorage/enable-usage-and-storage-logs.html", "https://cloud.google.com/storage/docs/access-logs" ], "Remediation": { diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_public_access/cloudstorage_bucket_public_access.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_public_access/cloudstorage_bucket_public_access.metadata.json index e78196c6d0..6b7b359591 100644 --- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_public_access/cloudstorage_bucket_public_access.metadata.json +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_public_access/cloudstorage_bucket_public_access.metadata.json @@ -1,27 +1,34 @@ { "Provider": "gcp", "CheckID": "cloudstorage_bucket_public_access", - "CheckTitle": "Ensure That Cloud Storage Bucket Is Not Anonymously or Publicly Accessible", + "CheckTitle": "Cloud Storage bucket is not publicly accessible", "CheckType": [], "ServiceName": "cloudstorage", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Bucket", + "Severity": "critical", + "ResourceType": "storage.googleapis.com/Bucket", "ResourceGroup": "storage", - "Description": "Ensure That Cloud Storage Bucket Is Not Anonymously or Publicly Accessible", - "Risk": "Allowing anonymous or public access grants permissions to anyone to access bucket content. Such access might not be desired if you are storing any sensitive data. Hence, ensure that anonymous or public access to a bucket is not allowed.", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/publicly-accessible-storage-buckets.html", + "Description": "**Cloud Storage buckets** are assessed for **anonymous or public access** by detecting permissions granted to broad principals like `allUsers` or `allAuthenticatedUsers` that make bucket data reachable without authentication.", + "Risk": "**Public buckets** undermine **confidentiality** and **integrity**. Anyone can list or download objects; if write access exists, content can be overwritten or deleted. Abuse enables hotlinking and malware hosting, impacting **availability** and driving unexpected egress costs.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudStorage/publicly-accessible-storage-buckets.html", + "https://docs.cloud.google.com/storage/docs/public-access-prevention", + "https://docs.cloud.google.com/storage/docs/access-control/iam", + "https://docs.cloud.google.com/storage/docs/access-control/iam-reference", + "https://docs.cloud.google.com/storage/docs/using-uniform-bucket-level-access" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud storage buckets update gs:// --public-access-prevention enforced", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-public-policies/bc_gcp_public_1", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-public-policies/bc_gcp_public_1#terraform" + "Other": "1. In Google Cloud Console, go to Storage > Buckets and open \n2. Click the Permissions tab\n3. Set Public access prevention to Enforced\n4. Click Save", + "Terraform": "```hcl\nresource \"google_storage_bucket\" \"\" {\n name = \"\"\n location = \"\"\n\n public_access_prevention = \"enforced\" # Critical: blocks allUsers/allAuthenticatedUsers, making the bucket not publicly accessible\n}\n```" }, "Recommendation": { - "Text": "It is recommended that IAM policy on Cloud Storage bucket does not allows anonymous or public access.", - "Url": "https://cloud.google.com/storage/docs/access-control/iam-reference" + "Text": "Adopt **least privilege**: remove `allUsers`/`allAuthenticatedUsers` and grant only required identities. Enforce **Public Access Prevention** and use uniform bucket-level access. *If external sharing is needed*, issue **signed URLs** or use an authenticated proxy/CDN, and review permissions regularly.", + "Url": "https://hub.prowler.com/check/cloudstorage_bucket_public_access" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_soft_delete_enabled/cloudstorage_bucket_soft_delete_enabled.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_soft_delete_enabled/cloudstorage_bucket_soft_delete_enabled.metadata.json index be5e592164..e02c1ee285 100644 --- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_soft_delete_enabled/cloudstorage_bucket_soft_delete_enabled.metadata.json +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_soft_delete_enabled/cloudstorage_bucket_soft_delete_enabled.metadata.json @@ -1,7 +1,7 @@ { "Provider": "gcp", "CheckID": "cloudstorage_bucket_soft_delete_enabled", - "CheckTitle": "Cloud Storage buckets have Soft Delete enabled", + "CheckTitle": "Cloud Storage bucket has Soft Delete enabled", "CheckType": [], "ServiceName": "cloudstorage", "SubServiceName": "", @@ -9,22 +9,22 @@ "Severity": "medium", "ResourceType": "storage.googleapis.com/Bucket", "ResourceGroup": "storage", - "Description": "**Google Cloud Storage buckets** are evaluated to ensure that **Soft Delete** is enabled. Soft Delete helps protect data from accidental or malicious deletion by retaining deleted objects for a specified duration, allowing recovery within that retention window.", - "Risk": "Buckets without Soft Delete enabled are at higher risk of irreversible data loss caused by accidental or unauthorized deletions, since deleted objects cannot be recovered once removed.", + "Description": "**Google Cloud Storage buckets** are assessed for **Soft Delete** being enabled with a non-zero retention window, meaning deleted objects are temporarily preserved and can be restored until the window expires.", + "Risk": "**No Soft Delete** makes object deletions **immediate and irreversible**, undermining data **availability** and **integrity**. Accidental removal, compromised credentials, wiper malware, or misconfigured lifecycle rules can erase datasets with no recovery path, breaking RPO/RTO and legal retention expectations.", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/storage/docs/soft-delete", - "https://cloud.google.com/blog/products/storage-data-transfer/understanding-cloud-storages-new-soft-delete-feature" + "https://docs.cloud.google.com/storage/docs/soft-delete", + "https://docs.cloud.google.com/storage/docs/use-soft-delete" ], "Remediation": { "Code": { - "CLI": "gcloud storage buckets update gs:// --soft-delete-retention-duration=", + "CLI": "gcloud storage buckets update gs:// --soft-delete-duration=", "NativeIaC": "", - "Other": "1) Open Google Cloud Console → Storage → Buckets → \n2) Tab 'Configuration'\n3) Under 'Soft Delete', click 'Enable Soft Delete'\n4) Set the desired retention duration and save changes", - "Terraform": "```hcl\n# Example: enable Soft Delete on a Cloud Storage bucket\nresource \"google_storage_bucket\" \"example\" {\n name = var.bucket_name\n location = var.location\n\n soft_delete_policy {\n retention_duration_seconds = 604800 # 7 days\n }\n}\n```" + "Other": "1. In Google Cloud Console, go to Storage > Buckets and open \n2. Click the Configuration tab\n3. In the Soft Delete section, click Enable Soft Delete\n4. Set a retention duration > 0 and click Save", + "Terraform": "```hcl\nresource \"google_storage_bucket\" \"\" {\n name = \"\"\n location = \"\"\n\n soft_delete_policy {\n retention_duration_seconds = 604800 # Critical: >0 enables Soft Delete (7 days)\n }\n}\n```" }, "Recommendation": { - "Text": "Enable Soft Delete on Cloud Storage buckets to retain deleted objects for a defined period, improving data recoverability and resilience against accidental or malicious deletions.", + "Text": "Enable **Soft Delete** with a retention window aligned to your RPO/RTO. Apply **least privilege** for delete/undelete actions and use **defense in depth** with object versioning and retention policies. Monitor deletion events and regularly test restore procedures to ensure recoverability.", "Url": "https://hub.prowler.com/check/cloudstorage_bucket_soft_delete_enabled" } }, diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period.metadata.json index 9f7718d45e..ef069ddc10 100644 --- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period.metadata.json +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period.metadata.json @@ -13,7 +13,7 @@ "Risk": "Insufficient or missing retention allows premature deletion or modification of objects, weakening data recovery and compliance with retention requirements.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/sufficient-retention-period.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudStorage/sufficient-retention-period.html" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_uniform_bucket_level_access/cloudstorage_bucket_uniform_bucket_level_access.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_uniform_bucket_level_access/cloudstorage_bucket_uniform_bucket_level_access.metadata.json index b53e41e07f..eabf13f14a 100644 --- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_uniform_bucket_level_access/cloudstorage_bucket_uniform_bucket_level_access.metadata.json +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_uniform_bucket_level_access/cloudstorage_bucket_uniform_bucket_level_access.metadata.json @@ -1,30 +1,38 @@ { "Provider": "gcp", "CheckID": "cloudstorage_bucket_uniform_bucket_level_access", - "CheckTitle": "Ensure That Cloud Storage Buckets Have Uniform Bucket-Level Access Enabled", + "CheckTitle": "Cloud Storage bucket has uniform bucket-level access enabled", "CheckType": [], "ServiceName": "cloudstorage", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Bucket", + "ResourceType": "storage.googleapis.com/Bucket", "ResourceGroup": "storage", - "Description": "Ensure That Cloud Storage Buckets Have Uniform Bucket-Level Access Enabled", - "Risk": "Enabling uniform bucket-level access guarantees that if a Storage bucket is not publicly accessible, no object in the bucket is publicly accessible either.", + "Description": "Cloud Storage buckets have **uniform bucket-level access (UBLA)** enabled so object permissions are controlled solely by **bucket-level IAM**, with object ACLs disabled.", + "Risk": "Without **UBLA**, object ACLs can bypass bucket IAM, enabling unintended public reads or unauthorized writes. This threatens **confidentiality** through data exposure, undermines **integrity** via object tampering, and reduces **auditability** with fragmented, hard-to-review permissions.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudStorage/enable-uniform-bucket-level-access.html", + "https://docs.cloud.google.com/storage/docs/using-uniform-bucket-level-access", + "https://docs.cloud.google.com/storage/docs/public-access-prevention", + "https://docs.cloud.google.com/storage/docs/access-control/iam" + ], "Remediation": { "Code": { - "CLI": "gsutil uniformbucketlevelaccess set on gs://BUCKET_NAME/", + "CLI": "gcloud storage buckets update gs:// --uniform-bucket-level-access", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/enable-uniform-bucket-level-access.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-storage-gcs-policies/bc_gcp_gcs_2#terraform" + "Other": "1. In Google Cloud Console, go to Storage > Buckets\n2. Click the bucket name ()\n3. Open the Permissions tab (or Configuration if shown)\n4. In Access control, select Uniform and click Save", + "Terraform": "```hcl\nresource \"google_storage_bucket\" \"\" {\n name = \"\"\n location = \"\"\n\n uniform_bucket_level_access = true # Critical: enables UBLA so the bucket passes the check\n}\n```" }, "Recommendation": { - "Text": "It is recommended that uniform bucket-level access is enabled on Cloud Storage buckets.", - "Url": "https://cloud.google.com/storage/docs/using-uniform-bucket-level-access" + "Text": "Enable **UBLA** on all buckets to centralize authorization and apply **least privilege** with IAM. Eliminate reliance on object ACLs; use **Public Access Prevention** and **organization policies** to enforce non-public defaults. Monitor access with logs and periodic reviews as part of **defense in depth**.", + "Url": "https://hub.prowler.com/check/cloudstorage_bucket_uniform_bucket_level_access" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_versioning_enabled/cloudstorage_bucket_versioning_enabled.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_versioning_enabled/cloudstorage_bucket_versioning_enabled.metadata.json index 44fed4fd1a..0be45c7248 100644 --- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_versioning_enabled/cloudstorage_bucket_versioning_enabled.metadata.json +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_versioning_enabled/cloudstorage_bucket_versioning_enabled.metadata.json @@ -1,7 +1,7 @@ { "Provider": "gcp", "CheckID": "cloudstorage_bucket_versioning_enabled", - "CheckTitle": "Cloud Storage buckets have Object Versioning enabled", + "CheckTitle": "Cloud Storage bucket has Object Versioning enabled", "CheckType": [], "ServiceName": "cloudstorage", "SubServiceName": "", @@ -9,22 +9,25 @@ "Severity": "medium", "ResourceType": "storage.googleapis.com/Bucket", "ResourceGroup": "storage", - "Description": "**Google Cloud Storage buckets** are evaluated to ensure that **Object Versioning** is enabled. Object Versioning preserves older versions of objects, allowing data recovery, maintaining audit trails, and protecting against accidental deletions or overwrites.", - "Risk": "Buckets without Object Versioning enabled cannot recover previous object versions, which increases the risk of permanent data loss from accidental deletion or modification.", + "Description": "**Cloud Storage buckets** with **Object Versioning** keep prior object generations. The finding indicates whether the bucket's `versioning` setting is enabled.", + "Risk": "Without **Object Versioning**, deleted or overwritten objects can't be restored, reducing **availability** and **integrity**. Compromised credentials or faulty processes can irreversibly delete or corrupt data, enabling ransomware-style destruction, accidental loss, and weakening forensic reconstruction.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/enable-versioning.html", - "https://cloud.google.com/storage/docs/object-versioning" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudStorage/enable-versioning.html", + "https://docs.cloud.google.com/storage/docs/object-versioning", + "https://docs.cloud.google.com/storage/docs/using-object-versioning", + "https://docs.cloud.google.com/storage/docs/deleting-objects#restoring_noncurrent_versions", + "https://docs.cloud.google.com/storage/docs/lifecycle#delete" ], "Remediation": { "Code": { "CLI": "gcloud storage buckets update gs:// --versioning", "NativeIaC": "", - "Other": "1) Open Google Cloud Console → Storage → Buckets → \n2) Tab 'Configuration'\n3) Under 'Object versioning', click 'Enable Object Versioning'\n4) Save changes", - "Terraform": "```hcl\n# Example: enable Object Versioning on a Cloud Storage bucket\nresource \"google_storage_bucket\" \"example\" {\n name = var.bucket_name\n location = var.location\n\n versioning {\n enabled = true\n }\n}\n```" + "Other": "1. In Google Cloud Console, go to Storage > Buckets and open \n2. Click the Configuration tab, then click Edit\n3. Set Object versioning to Enabled\n4. Click Save", + "Terraform": "```hcl\nresource \"google_storage_bucket\" \"\" {\n name = \"\"\n location = \"\"\n\n versioning { # Critical: enables Object Versioning\n enabled = true # This makes the check pass\n }\n}\n```" }, "Recommendation": { - "Text": "Enable Object Versioning on Cloud Storage buckets to preserve previous object versions and improve data recoverability and auditability.", + "Text": "Enable **Object Versioning** on buckets holding important data. Pair with `lifecycle` rules to expire noncurrent versions and control cost. Enforce **least privilege** for delete/overwrite actions, and add bucket `retention` policies or object holds for defense-in-depth and auditability.", "Url": "https://hub.prowler.com/check/cloudstorage_bucket_versioning_enabled" } }, diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_uses_vpc_service_controls/cloudstorage_uses_vpc_service_controls.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_uses_vpc_service_controls/cloudstorage_uses_vpc_service_controls.metadata.json index 6ed6584097..ce5cf015fa 100644 --- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_uses_vpc_service_controls/cloudstorage_uses_vpc_service_controls.metadata.json +++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_uses_vpc_service_controls/cloudstorage_uses_vpc_service_controls.metadata.json @@ -13,7 +13,7 @@ "Risk": "Projects without VPC Service Controls protection for Cloud Storage may be vulnerable to unauthorized data access and exfiltration, even with proper IAM policies in place. VPC Service Controls provide an additional layer of network-level security that restricts API access based on the context of the request.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/use-vpc-service-controls.html", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudStorage/use-vpc-service-controls.html", "https://cloud.google.com/vpc-service-controls/docs/create-service-perimeters" ], "Remediation": { diff --git a/prowler/providers/gcp/services/compute/compute_firewall_rdp_access_from_the_internet_allowed/compute_firewall_rdp_access_from_the_internet_allowed.metadata.json b/prowler/providers/gcp/services/compute/compute_firewall_rdp_access_from_the_internet_allowed/compute_firewall_rdp_access_from_the_internet_allowed.metadata.json index 3ca83f22e6..4aefa91e90 100644 --- a/prowler/providers/gcp/services/compute/compute_firewall_rdp_access_from_the_internet_allowed/compute_firewall_rdp_access_from_the_internet_allowed.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_firewall_rdp_access_from_the_internet_allowed/compute_firewall_rdp_access_from_the_internet_allowed.metadata.json @@ -1,27 +1,30 @@ { "Provider": "gcp", "CheckID": "compute_firewall_rdp_access_from_the_internet_allowed", - "CheckTitle": "Ensure That RDP Access Is Restricted From the Internet", + "CheckTitle": "Firewall rule does not allow ingress from 0.0.0.0/0 to TCP port 3389 (RDP)", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "FirewallRule", - "ResourceGroup": "network", - "Description": "GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided.", - "Risk": "Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks.", + "ResourceType": "compute.googleapis.com/Firewall", + "Description": "**VPC firewall rules** permitting inbound **RDP** (`TCP 3389`) from `0.0.0.0/0` are flagged, including ingress rules that allow all TCP ports or `all` protocols", + "Risk": "Exposed **RDP** enables Internet-wide scanning and **brute force**. Exploits can yield **remote code execution**, followed by **lateral movement** and data theft.\n\nThis endangers **confidentiality**, **integrity**, and **availability** (e.g., ransomware, service disruption).", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/vpc/docs/using-firewalls", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute firewall-rules delete default-allow-rdp", + "CLI": "gcloud compute firewall-rules delete ", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform" + "Other": "1. In Google Cloud Console, go to Networking > VPC network > Firewall.\n2. Find the ingress rule that allows TCP port 3389 with Source IPv4 ranges set to 0.0.0.0/0.\n3. Select the rule and click Delete, then confirm.", + "Terraform": "```hcl\nresource \"google_compute_firewall\" \"\" {\n name = \"\"\n network = \"\"\n\n allow {\n protocol = \"tcp\"\n ports = [\"3389\"]\n }\n\n source_ranges = [\"10.0.0.0/8\"] # CRITICAL: removes 0.0.0.0/0 so RDP is not exposed to the Internet\n}\n```" }, "Recommendation": { - "Text": "Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server.", - "Url": "https://cloud.google.com/vpc/docs/using-firewalls" + "Text": "Restrict **RDP** to trusted IP ranges or a hardened **bastion/IAP** proxy; prefer private access with no public IPs. Apply **least privilege** and network segmentation, use just-in-time access and strong authentication, and monitor logs. Aim for **defense in depth** to minimize exposure.", + "Url": "https://hub.prowler.com/check/compute_firewall_rdp_access_from_the_internet_allowed" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/compute/compute_firewall_ssh_access_from_the_internet_allowed/compute_firewall_ssh_access_from_the_internet_allowed.metadata.json b/prowler/providers/gcp/services/compute/compute_firewall_ssh_access_from_the_internet_allowed/compute_firewall_ssh_access_from_the_internet_allowed.metadata.json index 52fbcf652e..115c6df402 100644 --- a/prowler/providers/gcp/services/compute/compute_firewall_ssh_access_from_the_internet_allowed/compute_firewall_ssh_access_from_the_internet_allowed.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_firewall_ssh_access_from_the_internet_allowed/compute_firewall_ssh_access_from_the_internet_allowed.metadata.json @@ -1,27 +1,30 @@ { "Provider": "gcp", "CheckID": "compute_firewall_ssh_access_from_the_internet_allowed", - "CheckTitle": "Ensure That SSH Access Is Restricted From the Internet", + "CheckTitle": "Firewall does not expose TCP port 22 (SSH) to the Internet", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "FirewallRule", - "ResourceGroup": "network", - "Description": "GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow the user to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, only an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the internet to VPC or VM instance using `SSH` on `Port 22` can be avoided.", - "Risk": "Exposing Secure Shell (SSH) port 22 to the Internet can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and brute-force attacks.", + "ResourceType": "compute.googleapis.com/Firewall", + "Description": "**VPC firewall rules** allowing Internet-sourced **ingress** (`0.0.0.0/0`) to `TCP port 22 (SSH)` are identified, including rules using protocol `all` or `tcp` whose ports or ranges include `22`.", + "Risk": "Exposed **SSH (22)** enables Internet-wide scanning, **brute force** and **credential stuffing**. Compromise can yield shell access for **data exfiltration**, command execution, and **lateral movement**, undermining **confidentiality** and **integrity**, and risking **availability** through abuse or lockouts.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/vpc/docs/using-firewalls", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/unrestricted-ssh-access.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute firewall-rules delete default-allow-ssh", + "CLI": "gcloud compute firewall-rules update --source-ranges=", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-ssh-access.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_1#terraform" + "Other": "1. In Google Cloud Console, go to Networking > VPC network > Firewall\n2. Locate the INGRESS rule that allows tcp:22 with Source IPv4 ranges set to 0.0.0.0/0 and open it\n3. Click Edit\n4. Replace Source IPv4 ranges from 0.0.0.0/0 to your trusted CIDR (e.g., )\n5. Click Save", + "Terraform": "```hcl\nresource \"google_compute_firewall\" \"\" {\n name = \"\"\n network = \"\"\n\n source_ranges = [\"\"] # Critical: removes 0.0.0.0/0 to stop exposing SSH to the Internet\n\n allow {\n protocol = \"tcp\" # Critical: limit to SSH only\n ports = [\"22\"]\n }\n}\n```" }, "Recommendation": { - "Text": "Check your Google Cloud Virtual Private Cloud (VPC) firewall rules for inbound rules that allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 22 and restrict the access to trusted IP addresses or IP ranges only in order to implement the principle of least privilege and reduce the attack surface. TCP port 22 is used for secure remote login by connecting an SSH client application with an SSH server. It is strongly recommended to configure your Google Cloud VPC firewall rules to limit inbound traffic on TCP port 22 to known IP addresses only.", - "Url": "https://cloud.google.com/vpc/docs/using-firewalls" + "Text": "Restrict **SSH** to trusted sources; avoid `0.0.0.0/0`. Prefer **bastion hosts** or **IAP TCP forwarding**, or use **VPN/peering**. Enforce **least privilege** and **defense in depth**: limit to required CIDRs, use **key-based auth**, disable `PasswordAuthentication`, and monitor/alert on access attempts.", + "Url": "https://hub.prowler.com/check/compute_firewall_ssh_access_from_the_internet_allowed" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.metadata.json b/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.metadata.json index 513938f322..a24d47ec73 100644 --- a/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.metadata.json @@ -13,8 +13,7 @@ "Risk": "Publicly shared disk images can expose **sensitive data** and application configurations to unauthorized users.\n\n- Any authenticated GCP user can access the image content\n- Could lead to **data breaches** if images contain secrets or proprietary code\n- Attackers may use exposed images to understand application architecture", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/compute/docs/images/managing-access-custom-images", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/publicly-shared-disk-images.html" + "https://cloud.google.com/compute/docs/images/managing-access-custom-images" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_automatic_restart_enabled/compute_instance_automatic_restart_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_automatic_restart_enabled/compute_instance_automatic_restart_enabled.metadata.json index 64c1ffe547..d3d075f85a 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_automatic_restart_enabled/compute_instance_automatic_restart_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_automatic_restart_enabled/compute_instance_automatic_restart_enabled.metadata.json @@ -13,7 +13,6 @@ "Risk": "VM instances without Automatic Restart enabled will not recover automatically from host maintenance events or unexpected failures, potentially leading to prolonged service downtime and requiring manual intervention to restore services.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-automatic-restart.html", "https://cloud.google.com/compute/docs/instances/setting-instance-scheduling-options" ], "Remediation": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled.metadata.json index c9e4259157..1e9ebee74a 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "compute_instance_block_project_wide_ssh_keys_disabled", - "CheckTitle": "Ensure “Block Project-Wide SSH Keys” Is Enabled for VM Instances", + "CheckTitle": "VM instance has Block project-wide SSH keys enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "It is recommended to use Instance specific SSH key(s) instead of using common/shared project-wide SSH key(s) to access Instances.", - "Risk": "Project-wide SSH keys are stored in Compute/Project-meta-data. Project wide SSH keys can be used to login into all the instances within project. Using project-wide SSH keys eases the SSH key management but if compromised, poses the security risk which can impact all the instances within project.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Compute Engine VMs** are evaluated for the metadata key `block-project-ssh-keys` set to `true`, indicating **project-wide SSH keys** are blocked and only instance-level or OS Login credentials are honored.", + "Risk": "Allowing **project-wide SSH keys** lets a single compromised key reach many VMs, amplifying blast radius. This endangers **confidentiality** (data exposure) and **integrity** (unauthorized changes) and enables **lateral movement**. Per-instance revocation and accountability are weakened.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/compute/docs/instances/adding-removing-ssh-keys", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/enable-block-project-wide-ssh-keys.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute instances add-metadata --metadata block-projectssh-keys=TRUE", + "CLI": "gcloud compute instances add-metadata --zone --metadata=block-project-ssh-keys=true", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-block-project-wide-ssh-keys.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_8#terraform" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Click the target VM and then click Edit\n3. Under Custom metadata, click Add item\n4. Key: block-project-ssh-keys, Value: true\n5. Click Save", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"vm\" {\n name = \"\"\n zone = \"\"\n machine_type = \"e2-micro\"\n\n boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-12\"\n }\n }\n\n network_interface {\n network = \"default\"\n }\n\n metadata = {\n block-project-ssh-keys = \"true\" # Critical: blocks project-wide SSH keys for this VM\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to use Instance specific SSH keys which can limit the attack surface if the SSH keys are compromised.", - "Url": "https://cloud.google.com/compute/docs/instances/adding-removing-ssh-keys" + "Text": "Set `block-project-ssh-keys=true` to prevent shared key inheritance. Prefer **OS Login** or instance-specific keys, enforce **least privilege** and **separation of duties** for metadata changes, use **short-lived credentials** with rotation, limit direct SSH, and monitor access for anomalies.", + "Url": "https://hub.prowler.com/check/compute_instance_block_project_wide_ssh_keys_disabled" } }, - "Categories": [], + "Categories": [ + "identity-access", + "secrets" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_confidential_computing_enabled/compute_instance_confidential_computing_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_confidential_computing_enabled/compute_instance_confidential_computing_enabled.metadata.json index fb999ed9d6..c8f7f080a5 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_confidential_computing_enabled/compute_instance_confidential_computing_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_confidential_computing_enabled/compute_instance_confidential_computing_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_instance_confidential_computing_enabled", - "CheckTitle": "Ensure Compute Instances Have Confidential Computing Enabled", + "CheckTitle": "Compute instance has Confidential Computing enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "Ensure that the Confidential Computing security feature is enabled for your Google Cloud virtual machine (VM) instances in order to add protection to your sensitive data in use by keeping it encrypted in memory and using encryption keys that Google doesn't have access to. Confidential Computing is a breakthrough technology which encrypts data while it is being processed. This technology keeps data encrypted in memory, outside the CPU.", - "Risk": "Confidential Computing keeps your sensitive data encrypted while it is used, indexed, queried, or trained on, and does not allow Google to access the encryption keys (these keys are generated in hardware, per VM instance, and can't be exported). In this way, the Confidential Computing feature can help alleviate concerns about risk related to either dependency on Google Cloud infrastructure or Google insiders' access to your data in the clear.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Google Compute Engine VMs** configured as **Confidential VMs** encrypt data in use with hardware-based memory protection and per-instance keys.\n\nThis assessment identifies whether **Confidential Computing** is enabled on each VM instance.", + "Risk": "Absent **Confidential Computing**, plaintext data in RAM can be exposed via host introspection, hypervisor compromise, or cold-boot/DMA attacks, undermining **confidentiality** and enabling **in-memory tampering** that impacts **integrity** of computations, models, and secrets.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/confidential-computing.html", + "https://cloud.google.com/compute/confidential-vm/docs/creating-cvm-instance:https://cloud.google.com/compute/confidential-vm/docs/about-cvm:https://cloud.google.com/confidential-computing:https://cloud.google.com/blog/products/identity-security/introducing-google-cloud-confidential-computing-with-confidential-vms" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/confidential-computing.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Select and click Stop\n3. Click Edit\n4. Under Confidential VM service, check Enable Confidential VM service\n5. If the option is unavailable, change Machine series to a supported one (e.g., N2D) and select a type\n6. Click Save, then click Start to power on the instance", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"n2d-standard-2\" # Supported for Confidential VM\n zone = \"\"\n\n boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-12\"\n }\n }\n\n network_interface {}\n\n # Critical: Enables Confidential Computing on the VM\n confidential_instance_config {\n enable_confidential_compute = true # Turns on Confidential VM\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that the Confidential Computing security feature is enabled for your Google Cloud virtual machine (VM) instances in order to add protection to your sensitive data in use by keeping it encrypted in memory and using encryption keys that Google doesn't have access to. Confidential Computing is a breakthrough technology which encrypts data while it is being processed. This technology keeps data encrypted in memory, outside the CPU.", - "Url": "https://cloud.google.com/compute/confidential-vm/docs/creating-cvm-instance:https://cloud.google.com/compute/confidential-vm/docs/about-cvm:https://cloud.google.com/confidential-computing:https://cloud.google.com/blog/products/identity-security/introducing-google-cloud-confidential-computing-with-confidential-vms" + "Text": "Enable **Confidential VMs** for workloads processing sensitive data to protect data-in-use. Apply **defense in depth**: enforce **least privilege** on administrative access, use disk encryption with `CMEK`, and require workload attestation/trusted images. *If unsupported*, isolate or refactor workloads to compatible options.", + "Url": "https://hub.prowler.com/check/compute_instance_confidential_computing_enabled" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use/compute_instance_default_service_account_in_use.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use/compute_instance_default_service_account_in_use.metadata.json index 5d803c5964..9f6b0a4c77 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use/compute_instance_default_service_account_in_use.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use/compute_instance_default_service_account_in_use.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_instance_default_service_account_in_use", - "CheckTitle": "Ensure That Instances Are Not Configured To Use the Default Service Account", + "CheckTitle": "Compute Engine instance does not use the default service account", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "It is recommended to configure your instance to not use the default Compute Engine service account because it has the Editor role on the project.", - "Risk": "The default Compute Engine service account has the Editor role on the project, which allows read and write access to most Google Cloud Services. This can lead to a privilege escalations if your VM is compromised allowing an attacker gaining access to all of your project", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/default-service-accounts-in-use.html", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Compute Engine VMs** are evaluated for use of the **default service account** (`[PROJECT_NUMBER]-compute@developer.gserviceaccount.com`). The finding highlights instances configured with that account rather than a workload-specific service account. *GKE node VMs are ignored.*", + "Risk": "Using the default service account often grants project-wide rights (e.g., `roles/editor`). If a VM is compromised, metadata tokens can be abused to read/modify resources, exfiltrate data, and pivot across services, impacting **confidentiality** and **integrity**, and potentially **availability**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/iam/docs/granting-changing-revoking-access", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/default-service-accounts-in-use.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute instances set-service-account --service-account=", + "CLI": "", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_1", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_1#terraform" + "Other": "1. In Google Cloud console, go to Compute Engine > VM instances\n2. Click the VM, then click Stop and wait until it is stopped\n3. Click Edit\n4. Under Service account, select a non-default service account (not ending with \"-compute@developer.gserviceaccount.com\")\n5. Click Save, then click Start to power the VM back on\n6. If no suitable service account exists: IAM & Admin > Service Accounts > Create service account, grant only required roles, then repeat steps 2-5", + "Terraform": "```hcl\n# Create a non-default service account\nresource \"google_service_account\" \"\" {\n account_id = \"\" # CRITICAL: custom SA to avoid default \"-compute@developer.gserviceaccount.com\"\n}\n\n# Attach the non-default service account to the VM\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-micro\"\n zone = \"\"\n\n boot_disk { initialize_params { image = \"debian-cloud/debian-12\" } }\n network_interface { network = \"default\" }\n\n service_account {\n email = google_service_account..email # CRITICAL: use non-default SA so the check passes\n }\n}\n```" }, "Recommendation": { - "Text": "To defend against privilege escalations if your VM is compromised and prevent an attacker from gaining access to all of your project, it is recommended to not use the default Compute Engine service account. Instead, you should create a new service account and assigning only the permissions needed by your instance. The default Compute Engine service account is named `[PROJECT_NUMBER]-compute@developer.gserviceaccount.com`.", - "Url": "https://cloud.google.com/iam/docs/granting-changing-revoking-access" + "Text": "Avoid the default service account. Create per-workload service accounts and grant only required roles under **least privilege** and **separation of duties**. Remove broad roles like `roles/editor`. Prefer short-lived credentials and monitor service account usage to enforce **defense in depth**.", + "Url": "https://hub.prowler.com/check/compute_instance_default_service_account_in_use" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use_with_full_api_access/compute_instance_default_service_account_in_use_with_full_api_access.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use_with_full_api_access/compute_instance_default_service_account_in_use_with_full_api_access.metadata.json index f115c276cd..6d14d73c6f 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use_with_full_api_access/compute_instance_default_service_account_in_use_with_full_api_access.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use_with_full_api_access/compute_instance_default_service_account_in_use_with_full_api_access.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_instance_default_service_account_in_use_with_full_api_access", - "CheckTitle": "Ensure That Instances Are Not Configured To Use the Default Service Account With Full Access to All Cloud APIs", + "CheckTitle": "Compute Engine instance does not use the default service account with full access to all Cloud APIs", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "To support principle of least privileges and prevent potential privilege escalation it is recommended that instances are not assigned to default service account `Compute Engine default service account` with Scope `Allow full access to all Cloud APIs`.", - "Risk": "When an instance is configured with `Compute Engine default service account` with Scope `Allow full access to all Cloud APIs`, based on IAM roles assigned to the user(s) accessing Instance, it may allow user to perform cloud operations/API calls that user is not supposed to perform leading to successful privilege escalation.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Compute Engine VM instances** using the **default service account** with the `cloud-platform` scope (`Allow full access to all Cloud APIs`) are identified. *GKE nodes are excluded.*", + "Risk": "With full API scope, any code on the VM can obtain tokens and, combined with the service account's roles, call broad Google Cloud APIs. This enables **privilege escalation**, **data exfiltration**, unauthorized config changes, and service disruption, impacting **confidentiality, integrity, and availability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/iam/docs/granting-changing-revoking-access", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/default-service-accounts-with-full-access-in-use.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute instances set-service-account --service-account= --scopes [,,...]", + "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/default-service-accounts-with-full-access-in-use.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_2#terraform" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Click the affected VM\n3. Click Stop and confirm\n4. Click Edit\n5. Under Service account, select a non-default service account (not -compute@developer.gserviceaccount.com) OR change Cloud API access scopes to not use \"Allow full access to all Cloud APIs\" (use Default access or select specific APIs)\n6. Click Save\n7. Click Start to restart the VM", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-micro\"\n zone = \"us-central1-a\"\n\n boot_disk { initialize_params { image = \"debian-cloud/debian-12\" } }\n network_interface { network = \"default\" }\n\n service_account {\n email = \"\" # FIX: use a non-default service account to avoid the default SA\n scopes = [\"https://www.googleapis.com/auth/devstorage.read_only\"] # FIX: avoid cloud-platform (full API access)\n }\n}\n```" }, "Recommendation": { - "Text": "To enforce the principle of least privileges and prevent potential privilege escalation, ensure that your Google Compute Engine instances are not configured to use the default service account with the Cloud API access scope set to \"Allow full access to all Cloud APIs\". The principle of least privilege (POLP), also known as the principle of least authority, is the security concept of giving the user/system/service the minimal set of permissions required to successfully perform its tasks.", - "Url": "https://cloud.google.com/iam/docs/granting-changing-revoking-access" + "Text": "Use a **custom, least-privileged service account** per VM and avoid the default account. Restrict Cloud API scopes-prefer minimal or per-API scopes, not `Allow full access to all Cloud APIs`. Enforce **least privilege** and **separation of duties**, and regularly review roles to remove excessive permissions.", + "Url": "https://hub.prowler.com/check/compute_instance_default_service_account_in_use_with_full_api_access" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_deletion_protection_enabled/compute_instance_deletion_protection_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_deletion_protection_enabled/compute_instance_deletion_protection_enabled.metadata.json index 7a862d1383..440986a5a8 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_deletion_protection_enabled/compute_instance_deletion_protection_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_deletion_protection_enabled/compute_instance_deletion_protection_enabled.metadata.json @@ -13,8 +13,7 @@ "Risk": "Without deletion protection enabled, VM instances are vulnerable to **accidental deletion** by users with sufficient permissions.\n\nThis could result in:\n- **Service disruption** and downtime for critical applications\n- **Data loss** if persistent disks are also deleted\n- **Recovery delays** while recreating instances and restoring configurations", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/compute/docs/instances/preventing-accidental-vm-deletion", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-deletion-protection.html" + "https://cloud.google.com/compute/docs/instances/preventing-accidental-vm-deletion" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_disk_auto_delete_disabled/compute_instance_disk_auto_delete_disabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_disk_auto_delete_disabled/compute_instance_disk_auto_delete_disabled.metadata.json index 62e9eba637..be993c23e0 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_disk_auto_delete_disabled/compute_instance_disk_auto_delete_disabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_disk_auto_delete_disabled/compute_instance_disk_auto_delete_disabled.metadata.json @@ -12,8 +12,7 @@ "Risk": "With auto-delete enabled, persistent disks are automatically deleted when the associated VM instance is terminated.\n\nThis could result in:\n- **Permanent data loss** if the instance is accidentally or intentionally deleted\n- **Recovery challenges** for mission-critical workloads\n- **Compliance violations** where data retention is required", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/compute/docs/disks/add-persistent-disk", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/disable-auto-delete.html" + "https://cloud.google.com/compute/docs/disks/add-persistent-disk" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_encryption_with_csek_enabled/compute_instance_encryption_with_csek_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_encryption_with_csek_enabled/compute_instance_encryption_with_csek_enabled.metadata.json index 29334d675c..14fae6dac7 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_encryption_with_csek_enabled/compute_instance_encryption_with_csek_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_encryption_with_csek_enabled/compute_instance_encryption_with_csek_enabled.metadata.json @@ -1,27 +1,30 @@ { "Provider": "gcp", "CheckID": "compute_instance_encryption_with_csek_enabled", - "CheckTitle": "Ensure VM Disks for Critical VMs Are Encrypted With Customer-Supplied Encryption Keys (CSEK)", + "CheckTitle": "VM instance has all disks encrypted with Customer-Supplied Encryption Keys (CSEK)", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Disks", - "ResourceGroup": "storage", - "Description": "Customer-Supplied Encryption Keys (CSEK) are a feature in Google Cloud Storage and Google Compute Engine. If you supply your own encryption keys, Google uses your key to protect the Google-generated keys used to encrypt and decrypt your data. By default, Google Compute Engine encrypts all data at rest. Compute Engine handles and manages this encryption for you without any additional actions on your part. However, if you wanted to control and manage this encryption yourself, you can provide your own encryption keys.", - "Risk": "By default, Compute Engine service encrypts all data at rest. The cloud service manages this type of encryption without any additional actions from you and your application. However, if you want to fully control and manage instance disk encryption, you can provide your own encryption keys.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "Compute Engine VM disks use **Customer-Supplied Encryption Keys** (`CSEK`) rather than provider-managed keys. The finding flags instances where any attached disk is not protected with the customer-provided key.", + "Risk": "Without **CSEK**, encryption depends on provider-managed keys, reducing control over key lifecycle and access. This weakens confidentiality, impedes separation of duties, and can delay key revocation, increasing exposure to unauthorized data access and regulatory gaps.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/storage/docs/encryption/using-customer-supplied-keys", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/enable-encryption-with-csek.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute disks create --size= --type= --zone= --source-snapshot= --csek-key-file=", + "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-encryption-with-csek.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/bc_gcp_general_x#terraform" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Click Create instance (you must recreate VMs to use CSEK on boot disks)\n3. In Boot disk, click Change\n4. Expand Encryption and select Customer-supplied key\n5. Paste your base64-encoded 256-bit key and click Select\n6. If adding additional disks: in Additional disks, add a disk and set Encryption to Customer-supplied key with the same key\n7. Click Create to launch the VM with all disks encrypted using CSEK\n8. Migrate workload from the old VM and delete it when done", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-medium\"\n zone = \"\"\n\n boot_disk {\n initialize_params { image = \"debian-cloud/debian-12\" }\n # Critical: enables Customer-Supplied Encryption Key (CSEK) for the boot disk\n disk_encryption_key { raw_key = \"\" } # base64-encoded AES-256 key\n }\n\n network_interface { network = \"default\" }\n}\n```" }, "Recommendation": { - "Text": "Ensure that the disks attached to your production Google Compute Engine instances are encrypted with Customer-Supplied Encryption Keys (CSEKs) in order to have complete control over the data-at-rest encryption and decryption process, and meet strict compliance requirements. These custom keys, also known as Customer-Supplied Encryption Keys (CSEKs), are used by Google Compute Engine to protect the Google-generated keys used to encrypt and decrypt your instance data. Compute Engine service does not store your CSEKs on its servers and cannot access your protected data unless you provide the required key.", - "Url": "https://cloud.google.com/storage/docs/encryption/using-customer-supplied-keys" + "Text": "Use **CSEK** for VM disks that require full control over data-at-rest keys. Apply **least privilege** to key custodians, store keys in hardened vaults/HSMs, enforce rotation and rapid revocation, and document recovery procedures. Combine with **defense in depth** (network and IAM controls) to limit blast radius.", + "Url": "https://hub.prowler.com/check/compute_instance_encryption_with_csek_enabled" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/compute/compute_instance_group_autohealing_enabled/compute_instance_group_autohealing_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_group_autohealing_enabled/compute_instance_group_autohealing_enabled.metadata.json index 4805252f1c..f364573e2e 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_group_autohealing_enabled/compute_instance_group_autohealing_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_group_autohealing_enabled/compute_instance_group_autohealing_enabled.metadata.json @@ -13,8 +13,7 @@ "Risk": "Without autohealing, MIGs cannot detect application-level failures such as crashes, freezes, or memory issues. Instances that are technically running but experiencing problems will remain undetected and unreplaced, leading to:\n\n- **Service degradation** from unhealthy instances\n- **Extended downtime** during application failures\n- **Manual intervention** required to detect and replace failed instances", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/compute/docs/instance-groups/autohealing-instances-in-migs", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-instance-group-autohealing.html" + "https://cloud.google.com/compute/docs/instance-groups/autohealing-instances-in-migs" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_group_load_balancer_attached/compute_instance_group_load_balancer_attached.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_group_load_balancer_attached/compute_instance_group_load_balancer_attached.metadata.json index 46068fb81b..b1f651538a 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_group_load_balancer_attached/compute_instance_group_load_balancer_attached.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_group_load_balancer_attached/compute_instance_group_load_balancer_attached.metadata.json @@ -14,8 +14,7 @@ "RelatedUrl": "", "AdditionalURLs": [ "https://cloud.google.com/compute/docs/instance-groups", - "https://cloud.google.com/load-balancing/docs/backend-service", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/mig-load-balancer-check.html" + "https://cloud.google.com/load-balancing/docs/backend-service" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_ip_forwarding_is_enabled/compute_instance_ip_forwarding_is_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_ip_forwarding_is_enabled/compute_instance_ip_forwarding_is_enabled.metadata.json index 764f81757e..e9f39a18a4 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_ip_forwarding_is_enabled/compute_instance_ip_forwarding_is_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_ip_forwarding_is_enabled/compute_instance_ip_forwarding_is_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_instance_ip_forwarding_is_enabled", - "CheckTitle": "Ensure That IP Forwarding Is Not Enabled on Instances", + "CheckTitle": "Compute Engine VM instance has IP forwarding disabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "Compute Engine instance cannot forward a packet unless the source IP address of the packet matches the IP address of the instance. Similarly, GCP won't deliver a packet whose destination IP address is different than the IP address of the instance receiving the packet. However, both capabilities are required if you want to use instances to help route packets. Forwarding of data packets should be disabled to prevent data loss or information disclosure.", - "Risk": "When the IP Forwarding feature is enabled on a virtual machine's network interface (NIC), it allows the VM to act as a router and receive traffic addressed to other destinations. ", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/disable-ip-forwarding.html", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Compute Engine VM instances** with `canIpForward` enabled are identified. This setting allows a VM to process packets not addressed to its own IP.\n\nInstances created by GKE (`gke-` prefix) are excluded from this evaluation.", + "Risk": "With **IP forwarding** a VM can route traffic for other addresses. If compromised, it can:\n- Spoof or tamper flows (**integrity**)\n- Intercept/redirect internal traffic (**confidentiality**)\n- Mask egress for exfiltration and enable lateral movement, degrading **availability**", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/compute/docs/instances/create-start-instance", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/disable-ip-forwarding.html" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud compute instances update-from-file --zone --source= --most-disruptive-allowed-action=RESTART", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_12", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_12#terraform" + "Other": "1. In Google Cloud console, go to Compute Engine > VM instances and select the VM (exclude names starting with gke-)\n2. Click Delete to remove the instance with IP forwarding enabled\n3. Click Create instance\n4. Expand Networking > Network interfaces > Edit and ensure IP forwarding is Off (default)\n5. Click Create", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-micro\"\n zone = \"\"\n\n can_ip_forward = false # Critical: disables IP forwarding to pass the check\n\n boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-12\"\n }\n }\n\n network_interface {\n network = \"default\"\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that IP Forwarding feature is not enabled at the Google Compute Engine instance level for security and compliance reasons, as instances with IP Forwarding enabled act as routers/packet forwarders. Because IP forwarding is rarely required, except when the virtual machine (VM) is used as a network virtual appliance, each Google Cloud VM instance should be reviewed in order to decide whether the IP forwarding is really needed for the verified instance. IP Forwarding is enabled at the VM instance level and applies to all network interfaces (NICs) attached to the instance. In addition, Instances created by GKE should be excluded from this recommendation because they need to have IP forwarding enabled and cannot be changed. Instances created by GKE have names that start with \"gke- \".", - "Url": "https://cloud.google.com/compute/docs/instances/create-start-instance" + "Text": "Disable **IP forwarding** on general-purpose VMs and allow it only for vetted **network appliances**, following **least privilege**.\n\nEnforce **network segmentation**, restrict routes, review exceptions regularly, and monitor egress to uphold **defense in depth**. *Exclude platform-managed nodes that require forwarding.*", + "Url": "https://hub.prowler.com/check/compute_instance_ip_forwarding_is_enabled" } }, - "Categories": [], + "Categories": [ + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_on_host_maintenance_migrate/compute_instance_on_host_maintenance_migrate.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_on_host_maintenance_migrate/compute_instance_on_host_maintenance_migrate.metadata.json index fe8d76dac0..bd580349f4 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_on_host_maintenance_migrate/compute_instance_on_host_maintenance_migrate.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_on_host_maintenance_migrate/compute_instance_on_host_maintenance_migrate.metadata.json @@ -13,7 +13,6 @@ "Risk": "VM instances configured with On Host Maintenance set to `TERMINATE` will be shut down during host maintenance events, causing **service interruptions** and **unplanned downtime**. This can impact application availability and may require manual intervention to restart services.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/configure-maintenance-behavior.html", "https://cloud.google.com/compute/docs/instances/setting-instance-scheduling-options" ], "Remediation": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_preemptible_vm_disabled/compute_instance_preemptible_vm_disabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_preemptible_vm_disabled/compute_instance_preemptible_vm_disabled.metadata.json index 5e8b62c6ce..7513e346da 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_preemptible_vm_disabled/compute_instance_preemptible_vm_disabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_preemptible_vm_disabled/compute_instance_preemptible_vm_disabled.metadata.json @@ -14,8 +14,7 @@ "RelatedUrl": "", "AdditionalURLs": [ "https://cloud.google.com/compute/docs/instances/preemptible", - "https://cloud.google.com/compute/docs/instances/spot", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/disable-preemptibility.html" + "https://cloud.google.com/compute/docs/instances/spot" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_public_ip/compute_instance_public_ip.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_public_ip/compute_instance_public_ip.metadata.json index 469772e43a..4fed0003e7 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_public_ip/compute_instance_public_ip.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_public_ip/compute_instance_public_ip.metadata.json @@ -1,27 +1,29 @@ { "Provider": "gcp", "CheckID": "compute_instance_public_ip", - "CheckTitle": "Check for Virtual Machine Instances with Public IP Addresses", + "CheckTitle": "VM instance does not have a public IP address", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "Check for Virtual Machine Instances with Public IP Addresses", - "Risk": "To reduce your attack surface, Compute instances should not have public IP addresses. Instead, instances should be configured behind load balancers, to minimize the instance's exposure to the internet.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Compute Engine VM instances** with an assigned **external (public) IP address** on any network interface are identified.\n\nInstances without an external IP are considered internal-only.", + "Risk": "**Internet-exposed VMs** face automated scanning, **brute force**, and **remote exploit** attempts.\n\nCompromise can enable **data exfiltration**, **service account abuse**, and **lateral movement** within the VPC, while public endpoints invite **DDoS**, degrading availability.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/compute/docs/instances/connecting-to-instance" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud compute instances delete-access-config --access-config-name=\"External NAT\" --zone=", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-public-policies/bc_gcp_public_2", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-public-policies/bc_gcp_public_2#terraform" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Click the VM name\n3. Click Edit\n4. Under Network interfaces, set External IP to None\n5. Click Save", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-micro\"\n zone = \"\"\n\n boot_disk {\n initialize_params { image = \"debian-cloud/debian-11\" }\n }\n\n network_interface {\n network = \"default\" # Critical: no access_config block -> no public IP\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that your Google Compute Engine instances are not configured to have external IP addresses in order to minimize their exposure to the Internet.", - "Url": "https://cloud.google.com/compute/docs/instances/connecting-to-instance" + "Text": "Adopt **private-only VMs** and remove external IPs.\n- Place workloads behind **load balancers** or **reverse proxies**\n- Use **Cloud NAT** for egress; admin access via **IAP**, **VPN**, or a hardened **bastion**\n- Apply **least privilege** firewall rules and network segmentation for **defense in depth**", + "Url": "https://hub.prowler.com/check/compute_instance_public_ip" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/compute/compute_instance_serial_ports_in_use/compute_instance_serial_ports_in_use.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_serial_ports_in_use/compute_instance_serial_ports_in_use.metadata.json index e4e206fd9e..6633b7de87 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_serial_ports_in_use/compute_instance_serial_ports_in_use.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_serial_ports_in_use/compute_instance_serial_ports_in_use.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_instance_serial_ports_in_use", - "CheckTitle": "Ensure ‘Enable Connecting to Serial Ports’ Is Not Enabled for VM Instance", + "CheckTitle": "VM instance has 'Enable Connecting to Serial Ports' disabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "Interacting with a serial port is often referred to as the serial console, which is similar to using a terminal window, in that input and output is entirely in text mode and there is no graphical interface or mouse support. If you enable the interactive serial console on an instance, clients can attempt to connect to that instance from any IP address. Therefore interactive serial console support should be disabled.", - "Risk": "If you enable the interactive serial console on your VM instance, clients can attempt to connect to your instance from any IP address and this allows anybody to access the instance if they know the user name, the SSH key, the project ID, and the instance name and zone.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Compute Engine VM instance** with the **interactive serial console** enabled via metadata `serial-port-enable` (`1`/`true`). Instances with this flag disabled do not allow interactive serial console connections.", + "Risk": "Enabling the **serial console** creates **out-of-band access** that can bypass network controls. Abuse can grant low-level OS interaction, expose sensitive boot logs, alter configuration, or disrupt services, degrading **confidentiality**, **integrity**, and **availability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/compute", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/disable-interactive-serial-console-support.html" + ], "Remediation": { "Code": { "CLI": "gcloud compute instances add-metadata --zone= --metadata=serial-port-enable=false", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/disable-interactive-serial-console-support.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_11#terraform" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Click the target VM name, then click Edit\n3. Uncheck \"Enable connecting to serial ports\"\n4. Click Save", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-micro\"\n zone = \"us-central1-a\"\n\n boot_disk {\n initialize_params { image = \"debian-cloud/debian-12\" }\n }\n\n network_interface { network = \"default\" }\n\n metadata = {\n serial-port-enable = \"false\" # Critical: disables connecting to serial ports to pass the check\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that \"Enable connecting to serial ports\" configuration setting is disabled for all your production Google Compute Engine instances. A Google Cloud virtual machine (VM) instance has 4 virtual serial ports. On your VM instances, the operating system (OS), BIOS, and other system-level entities write often output data to the serial ports and can accept input, such as commands or answers, to prompts. Usually, these system-level entities use the first serial port (Port 1) and Serial Port 1 is often referred to as the interactive serial console. This interactive serial console does not support IP-based access restrictions such as IP address whitelists. To adhere to cloud security best practices and reduce the risk of unauthorized access, interactive serial console support should be disabled for all instances used in production.", - "Url": "https://cloud.google.com/compute" + "Text": "Disable the **interactive serial console** on production VMs (`serial-port-enable=false`). Use it only for *break-glass* cases. Enforce **least privilege** for console roles, prefer controlled access paths (IAP/SSH or session tools), and monitor access. Apply **defense in depth** to reduce alternate entry points.", + "Url": "https://hub.prowler.com/check/compute_instance_serial_ports_in_use" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_shielded_vm_enabled/compute_instance_shielded_vm_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_shielded_vm_enabled/compute_instance_shielded_vm_enabled.metadata.json index d7d6c30ce8..8b4d09acc0 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_shielded_vm_enabled/compute_instance_shielded_vm_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_shielded_vm_enabled/compute_instance_shielded_vm_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_instance_shielded_vm_enabled", - "CheckTitle": "Ensure Compute Instances Are Launched With Shielded VM Enabled", + "CheckTitle": "Compute instance has vTPM and Integrity Monitoring enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "To defend against advanced threats and ensure that the boot loader and firmware on your VMs are signed and untampered, it is recommended that Compute instances are launched with Shielded VM enabled.", - "Risk": "Whithout shielded VM enabled is not possible to defend against advanced threats and ensure that the boot loader and firmware on your Google Compute Engine instances are signed and untampered.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "Compute Engine VM instances have **vTPM** and **Integrity Monitoring** enabled as part of Shielded VM configuration.", + "Risk": "Without **vTPM** or **Integrity Monitoring**, boot integrity isn't verified. Attackers can persist **bootkits/rootkits**, alter firmware, and evade attestation, enabling covert control and data theft.\n- Integrity: compromised boot chain\n- Confidentiality: secrets bound to TPM exposed\n- Availability: malicious boot code can brick VMs", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/compute/docs/instances/modifying-shielded-vm", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/enable-shielded-vm.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute instances update --shielded-vtpm --shielded-vmintegrity-monitoring", + "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-shielded-vm.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/bc_gcp_general_y#terraform" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Click the VM name\n3. Click Stop and wait for the VM to stop\n4. Click Edit\n5. In Shielded VM, enable vTPM and enable Integrity monitoring\n6. Click Save\n7. Click Start to start the VM", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-micro\"\n\n boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-11\"\n }\n }\n\n network_interface {\n network = \"default\"\n }\n\n shielded_instance_config {\n enable_vtpm = true # Critical: enable vTPM\n enable_integrity_monitoring = true # Critical: enable Integrity Monitoring\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that your Google Compute Engine instances are configured to use Shielded VM security feature for protection against rootkits and bootkits.Google Compute Engine service can enable 3 advanced security components for Shielded VM instances: 1. Virtual Trusted Platform Module (vTPM) - this component validates the guest virtual machine (VM) pre-boot and boot integrity, and provides key generation and protection. 2. Integrity Monitoring - lets you monitor and verify the runtime boot integrity of your shielded VM instances using Google Cloud Operations reports (also known as Stackdriver reports). 3. Secure boot helps - this security component protects your VM instances against boot-level and kernel-level malware and rootkits. To defend against advanced threats and ensure that the boot loader and firmware on your Google Compute Engine instances are signed and untampered, it is strongly recommended that your production instances are launched with Shielded VM enabled.", - "Url": "https://cloud.google.com/compute/docs/instances/modifying-shielded-vm" + "Text": "Enable **Shielded VM** with `vTPM` and **Integrity Monitoring** set to `enabled` on all VMs. Prefer **Secure Boot** where compatible. Enforce via hardened images/templates, apply **least privilege** to shielded settings, and monitor integrity results-supporting **defense in depth** and trusted boot.", + "Url": "https://hub.prowler.com/check/compute_instance_shielded_vm_enabled" } }, - "Categories": [], + "Categories": [ + "node-security" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.metadata.json index 8c6069c6b6..8b19b1dab5 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.metadata.json @@ -13,7 +13,6 @@ "Risk": "Multiple network interfaces on a VM instance can:\n\n- **Expand attack surface** by providing additional entry points for unauthorized access\n- **Create unintended network paths** that bypass security controls\n- **Increase management complexity** leading to potential misconfigurations", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/vms-with-multiple-enis.html", "https://cloud.google.com/vpc/docs/multiple-interfaces-concepts" ], "Remediation": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json index d90b06b906..3e65d6f844 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json @@ -13,8 +13,7 @@ "Risk": "Persistent disks on suspended VM instances remain accessible through the GCP API and may contain **sensitive data**, creating potential security risks:\n\n- **Unauthorized data access** if credentials are compromised or permissions are misconfigured\n- **Data exposure** from forgotten infrastructure that is no longer actively monitored\n- **Security blind spots** where suspended resources are overlooked during security reviews and audits", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/icompute/docs/instances/suspend-resume-instance", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/persistent-disks-attached-to-suspended-vms.html" + "https://cloud.google.com/icompute/docs/instances/suspend-resume-instance" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_loadbalancer_logging_enabled/compute_loadbalancer_logging_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_loadbalancer_logging_enabled/compute_loadbalancer_logging_enabled.metadata.json index 09f4c593a4..d5c6b9e3bc 100644 --- a/prowler/providers/gcp/services/compute/compute_loadbalancer_logging_enabled/compute_loadbalancer_logging_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_loadbalancer_logging_enabled/compute_loadbalancer_logging_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "compute_loadbalancer_logging_enabled", - "CheckTitle": "Ensure Logging is enabled for HTTP(S) Load Balancer", + "CheckTitle": "HTTP(S) load balancer has logging enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "LoadBalancer", - "ResourceGroup": "network", - "Description": "Logging enabled on a HTTPS Load Balancer will show all network traffic and its destination.", - "Risk": "HTTP(S) load balancing log entries contain information useful for monitoring and debugging web traffic. Google Cloud exports this logging data to Cloud Monitoring service so that monitoring metrics can be created to evaluate a load balancer's configuration, usage, and performance, troubleshoot problems, and improve resource utilization and user experience.", + "Severity": "high", + "ResourceType": "compute.googleapis.com/BackendService", + "Description": "**Application Load Balancer** (HTTP/S) backend services have **Cloud Logging for requests** enabled at the backend service level.\n\n*Only load balancers with a backend service support this setting.*", + "Risk": "Without **request logs**, visibility into HTTP(S) traffic is reduced, hindering detection of credential stuffing, path traversal, WAF bypass, and data exfiltration. This impacts **confidentiality** and **integrity**, and delays incident response; availability issues (surges in `5xx`) may go unnoticed.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/load-balancing/docs/https/https-logging-monitoring#gcloud:-global-mode", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLoadBalancing/https-load-balancer-logging-enabled.html", + "https://cloud.google.com/load-balancing/docs/l7-internal/monitoring" + ], "Remediation": { "Code": { - "CLI": "gcloud compute backend-services update --region=REGION --enable-logging --logging-sample-rate=", + "CLI": "gcloud compute backend-services update --global --enable-logging", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLoadBalancing/enableLoad-balancing-backend-service-logging.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Networking > Load balancing\n2. Click your HTTP(S) load balancer, then click Edit\n3. Open Backend configuration and click Edit next to the backend service\n4. Check Enable logging\n5. Click Update (backend service), then Update (load balancer)\n6. Verify logs appear in Logs Explorer under Cloud HTTP Load Balancer", + "Terraform": "```hcl\nresource \"google_compute_backend_service\" \"\" {\n name = \"\"\n health_checks = [\"\"]\n\n log_config {\n enable = true # Critical: enables logging on the backend service\n }\n}\n```" }, "Recommendation": { - "Text": "Logging will allow you to view HTTPS network traffic to your web applications.", - "Url": "https://cloud.google.com/load-balancing/docs/https/https-logging-monitoring#gcloud:-global-mode" + "Text": "Enable **request logging** on backend services with a risk-appropriate `sampleRate`; include key optional fields when needed. Export logs to monitoring for alerts and dashboards, enforce retention and integrity controls, and restrict access using **least privilege** and **defense in depth**.", + "Url": "https://hub.prowler.com/check/compute_loadbalancer_logging_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_network_default_in_use/compute_network_default_in_use.metadata.json b/prowler/providers/gcp/services/compute/compute_network_default_in_use/compute_network_default_in_use.metadata.json index a2c7210558..720639e153 100644 --- a/prowler/providers/gcp/services/compute/compute_network_default_in_use/compute_network_default_in_use.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_network_default_in_use/compute_network_default_in_use.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_network_default_in_use", - "CheckTitle": "Ensure that the default network does not exist", + "CheckTitle": "Project does not have a default VPC network", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Network", - "ResourceGroup": "network", - "Description": "Ensure that the default network does not exist", - "Risk": "The default network has a preconfigured network configuration and automatically generates insecure firewall rules.", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/default-vpc-in-use.html", + "Severity": "medium", + "ResourceType": "compute.googleapis.com/Network", + "Description": "Projects are assessed for a **VPC network** named `default` (the pre-created, auto-mode network).", + "Risk": "Using the **default VPC** can weaken segmentation and expose services via **permissive firewall rules** (e.g., broad internal trust or public admin ports). This increases likelihood of **unauthorized access**, **lateral movement**, and data exfiltration, impacting **confidentiality** and **integrity**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/default-vpc-in-use.html", + "https://cloud.google.com/vpc/docs/using-vpc" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud compute networks delete default --quiet", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_7", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_7#terraform" + "Other": "1. In Google Cloud Console, go to Networking > VPC network > VPC networks\n2. Select the network named \"default\"\n3. Click Delete VPC network and confirm\n4. If deletion is blocked, remove or migrate any resources using the \"default\" network, then retry Delete", + "Terraform": "```hcl\n# Deletes the default VPC network to pass the check\nresource \"google_project_default_network\" \"\" {} # Ensures the 'default' network is removed\n```" }, "Recommendation": { - "Text": "When an organization deletes the default network, it may need to migrate or service onto a new network.", - "Url": "https://cloud.google.com/vpc/docs/using-vpc" + "Text": "Prefer **custom VPCs** over `default`. Remove unused default networks and apply **least privilege** with explicit firewall rules, private connectivity, and workload-based segmentation. Enforce creation controls (e.g., org policy to skip default network) and use **defense in depth** with logging and monitoring.", + "Url": "https://hub.prowler.com/check/compute_network_default_in_use" } }, - "Categories": [], + "Categories": [ + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_network_dns_logging_enabled/compute_network_dns_logging_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_network_dns_logging_enabled/compute_network_dns_logging_enabled.metadata.json index 6a11377f1a..1a4b5afc7a 100644 --- a/prowler/providers/gcp/services/compute/compute_network_dns_logging_enabled/compute_network_dns_logging_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_network_dns_logging_enabled/compute_network_dns_logging_enabled.metadata.json @@ -1,30 +1,37 @@ { "Provider": "gcp", "CheckID": "compute_network_dns_logging_enabled", - "CheckTitle": "Enable Cloud DNS Logging for VPC Networks", + "CheckTitle": "VPC network has Cloud DNS logging enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Network", - "ResourceGroup": "network", - "Description": "Ensure that Cloud DNS logging is enabled for all your Virtual Private Cloud (VPC) networks using DNS server policies. Cloud DNS logging records queries that the name servers resolve for your Google Cloud VPC networks, as well as queries from external entities directly to a public DNS zone. Recorded queries can come from virtual machine (VM) instances, GKE containers running in the same VPC network, peering zones, or other Google Cloud resources provisioned within your VPC.", - "Risk": "Cloud DNS logging is disabled by default on each Google Cloud VPC network. By enabling monitoring of Cloud DNS logs, you can increase visibility into the DNS names requested by the clients within your VPC network. Cloud DNS logs can be monitored for anomalous domain names and evaluated against threat intelligence.", + "ResourceType": "compute.googleapis.com/Network", + "Description": "**VPC networks** are assessed for a **DNS policy** that enables **Cloud DNS query logging**. When present, resolvers record queries for the network from VMs, GKE, peering, and inbound forwarding, with entries written to Cloud Logging.", + "Risk": "Without **DNS query logs**, suspicious lookups (C2, DGA, DNS exfiltration) go unseen, reducing **confidentiality** and hindering **incident response**. Visibility gaps also hide misconfigurations and elevated `NXDOMAIN` rates that can impact the **availability** of name resolution.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/dns/docs/monitoring", + "https://docs.cloud.google.com/compute/docs/networking/monitor-dns-failures", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/dns-logging-for-vpcs.html" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/dns-logging-for-vpcs.html", - "Terraform": "" + "Other": "1. In the Google Cloud console, go to Cloud DNS > Policies\n2. If the VPC already has a policy: select the policy, click Edit, check Enable logging, click Save\n3. If there is no policy for the VPC: click Create policy, enter a name, check Enable logging, add the target VPC network, click Create", + "Terraform": "```hcl\nresource \"google_dns_policy\" \"\" {\n name = \"\"\n enable_logging = true # CRITICAL: turns on DNS query logging for the policy\n\n networks {\n network_url = \"projects//global/networks/\" # Attach to the target VPC\n }\n}\n```" }, "Recommendation": { - "Text": "Cloud DNS logging records the queries from the name servers within your VPC to Stackdriver. Logged queries can come from Compute Engine VMs, GKE containers, or other GCP resources provisioned within the VPC.", - "Url": "https://cloud.google.com/dns/docs/monitoring" + "Text": "Enable **Cloud DNS query logging** for all VPC networks via **DNS policies** and route logs to centralized analysis. Enforce **least privilege** on log access, set retention and sampling to manage cost, and add detections for malicious domains. Apply **defense in depth** with DNS response policies and egress controls.", + "Url": "https://hub.prowler.com/check/compute_network_dns_logging_enabled" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_network_not_legacy/compute_network_not_legacy.metadata.json b/prowler/providers/gcp/services/compute/compute_network_not_legacy/compute_network_not_legacy.metadata.json index 1a1e15cc84..3b0406c2a5 100644 --- a/prowler/providers/gcp/services/compute/compute_network_not_legacy/compute_network_not_legacy.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_network_not_legacy/compute_network_not_legacy.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "compute_network_not_legacy", - "CheckTitle": "Ensure Legacy Networks Do Not Exist", + "CheckTitle": "VPC network is not legacy", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Network", - "ResourceGroup": "network", - "Description": "In order to prevent use of legacy networks, a project should not have a legacy network configured. As of now, Legacy Networks are gradually being phased out, and you can no longer create projects with them. This recommendation is to check older projects to ensure that they are not using Legacy Networks.", - "Risk": "Google Cloud legacy networks have a single global IPv4 range which cannot be divided into subnets, and a single gateway IP address for the whole network. Legacy networks do not support several Google Cloud networking features such as subnets, alias IP ranges, multiple network interfaces, Cloud NAT (Network Address Translation), Virtual Private Cloud (VPC) Peering, and private access options for GCP services. Legacy networks are not recommended for high network traffic projects and are subject to a single point of contention or failure.", + "ResourceType": "compute.googleapis.com/Network", + "Description": "**Google Cloud networks** are evaluated for **legacy mode** (`subnet_mode: legacy`). The finding highlights networks using the older, non-subnetted design instead of **VPC with regional subnets**.", + "Risk": "Legacy networks lack subnets, peering, and private access. This reduces isolation and forces public IP paths, weakening **confidentiality** and enabling lateral movement/data exfiltration. Coarse controls and routing limits threaten **integrity**. A single global range and gateway create contention that can degrade **availability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/legacy-vpc-in-use.html", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/legacy-vpc-in-use.html#", + "https://cloud.google.com/vpc/docs/using-legacy#deleting_a_legacy_network" + ], "Remediation": { "Code": { - "CLI": "gcloud compute networks delete ", + "CLI": "gcloud beta compute networks update --switch-to-custom-subnet-mode", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/legacy-vpc-in-use.html#", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/ensure-legacy-networks-do-not-exist-for-a-project#terraform" + "Other": "1. In Google Cloud Console, go to Networking > VPC network > VPC networks\n2. Find the network with Subnet creation mode showing Legacy\n3. Select it and click Delete VPC network\n4. Type the network name to confirm and click Delete", + "Terraform": "" }, "Recommendation": { - "Text": "Ensure that your Google Cloud Platform (GCP) projects are not using legacy networks as this type of network is no longer recommended for production environments because it does not support advanced networking features. Instead, it is strongly recommended to use Virtual Private Cloud (VPC) networks for existing and future GCP projects.", - "Url": "https://cloud.google.com/vpc/docs/using-legacy#deleting_a_legacy_network" + "Text": "Decommission legacy networks. Migrate to **custom-mode VPCs** with regional subnets and granular firewall policies. Apply **least privilege** segmentation, enable private access and **Cloud NAT** to avoid public exposure, and use peering or private connectivity for dependencies. *Plan and test migration to limit downtime*.", + "Url": "https://hub.prowler.com/check/compute_network_not_legacy" } }, - "Categories": [], + "Categories": [ + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_project_os_login_2fa_enabled/compute_project_os_login_2fa_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_project_os_login_2fa_enabled/compute_project_os_login_2fa_enabled.metadata.json index bec06a7838..d41fd0fe00 100644 --- a/prowler/providers/gcp/services/compute/compute_project_os_login_2fa_enabled/compute_project_os_login_2fa_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_project_os_login_2fa_enabled/compute_project_os_login_2fa_enabled.metadata.json @@ -13,8 +13,7 @@ "Risk": "Without 2FA enforcement, compromised credentials (stolen SSH keys or passwords) grant immediate access to VM instances. Attackers could:\n\n- Gain unauthorized shell access to production systems\n- Exfiltrate sensitive data or deploy malware\n- Move laterally within the infrastructure\n\nThis single point of failure significantly increases the attack surface.", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/compute/docs/oslogin/set-up-oslogin", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-os-login-with-2fa-authentication.html" + "https://cloud.google.com/compute/docs/oslogin/set-up-oslogin" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled.metadata.json index 0a29243bb8..b650c6effd 100644 --- a/prowler/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_project_os_login_enabled", - "CheckTitle": "Ensure Os Login Is Enabled for a Project", + "CheckTitle": "Project has OS Login enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "GCPProject", - "ResourceGroup": "governance", - "Description": "Ensure that the OS Login feature is enabled at the Google Cloud Platform (GCP) project level in order to provide you with centralized and automated SSH key pair management.", - "Risk": "Enabling OS Login feature ensures that the SSH keys used to connect to VM instances are mapped with Google Cloud IAM users. Revoking access to corresponding IAM users will revoke all the SSH keys associated with these users, therefore it facilitates centralized SSH key pair management, which is extremely useful in handling compromised or stolen SSH key pairs and/or revocation of external/third-party/vendor users.", + "ResourceType": "compute.googleapis.com/Project", + "Description": "Project metadata has **OS Login** enabled (`enable-oslogin`), so VM SSH access uses IAM-linked Linux identities instead of static project or instance keys.", + "Risk": "Without **OS Login**, SSH relies on static metadata keys that are hard to rotate and revoke. Leaked or orphaned keys can retain VM access, enabling unauthorized commands, data exfiltration, and lateral movement-impacting **confidentiality** and **integrity** and weakening accountability.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/enable-os-login.html", + "https://cloud.google.com/compute/confidential-vm/docs/creating-cvm-instance:https://cloud.google.com/compute/confidential-vm/docs/about-cvm:https://cloud.google.com/confidential-computing:https://cloud.google.com/blog/products/identity-security/introducing-google-cloud-confidential-computing-with-confidential-vms" + ], "Remediation": { "Code": { "CLI": "gcloud compute project-info add-metadata --metadata enable-oslogin=TRUE", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-os-login.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_9#terraform" + "Other": "1. In Google Cloud Console, select your project\n2. Go to Compute Engine > Metadata\n3. Click Edit > Add item\n4. Set Key to enable-oslogin and Value to TRUE\n5. Click Save", + "Terraform": "```hcl\nresource \"google_compute_project_metadata_item\" \"\" {\n # Critical: this key/value enables OS Login at the project level\n key = \"enable-oslogin\"\n value = \"TRUE\"\n}\n```" }, "Recommendation": { - "Text": "Ensure that the OS Login feature is enabled at the Google Cloud Platform (GCP) project level in order to provide you with centralized and automated SSH key pair management.", - "Url": "https://cloud.google.com/compute/confidential-vm/docs/creating-cvm-instance:https://cloud.google.com/compute/confidential-vm/docs/about-cvm:https://cloud.google.com/confidential-computing:https://cloud.google.com/blog/products/identity-security/introducing-google-cloud-confidential-computing-with-confidential-vms" + "Text": "Enable **OS Login** at the project level to centralize SSH through **IAM**.\n- Apply **least privilege** to OS Login roles\n- Remove metadata SSH keys\n- Enforce MFA and short-lived credentials\n- Monitor login activity and add network restrictions for **defense in depth**", + "Url": "https://hub.prowler.com/check/compute_project_os_login_enabled" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_public_address_shodan/compute_public_address_shodan.metadata.json b/prowler/providers/gcp/services/compute/compute_public_address_shodan/compute_public_address_shodan.metadata.json index 3839f5a668..d7435f0438 100644 --- a/prowler/providers/gcp/services/compute/compute_public_address_shodan/compute_public_address_shodan.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_public_address_shodan/compute_public_address_shodan.metadata.json @@ -1,29 +1,27 @@ { "Provider": "gcp", "CheckID": "compute_public_address_shodan", - "CheckTitle": "Check if any of the Public Addresses are in Shodan (requires Shodan API KEY).", - "CheckType": [ - "Infrastructure Security" - ], + "CheckTitle": "Public IP address is not listed in Shodan", + "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "GCPComputeAddress", - "ResourceGroup": "network", - "Description": "Check if any of the Public Addresses are in Shodan (requires Shodan API KEY).", - "Risk": "Sites like Shodan index exposed systems and further expose them to wider audiences as a quick way to find exploitable systems.", + "Severity": "medium", + "ResourceType": "compute.googleapis.com/Address", + "Description": "**Compute Engine** public IP addresses are cross-checked with **Shodan** to identify Internet-exposed hosts that have been indexed, including observed open ports and metadata.\n\n*Only `EXTERNAL` addresses are evaluated.*", + "Risk": "Being listed in **Shodan** indicates an Internet-reachable host with identifiable services. Adversaries can quickly enumerate ports, run brute-force or exploit scans, and weaponize misconfigurations, leading to data exposure (C), service tampering (I), and outages from abuse or DDoS (A).", "RelatedUrl": "", + "AdditionalURLs": [], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud compute addresses delete --region ", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. In the Google Cloud Console, go to: VPC network > IP addresses > External\n2. Find the public IP shown in the finding\n3. If it is attached to a VM: go to the VM > Edit > Network interfaces > set External IP to None > Save\n4. Return to External IP addresses and click Release to delete the public IP", + "Terraform": "```hcl\n# Reserve an internal address instead of a public one\nresource \"google_compute_address\" \"\" {\n name = \"\"\n region = \"\"\n subnetwork = \"\"\n address_type = \"INTERNAL\" # FIX: use INTERNAL to avoid a public (EXTERNAL) IP listed by Shodan\n}\n```" }, "Recommendation": { - "Text": "Check Identified IPs, consider changing them to private ones and delete them from Shodan.", - "Url": "https://www.shodan.io/" + "Text": "Minimize Internet exposure:\n- Remove unused public IPs; prefer private addressing with controlled egress\n- Avoid `0.0.0.0/0`; restrict by allowlists and firewall policies\n- Place services behind proxies/VPN/bastions; close unused ports\n\nApply **least privilege** and **defense in depth**; continuously monitor external footprint.", + "Url": "https://hub.prowler.com/check/compute_public_address_shodan" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/compute/compute_snapshot_not_outdated/compute_snapshot_not_outdated.metadata.json b/prowler/providers/gcp/services/compute/compute_snapshot_not_outdated/compute_snapshot_not_outdated.metadata.json index ddf5920900..2efc613161 100644 --- a/prowler/providers/gcp/services/compute/compute_snapshot_not_outdated/compute_snapshot_not_outdated.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_snapshot_not_outdated/compute_snapshot_not_outdated.metadata.json @@ -13,7 +13,6 @@ "Risk": "Outdated snapshots containing **sensitive data** expand the **attack surface** and risk data exposure if compromised.\n\nStale snapshots may violate compliance requirements, complicate disaster recovery efforts, and introduce configuration drift that affects system **integrity**.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/remove-old-disk-snapshots.html", "https://cloud.google.com/compute/docs/disks/create-snapshots", "https://cloud.google.com/compute/docs/disks/snapshot-best-practices" ], diff --git a/prowler/providers/gcp/services/compute/compute_subnet_flow_logs_enabled/compute_subnet_flow_logs_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_subnet_flow_logs_enabled/compute_subnet_flow_logs_enabled.metadata.json index e056d32ee3..3f9d1e0938 100644 --- a/prowler/providers/gcp/services/compute/compute_subnet_flow_logs_enabled/compute_subnet_flow_logs_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_subnet_flow_logs_enabled/compute_subnet_flow_logs_enabled.metadata.json @@ -1,30 +1,41 @@ { "Provider": "gcp", "CheckID": "compute_subnet_flow_logs_enabled", - "CheckTitle": "Enable VPC Flow Logs for VPC Subnets", + "CheckTitle": "Subnet has VPC Flow Logs enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Subnet", - "ResourceGroup": "network", - "Description": "Ensure that VPC Flow Logs is enabled for every subnet created within your production Virtual Private Cloud (VPC) network. Flow Logs is a logging feature that enables users to capture information about the IP traffic (accepted, rejected, or all traffic) going to and from the network interfaces (ENIs) available within your VPC subnets.", - "Risk": "By default, the VPC Flow Logs feature is disabled when a new VPC network subnet is created. Once enabled, VPC Flow Logs will start collecting network traffic data to and from your Virtual Private Cloud (VPC) subnets, logging data that can be useful for understanding network usage, network traffic expense optimization, network forensics, and real-time security analysis. To enhance Google Cloud VPC network visibility and security it is strongly recommended to enable Flow Logs for every business-critical or production VPC subnet.", + "ResourceType": "compute.googleapis.com/Subnetwork", + "Description": "**GCP VPC subnets** have **VPC Flow Logs** enabled at the subnet scope to capture connection metadata for traffic to and from VM interfaces.", + "Risk": "Without **VPC Flow Logs**, network activity lacks visibility, weakening **detection and response**. Blind spots enable covert **data exfiltration** (C), undetected **lateral movement** and policy bypass (I), and hinder containment and recovery (A). Forensics and cost insights are degraded.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/vpc/docs/using-flow-logs#enabling_vpc_flow_logging", + "https://docs.cloud.google.com/vpc/docs/flow-logs", + "https://docs.cloud.google.com/vpc/docs/org-policy-flow-logs", + "https://docs.cloud.google.com/vpc/docs/access-flow-logs", + "https://cloud.google.com/blog/products/networking/how-to-use-vpc-flow-logs-in-gcp-for-network-traffic-analysis", + "https://docs.cloud.google.com/vpc/docs/using-flow-logs", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/enable-vpc-flow-logs.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute networks subnets update [SUBNET_NAME] --region [REGION] --enable-flow-logs", + "CLI": "gcloud compute networks subnets update --region --enable-flow-logs", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/enable-vpc-flow-logs.html", - "Terraform": "https://docs.prowler.com/checks/gcp/logging-policies-1/bc_gcp_logging_1#terraform" + "Other": "1. In the Google Cloud console, go to Networking > VPC networks\n2. Open the Subnets tab and click the target subnet\n3. Click Edit\n4. Set Flow logs to On\n5. Click Save", + "Terraform": "```hcl\nresource \"google_compute_subnetwork\" \"\" {\n name = \"\"\n ip_cidr_range = \"10.0.0.0/24\"\n region = \"\"\n network = \"\"\n\n enable_flow_logs = true # Critical: enables VPC Flow Logs so the subnet passes the check\n}\n```" }, "Recommendation": { - "Text": "Ensure that VPC Flow Logs is enabled for every subnet created within your production Virtual Private Cloud (VPC) network. Flow Logs is a logging feature that enables users to capture information about the IP traffic (accepted, rejected, or all traffic) going to and from the network interfaces (ENIs) available within your VPC subnets.", - "Url": "https://cloud.google.com/vpc/docs/using-flow-logs#enabling_vpc_flow_logging" + "Text": "Enable **VPC Flow Logs** on all production subnets. Tune aggregation, sampling, and metadata to balance visibility and cost.\n\nExport to centralized logging for analytics and alerting, apply **least privilege** to log access, and use organization guardrails to enforce consistent coverage as part of **defense in depth**.", + "Url": "https://hub.prowler.com/check/compute_subnet_flow_logs_enabled" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/dataproc/dataproc_encrypted_with_cmks_disabled/dataproc_encrypted_with_cmks_disabled.metadata.json b/prowler/providers/gcp/services/dataproc/dataproc_encrypted_with_cmks_disabled/dataproc_encrypted_with_cmks_disabled.metadata.json index ad781dec04..92dde729c1 100644 --- a/prowler/providers/gcp/services/dataproc/dataproc_encrypted_with_cmks_disabled/dataproc_encrypted_with_cmks_disabled.metadata.json +++ b/prowler/providers/gcp/services/dataproc/dataproc_encrypted_with_cmks_disabled/dataproc_encrypted_with_cmks_disabled.metadata.json @@ -1,32 +1,34 @@ { "Provider": "gcp", "CheckID": "dataproc_encrypted_with_cmks_disabled", - "CheckTitle": "Ensure that Dataproc Cluster is encrypted using Customer-Managed Encryption Key", + "CheckTitle": "Dataproc cluster is encrypted with a customer-managed encryption key (CMEK)", "CheckType": [], "ServiceName": "dataproc", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Cluster", - "ResourceGroup": "container", - "Description": "When you use Dataproc, cluster and job data is stored on Persistent Disks (PDs) associated with the Compute Engine VMs in your cluster and in a Cloud Storage staging bucket. This PD and bucket data is encrypted using a Google-generated data encryption key (DEK) and key encryption key (KEK). The CMEK feature allows you to create, use, and revoke the key encryption key (KEK). Google still controls the data encryption key (DEK).", - "Risk": "The Dataproc cluster data is encrypted using a Google-generated Data Encryption Key (DEK) and a Key Encryption Key (KEK). If you need to control and manage your cluster data encryption yourself, you can use your own Customer-Managed Keys (CMKs). Cloud KMS Customer-Managed Keys can be implemented as an additional security layer on top of existing data encryption, and are often used in the enterprise world, where compliance and security controls are very strict.", + "Severity": "medium", + "ResourceType": "dataproc.googleapis.com/Cluster", + "Description": "Dataproc clusters use **Customer-Managed Encryption Keys** (`CMEK`) for VM **persistent disk** encryption. The finding determines whether a customer KMS key is configured for disk data instead of the default Google-managed keys.", + "Risk": "Without **CMEK** on Dataproc disks, keys remain provider-controlled, limiting **rotation**, **revocation**, and **location control**. This reduces containment if disks or snapshots are exposed and may block **data sovereignty** requirements, impacting **confidentiality** and incident response.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/Dataproc/enable-encryption-with-cmks-for-dataproc-clusters.html", + "https://cloud.google.com/dataproc/docs/concepts/configuring-clusters/customer-managed-encryption" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/Dataproc/enable-encryption-with-cmks-for-dataproc-clusters.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/ensure-gcp-dataproc-cluster-is-encrypted-with-customer-supplied-encryption-keys-cseks#terraform" + "Other": "1. In Google Cloud Console, go to Dataproc > Clusters\n2. Click Create cluster\n3. In Cluster configuration, open Security (or Encryption)\n4. For Disk encryption key, select Customer-managed key and choose your Cloud KMS key\n5. Click Create\n6. Migrate workloads to the new cluster and delete the old non-CMEK cluster", + "Terraform": "```hcl\nresource \"google_dataproc_cluster\" \"\" {\n name = \"\"\n region = \"\"\n\n cluster_config {\n encryption_config {\n gce_pd_kms_key_name = \"projects//locations//keyRings//cryptoKeys/\" # FIX: Sets CMEK for persistent disks to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that your Google Cloud Dataproc clusters on Compute Engine are encrypted with Customer-Managed Keys (CMKs) in order to control the cluster data encryption/decryption process. You can create and manage your own Customer-Managed Keys (CMKs) with Cloud Key Management Service (Cloud KMS). Cloud KMS provides secure and efficient encryption key management, controlled key rotation, and revocation mechanisms.", - "Url": "https://cloud.google.com/dataproc/docs/concepts/configuring-clusters/customer-managed-encryption" + "Text": "Enable **CMEK** for Dataproc disk, job-argument, and staging-bucket encryption.\n- Grant KMS access with **least privilege** to required service accounts\n- Enforce **regular rotation** and support **revocation/disable** procedures\n- Keep keys co-located with data and monitor KMS usage\n- Consider **Cloud EKM** for external key control", + "Url": "https://hub.prowler.com/check/dataproc_encrypted_with_cmks_disabled" } }, "Categories": [ - "encryption", - "gen-ai" + "encryption" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/gcp/services/dns/dns_dnssec_disabled/dns_dnssec_disabled.metadata.json b/prowler/providers/gcp/services/dns/dns_dnssec_disabled/dns_dnssec_disabled.metadata.json index c0c0295695..81b7a96509 100644 --- a/prowler/providers/gcp/services/dns/dns_dnssec_disabled/dns_dnssec_disabled.metadata.json +++ b/prowler/providers/gcp/services/dns/dns_dnssec_disabled/dns_dnssec_disabled.metadata.json @@ -1,30 +1,40 @@ { "Provider": "gcp", "CheckID": "dns_dnssec_disabled", - "CheckTitle": "Ensure That DNSSEC Is Enabled for Cloud DNS", + "CheckTitle": "Cloud DNS managed zone has DNSSEC enabled", "CheckType": [], "ServiceName": "dns", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DNS_Zone", - "ResourceGroup": "network", - "Description": "Cloud Domain Name System (DNS) is a fast, reliable and cost-effective domain name system that powers millions of domains on the internet. Domain Name System Security Extensions (DNSSEC) in Cloud DNS enables domain owners to take easy steps to protect their domains against DNS hijacking and man-in-the-middle and other attacks.", - "Risk": "Attackers can hijack the process of domain/IP lookup and redirect users to malicious web content through DNS hijacking and Man-In-The-Middle (MITM) attacks.", + "ResourceType": "dns.googleapis.com/ManagedZone", + "Description": "**Cloud DNS managed zones** are assessed for **DNSSEC** status. Zones with DNSSEC sign zone data and publish `DNSKEY`/`RRSIG`; zones without it remain unsigned and unauthenticated.", + "Risk": "Without **DNSSEC**, DNS responses lack authenticity, enabling cache poisoning, spoofed referrals, and domain hijacking. Victims may be redirected to attacker hosts, exposing credentials and data (confidentiality), enabling tampered content and records (integrity), and causing misrouting or outages (availability).", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudDNS/enable-dns-sec.html", + "https://cloud.google.com/dns/docs/dnssec-config", + "https://cloud.google.com/sdk/gcloud/reference/dns/managed-zones/create?authuser=4", + "https://cloud.google.com/dns", + "https://docs.cloud.google.com/dns/docs/dnssec", + "https://cloud.google.com/dns/docs/dnssec-config?hl=vi", + "https://cloud.google.com/dns/docs/registrars?hl=Es" + ], "Remediation": { "Code": { "CLI": "gcloud dns managed-zones update --dnssec-state on", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudDNS/enable-dns-sec.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_5#terraform" + "Other": "1. In the Google Cloud Console, go to Cloud DNS\n2. Click the managed zone name\n3. Click Edit\n4. Under DNSSEC, select On\n5. Click Save", + "Terraform": "```hcl\nresource \"google_dns_managed_zone\" \"\" {\n name = \"\"\n dns_name = \"example.com.\"\n\n dnssec_config {\n state = \"on\" # Critical: enables DNSSEC for the managed zone\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that DNSSEC security feature is enabled for all your Google Cloud DNS managed zones in order to protect your domains against spoofing and cache poisoning attacks. By default, DNSSEC is not enabled for Google Cloud public DNS managed zones. DNSSEC security feature helps mitigate the risk of such attacks by encrypting signing DNS records. As a result, it prevents attackers from issuing fake DNS responses that may misdirect web clients to fake, fraudulent or scam websites.", - "Url": "https://cloud.google.com/dns/docs/dnssec-config" + "Text": "Enable **DNSSEC** on public zones and complete the chain of trust by publishing a `DS` record at your registrar. Use DNSSEC-validating resolvers, apply **least privilege** for DNS administration, and monitor key lifecycle events. *Private zones are not DNSSEC-signed.*", + "Url": "https://hub.prowler.com/check/dns_dnssec_disabled" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_key_sign_in_dnssec/dns_rsasha1_in_use_to_key_sign_in_dnssec.metadata.json b/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_key_sign_in_dnssec/dns_rsasha1_in_use_to_key_sign_in_dnssec.metadata.json index 85a739f425..64a75179d3 100644 --- a/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_key_sign_in_dnssec/dns_rsasha1_in_use_to_key_sign_in_dnssec.metadata.json +++ b/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_key_sign_in_dnssec/dns_rsasha1_in_use_to_key_sign_in_dnssec.metadata.json @@ -1,30 +1,38 @@ { "Provider": "gcp", "CheckID": "dns_rsasha1_in_use_to_key_sign_in_dnssec", - "CheckTitle": "Ensure That RSASHA1 Is Not Used for the Key-Signing Key in Cloud DNS DNSSEC", + "CheckTitle": "Cloud DNS managed zone DNSSEC key-signing key does not use RSASHA1", "CheckType": [], "ServiceName": "dns", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DNS_Zone", - "ResourceGroup": "network", - "Description": "NOTE: Currently, the SHA1 algorithm has been removed from general use by Google, and, if being used, needs to be whitelisted on a project basis by Google and will also, therefore, require a Google Cloud support contract. DNSSEC algorithm numbers in this registry may be used in CERT RRs. Zone signing (DNSSEC) and transaction security mechanisms (SIG(0) and TSIG) make use of particular subsets of these algorithms. The algorithm used for key signing should be a recommended one and it should be strong.", - "Risk": "SHA1 is considered weak and vulnerable to collision attacks.", + "ResourceType": "dns.googleapis.com/ManagedZone", + "Description": "**Cloud DNS zones** are assessed for DNSSEC **Key-Signing Key (KSK)** algorithms, specifically detecting use of `rsasha1`. Zones with KSKs on modern algorithms are distinguished from those still using `rsasha1`.", + "Risk": "Using `rsasha1` for KSK weakens DNSSEC. Collision-based forgeries can enable signed record spoofing, resulting in domain hijack, cache poisoning, and redirection-compromising **integrity** and **confidentiality**. Some validators reject SHA-1, causing resolution errors and reduced **availability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudDNS/dns-sec-key-signing-algorithm-in-use.html", + "https://cloud.google.com/dns/docs/dnssec-config", + "https://docs.cloud.google.com/dns/docs/dnssec-config", + "https://cloud.google.com/dns/docs/dnssec-advanced?hl=id", + "https://docs.cloud.google.com/dns/docs/dnssec-advanced" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud dns managed-zones update --dnssec-state on --ksk-algorithm RSASHA256 --ksk-key-length 2048 --zsk-algorithm RSASHA256 --zsk-key-length 1024", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudDNS/dns-sec-key-signing-algorithm-in-use.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_6#terraform" + "Other": "1. In Google Cloud Console, go to Networking > Cloud DNS and open the affected managed zone\n2. Click Edit\n3. If DNSSEC is enabled, set DNSSEC to Off and Save; then click Edit again\n4. Set DNSSEC to On, expand Advanced options\n5. Set Key-signing key (KSK) algorithm to RSASHA256 (not RSASHA1); set Zone-signing key (ZSK) algorithm to RSASHA256\n6. Click Save", + "Terraform": "```hcl\nresource \"google_dns_managed_zone\" \"\" {\n name = \"\"\n dns_name = \"example.com.\"\n\n dnssec_config {\n state = \"on\"\n\n default_key_specs {\n key_type = \"keySigning\"\n algorithm = \"rsasha256\" # FIX: use a non-RSASHA1 KSK algorithm to pass the check\n key_length = 2048\n }\n\n default_key_specs {\n key_type = \"zoneSigning\"\n algorithm = \"rsasha256\"\n key_length = 1024\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that Domain Name System Security Extensions (DNSSEC) feature is not using the deprecated RSASHA1 algorithm for the Key-Signing Key (KSK) associated with your DNS managed zone file. The algorithm used for DNSSEC signing should be a strong one, such as ECDSAP256SHA256 algorithm, as this is secure and widely deployed, and therefore it is a good choice for both DNSSEC validation and signing.", - "Url": "https://cloud.google.com/dns/docs/dnssec-config" + "Text": "Adopt **strong, supported DNSSEC algorithms** for KSKs (e.g., `ECDSAP256SHA256` or `RSASHA256`) and retire `rsasha1`. Rotate keys and validate changes before deployment. Keep KSK and ZSK algorithms consistent, document key-rotation policy, and enforce **least privilege** for DNS/DNSSEC administration.", + "Url": "https://hub.prowler.com/check/dns_rsasha1_in_use_to_key_sign_in_dnssec" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_zone_sign_in_dnssec/dns_rsasha1_in_use_to_zone_sign_in_dnssec.metadata.json b/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_zone_sign_in_dnssec/dns_rsasha1_in_use_to_zone_sign_in_dnssec.metadata.json index 7b8f0cb59d..356869799d 100644 --- a/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_zone_sign_in_dnssec/dns_rsasha1_in_use_to_zone_sign_in_dnssec.metadata.json +++ b/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_zone_sign_in_dnssec/dns_rsasha1_in_use_to_zone_sign_in_dnssec.metadata.json @@ -1,30 +1,39 @@ { "Provider": "gcp", "CheckID": "dns_rsasha1_in_use_to_zone_sign_in_dnssec", - "CheckTitle": "Ensure That RSASHA1 Is Not Used for the Zone-Signing Key in Cloud DNS DNSSEC", + "CheckTitle": "Cloud DNS managed zone does not use the RSASHA1 algorithm for the DNSSEC zone-signing key", "CheckType": [], "ServiceName": "dns", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DNS_Zone", - "ResourceGroup": "network", - "Description": "NOTE: Currently, the SHA1 algorithm has been removed from general use by Google, and, if being used, needs to be whitelisted on a project basis by Google and will also, therefore, require a Google Cloud support contract. DNSSEC algorithm numbers in this registry may be used in CERT RRs. Zone signing (DNSSEC) and transaction security mechanisms (SIG(0) and TSIG) make use of particular subsets of these algorithms. The algorithm used for key signing should be a recommended one and it should be strong.", - "Risk": "SHA1 is considered weak and vulnerable to collision attacks.", + "ResourceType": "dns.googleapis.com/ManagedZone", + "Description": "**Cloud DNS** DNSSEC settings are inspected for the **zone-signing key algorithm**. Zones that use `rsasha1` for zone signing are identified.", + "Risk": "Using **RSASHA1 for DNSSEC zone signing** weakens record integrity due to known **collision attacks**. Some validating resolvers no longer accept `SHA-1`, causing **resolution failures**. Adversaries may forge `RRSIGs`, enabling **DNS hijacking** or cache poisoning and redirecting traffic.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudDNS/dns-sec-zone-signing-algorithm-in-use.html", + "https://cloud.google.com/dns/docs/dnssec-config", + "https://cloud-kb.sentinelone.com/dns-security-rsa-sha1-enabled", + "https://datatracker.ietf.org/doc/html/rfc9905", + "https://stackoverflow.com/questions/68968312/terraform-errors-deploying-google-dns-managed-zone-with-rsasha1", + "https://docs.datadoghq.com/security/default_rules/def-000-jud/" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudDNS/dns-sec-zone-signing-algorithm-in-use.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_6#terraform" + "Other": "1. In Google Cloud Console, go to Networking > Network services > Cloud DNS\n2. Click the managed zone name, then click Edit\n3. If DNSSEC is On and the Zone signing algorithm is RSASHA1: set DNSSEC to Off and Save\n4. Click Edit again, set DNSSEC to On\n5. Open Advanced settings, set Zone signing algorithm to ECDSAP256SHA256 (or RSASHA256)\n6. Click Save", + "Terraform": "```hcl\nresource \"google_dns_managed_zone\" \"\" {\n name = \"\"\n dns_name = \"example.com.\"\n\n dnssec_config {\n state = \"on\"\n\n default_key_specs {\n algorithm = \"ecdsap256sha256\" # FIX: use a secure algorithm for zone signing\n key_type = \"zoneSigning\" # Ensures the zone-signing key is not RSASHA1\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that Domain Name System Security Extensions (DNSSEC) feature is not using the deprecated RSASHA1 algorithm for the Zone-Signing Key (ZSK) associated with your public DNS managed zone. The algorithm used for DNSSEC signing should be a strong one, such as RSASHA256, as this algorithm is secure and widely deployed, and therefore it is a good candidate for both DNSSEC validation and signing.", - "Url": "https://cloud.google.com/dns/docs/dnssec-config" + "Text": "Use **strong DNSSEC algorithms** for zone signing (e.g., `rsasha256`, `ecdsa-p256-sha256`, `ed25519`) and avoid `rsasha1`. Practice **crypto agility**: standardize secure defaults, rotate keys, and periodically validate signatures with modern resolvers. Apply **defense in depth** by monitoring DNSSEC health and limiting who can change settings.", + "Url": "https://hub.prowler.com/check/dns_rsasha1_in_use_to_zone_sign_in_dnssec" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/gcr/gcr_container_scanning_enabled/gcr_container_scanning_enabled.metadata.json b/prowler/providers/gcp/services/gcr/gcr_container_scanning_enabled/gcr_container_scanning_enabled.metadata.json index 52a71e20bc..4d05047a1c 100644 --- a/prowler/providers/gcp/services/gcr/gcr_container_scanning_enabled/gcr_container_scanning_enabled.metadata.json +++ b/prowler/providers/gcp/services/gcr/gcr_container_scanning_enabled/gcr_container_scanning_enabled.metadata.json @@ -1,33 +1,38 @@ { "Provider": "gcp", "CheckID": "gcr_container_scanning_enabled", - "CheckTitle": "Ensure Image Vulnerability Scanning using GCR Container Scanning or a third-party provider", - "CheckType": [ - "Security", - "Configuration" - ], + "CheckTitle": "Project has GCR Container Scanning API enabled", + "CheckType": [], "ServiceName": "gcr", - "SubServiceName": "Container Scanning", + "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Service", - "ResourceGroup": "container", - "Description": "Scan images stored in Google Container Registry (GCR) for vulnerabilities using GCR Container Scanning or a third-party provider. This helps identify and mitigate security risks associated with known vulnerabilities in container images.", - "Risk": "Without image vulnerability scanning, container images stored in GCR may contain known vulnerabilities, increasing the risk of exploitation by malicious actors.", - "RelatedUrl": "https://cloud.google.com/container-registry/docs/container-analysis", + "ResourceType": "serviceusage.googleapis.com/Service", + "Description": "**Google Cloud projects** with `containerscanning.googleapis.com` enabled perform **automatic vulnerability scanning** for images in Container Registry and Artifact Registry.\n\nThe finding indicates whether that service is active to generate and refresh vulnerability metadata for your container images.", + "Risk": "Without **image scanning**, vulnerable packages can reach production unchecked, enabling:\n- **Remote code execution** or **privilege escalation** (integrity/availability)\n- **Data exfiltration** from compromised workloads (confidentiality)\n- **Supply chain compromise** via unvetted base images", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ArtifactRegistry/enable-vulnerability-analysis.html", + "https://cloud.google.com/container-registry/docs/container-analysis", + "https://docs.cloud.google.com/artifact-analysis/docs/enable-automatic-scanning", + "https://cloud.google.com/container-registry/docs/container-best-practices" + ], "Remediation": { "Code": { "CLI": "gcloud services enable containerscanning.googleapis.com", "NativeIaC": "", - "Other": "", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/ensure-gcp-gcr-container-vulnerability-scanning-is-enabled#terraform" + "Other": "1. In the Google Cloud console, go to APIs & Services > Library\n2. Search for \"Container Scanning API\"\n3. Click the result and then click \"Enable\"", + "Terraform": "```hcl\nresource \"google_project_service\" \"\" {\n project = \"\"\n service = \"containerscanning.googleapis.com\" # Critical: enables Container Scanning API to pass the check\n}\n```" }, "Recommendation": { - "Text": "Enable vulnerability scanning for images stored in GCR using GCR Container Scanning or a third-party provider.", - "Url": "https://cloud.google.com/container-registry/docs/container-best-practices" + "Text": "Enable `containerscanning.googleapis.com` and integrate results into CI/CD gates. Apply **defense in depth**:\n- Use **Binary Authorization** to block noncompliant images\n- Enforce **least privilege** over who can disable scanning\n- Rebuild and patch frequently; prefer trusted, signed base images", + "Url": "https://hub.prowler.com/check/gcr_container_scanning_enabled" } }, - "Categories": [], + "Categories": [ + "vulnerabilities", + "container-security" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, GCR Container Scanning is disabled." diff --git a/prowler/providers/gcp/services/gke/gke_cluster_no_default_service_account/gke_cluster_no_default_service_account.metadata.json b/prowler/providers/gcp/services/gke/gke_cluster_no_default_service_account/gke_cluster_no_default_service_account.metadata.json index 7dafb56df8..9afae126c1 100644 --- a/prowler/providers/gcp/services/gke/gke_cluster_no_default_service_account/gke_cluster_no_default_service_account.metadata.json +++ b/prowler/providers/gcp/services/gke/gke_cluster_no_default_service_account/gke_cluster_no_default_service_account.metadata.json @@ -1,33 +1,34 @@ { "Provider": "gcp", "CheckID": "gke_cluster_no_default_service_account", - "CheckTitle": "Ensure GKE clusters are not running using the Compute Engine default service account", - "CheckType": [ - "Security", - "Configuration" - ], + "CheckTitle": "GKE cluster does not use the Compute Engine default service account", + "CheckType": [], "ServiceName": "gke", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Service", - "ResourceGroup": "container", - "Description": "Ensure GKE clusters are not running using the Compute Engine default service account. Create and use minimally privileged service accounts for GKE cluster nodes instead of using the Compute Engine default service account to minimize unnecessary permissions.", - "Risk": "Using the Compute Engine default service account for GKE cluster nodes may grant excessive permissions, increasing the risk of unauthorized access or compromise if a node is compromised.", - "RelatedUrl": "https://cloud.google.com/compute/docs/access/service-accounts#default_service_account", + "ResourceType": "container.googleapis.com/Cluster", + "Description": "**GKE clusters** are evaluated for use of the **Compute Engine default service account** (`default`) as the node identity. The expectation is that clusters and node pools run with dedicated, minimally privileged IAM service accounts instead of the project-wide default.", + "Risk": "**Default node service accounts** often have broad project access. If a node is compromised, its credentials can read secrets, modify resources, or delete infrastructure, enabling lateral movement and data exfiltration. This harms **confidentiality**, **integrity**, and **availability** across the environment.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/GKE/ensure-service-account-is-not-the-default-compute-engine-service-account.html" + ], "Remediation": { "Code": { - "CLI": "gcloud container node-pools create [NODE_POOL] --service-account=[SA_NAME]@[PROJECT_ID].iam.gserviceaccount.com --cluster=[CLUSTER_NAME] --zone [COMPUTE_ZONE]", + "CLI": "gcloud container node-pools create --cluster= --location --service-account=@.iam.gserviceaccount.com", "NativeIaC": "", - "Other": "", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-kubernetes-policies/ensure-gke-clusters-are-not-running-using-the-compute-engine-default-service-account#terraform" + "Other": "1. In Google Cloud Console, go to Kubernetes Engine > Clusters and open your cluster\n2. Click Add node pool\n3. In Security > Service account, select your non-default service account and click Create\n4. In Nodes > Node Pools, delete the node pool(s) that show Service account = default", + "Terraform": "```hcl\nresource \"google_container_node_pool\" \"\" {\n name = \"\"\n cluster = \"\"\n location = \"\"\n\n node_config {\n service_account = \"@.iam.gserviceaccount.com\" # critical: use a custom SA, not the Compute Engine default\n }\n}\n```" }, "Recommendation": { - "Text": "Create and use minimally privileged service accounts for GKE cluster nodes instead of using the Compute Engine default service account.", - "Url": "https://cloud.google.com/compute/docs/access/service-accounts#default_service_account" + "Text": "Assign a **custom, least-privileged IAM service account** to each node pool instead of `default`.\n- Grant only permissions required for node logging/monitoring and operations\n- Enforce **separation of duties** and restrict impersonation\n- Periodically review roles and audit usage for **defense in depth**", + "Url": "https://hub.prowler.com/check/gke_cluster_no_default_service_account" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, nodes use the Compute Engine default service account when you create a new cluster." diff --git a/prowler/providers/gcp/services/iam/iam_account_access_approval_enabled/iam_account_access_approval_enabled.metadata.json b/prowler/providers/gcp/services/iam/iam_account_access_approval_enabled/iam_account_access_approval_enabled.metadata.json index 61de3144f1..2f9136fe18 100644 --- a/prowler/providers/gcp/services/iam/iam_account_access_approval_enabled/iam_account_access_approval_enabled.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_account_access_approval_enabled/iam_account_access_approval_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "iam_account_access_approval_enabled", - "CheckTitle": "Ensure Access Approval is Enabled in your account", + "CheckTitle": "Project has Access Approval enabled", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Account", - "ResourceGroup": "governance", - "Description": "Ensure that Access Approval is enabled within your Google Cloud Platform (GCP) account in order to allow you to require your explicit approval whenever Google personnel need to access your GCP projects. Once the Access Approval feature is enabled, you can delegate users within your organization who can approve the access requests by giving them a security role in Identity and Access Management (IAM). These requests show the requester name/ID in an email or Pub/Sub message that you can choose to approve. This creates a new control and logging layer that reveals who in your organization approved/denied access requests to your projects.", - "Risk": "Controlling access to your Google Cloud data is crucial when working with business-critical and sensitive data. With Access Approval, you can be certain that your cloud information is accessed by approved Google personnel only. The Access Approval feature ensures that a cryptographically-signed approval is available for Google Cloud support and engineering teams when they need to access your cloud data (certain exceptions apply). By default, Access Approval and its dependency of Access Transparency are not enabled.", + "ResourceType": "accessapproval.googleapis.com/AccessApprovalSettings", + "Description": "**GCP project** has **Access Approval** configured at the project level, requiring explicit customer authorization before Google personnel can access project data. The evaluation looks for Access Approval settings associated with the project.", + "Risk": "Without Access Approval, Google support or engineering may access Customer Data without prior consent, weakening **confidentiality** and **accountability**. Reduced visibility hinders incident response and raises exposure for sensitive or regulated workloads.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/enable-access-approval.html", + "https://cloud.google.com/cloud-provider-access-management/access-approval/docs" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud access-approval settings update --project= --enrolled-services=all", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/enable-access-approval.html", - "Terraform": "" + "Other": "1. In the Google Cloud Console, go to Security > Access Approval (or search \"Access Approval\")\n2. Select the project \n3. Click Enable (or Edit settings if already open)\n4. Set Enrolled services to All Google Cloud services\n5. Click Save (enable the API if prompted)", + "Terraform": "```hcl\nresource \"google_access_approval_settings\" \"\" {\n project = \"\"\n\n enrolled_services {\n cloud_product = \"all\" # Critical: enroll all services to enable Access Approval for the project\n enrollment_level = \"BLOCK_ALL\" # Critical: require approval for all applicable access requests\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that Access Approval is enabled within your Google Cloud Platform (GCP) account in order to allow you to require your explicit approval whenever Google personnel need to access your GCP projects. Once the Access Approval feature is enabled, you can delegate users within your organization who can approve the access requests by giving them a security role in Identity and Access Management (IAM). These requests show the requester name/ID in an email or Pub/Sub message that you can choose to approve. This creates a new control and logging layer that reveals who in your organization approved/denied access requests to your projects.", - "Url": "https://cloud.google.com/cloud-provider-access-management/access-approval/docs" + "Text": "Enable **Access Approval** for projects and *where feasible* at higher hierarchy for consistency. Assign **least-privilege approvers** with **separation of duties**, integrate timely notifications, and monitor **Access Transparency** records to maintain **defense in depth**.", + "Url": "https://hub.prowler.com/check/iam_account_access_approval_enabled" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled.metadata.json b/prowler/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled.metadata.json index 25eb5242ec..70fd5d619c 100644 --- a/prowler/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled.metadata.json @@ -1,30 +1,37 @@ { "Provider": "gcp", "CheckID": "iam_audit_logs_enabled", - "CheckTitle": "Configure Google Cloud Audit Logs to Track All Activities", + "CheckTitle": "GCP project has Cloud Audit Logs enabled", "CheckType": [], "ServiceName": "iam", - "SubServiceName": "Audit Logs", + "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "GCPProject", - "ResourceGroup": "governance", - "Description": "Ensure that Google Cloud Audit Logs feature is configured to track Data Access logs for all Google Cloud Platform (GCP) services and users, in order to enhance overall access security and meet compliance requirements. Once configured, the feature can record all admin related activities, as well as all the read and write access requests to user data.", - "Risk": "In order to maintain an effective Google Cloud audit configuration for your project, folder, and organization, all 3 types of Data Access logs (ADMIN_READ, DATA_READ and DATA_WRITE) must be enabled for all supported GCP services. Also, Data Access logs should be captured for all IAM users, without exempting any of them. Exemptions let you control which users generate audit logs. When you add an exempted user to your log configuration, audit logs are not created for that user, for the selected log type(s). Data Access audit logs are disabled by default and must be explicitly enabled based on your business requirements.", + "ResourceType": "cloudresourcemanager.googleapis.com/Project", + "Description": "**GCP project** has **Cloud Audit Logs** configured to capture administrative operations and data access events for services and principals (*per IAM Audit Logs*, including `ADMIN_READ`, `DATA_READ`, `DATA_WRITE`).", + "Risk": "Absent or partial audit logging reduces visibility into who accessed data or changed configurations, hindering detection and forensics.\n\nMisused identities can alter IAM to persist access, exfiltrate data, or delete resources, impacting **confidentiality**, **integrity**, and **availability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/record-all-activities.html", + "https://cloud.google.com/logging/docs/audit/", + "https://docs.cloud.google.com/logging/docs/audit/configure-data-access" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/record-all-activities.html", - "Terraform": "https://docs.prowler.com/checks/gcp/logging-policies-1/ensure-that-cloud-audit-logging-is-configured-properly-across-all-services-and-all-users-from-a-project#terraform" + "Other": "1. In the Google Cloud console, go to IAM & Admin > Audit Logs\n2. Click Set default configuration\n3. Under Permission types, check Admin Read, Data Read, and Data Write\n4. Click Save", + "Terraform": "```hcl\n# Enable Cloud Audit Logs (Data Access) for all services\nresource \"google_project_iam_audit_config\" \"all\" {\n project = \"\"\n service = \"allServices\" # Critical: apply to all services\n\n # Critical: enable Data Access audit log types to pass the check\n audit_log_config { log_type = \"ADMIN_READ\" } # metadata/config reads\n audit_log_config { log_type = \"DATA_READ\" } # data reads\n audit_log_config { log_type = \"DATA_WRITE\" } # data writes\n}\n```" }, "Recommendation": { - "Text": "It is recommended that Cloud Audit Logging is configured to track all admin activities and read, write access to user data.", - "Url": "https://cloud.google.com/logging/docs/audit/" + "Text": "Enable comprehensive **Cloud Audit Logs** for all services and principals, including `ADMIN_READ`, `DATA_READ`, `DATA_WRITE`. *Avoid exemptions.* Set org/folder defaults, centralize and retain logs, enforce least privilege on log access, protect logs from alteration, and alert on anomalous access.", + "Url": "https://hub.prowler.com/check/iam_audit_logs_enabled" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_cloud_asset_inventory_enabled/iam_cloud_asset_inventory_enabled.metadata.json b/prowler/providers/gcp/services/iam/iam_cloud_asset_inventory_enabled/iam_cloud_asset_inventory_enabled.metadata.json index 7ca70567d5..e2a47eefc9 100644 --- a/prowler/providers/gcp/services/iam/iam_cloud_asset_inventory_enabled/iam_cloud_asset_inventory_enabled.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_cloud_asset_inventory_enabled/iam_cloud_asset_inventory_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "iam_cloud_asset_inventory_enabled", - "CheckTitle": "Ensure Cloud Asset Inventory Is Enabled", + "CheckTitle": "Project has Cloud Asset Inventory API enabled", "CheckType": [], "ServiceName": "iam", - "SubServiceName": "Asset Inventory", + "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Service", - "ResourceGroup": "governance", - "Description": "GCP Cloud Asset Inventory is services that provides a historical view of GCP resources and IAM policies through a time-series database. The information recorded includes metadata on Google Cloud resources, metadata on policies set on Google Cloud projects or resources, and runtime information gathered within a Google Cloud resource.", - "Risk": "Gaining insight into Google Cloud resources and policies is vital for tasks such as DevOps, security analytics, multi-cluster and fleet management, auditing, and governance. With Cloud Asset Inventory you can discover, monitor, and analyze all GCP assets in one place, achieving a better understanding of all your cloud assets across projects and services.", + "ResourceType": "serviceusage.googleapis.com/Service", + "Description": "**Project service usage** includes the **Cloud Asset Inventory** API (`cloudasset.googleapis.com`), enabling resource and IAM policy inventory with time-series metadata and change history.", + "Risk": "Without **Cloud Asset Inventory**, gaps in asset and IAM visibility hinder detection of drift and unauthorized changes, weakening access control integrity and risking data confidentiality. Shadow assets and silent privilege escalation can persist, delaying incident response.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudAPI/enabled-cloud-asset-inventory.html", + "https://cloud.google.com/asset-inventory/docs" + ], "Remediation": { "Code": { - "CLI": "gcloud services enable cloudasset.googleapis.com", + "CLI": "gcloud services enable cloudasset.googleapis.com --project ", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudAPI/enabled-cloud-asset-inventory.html", - "Terraform": "" + "Other": "1. In the Google Cloud Console, select the project from the project picker.\n2. Go to APIs & Services > Library.\n3. Search for \"Cloud Asset Inventory API\" and select it.\n4. Click Enable.\n5. Verify it appears under APIs & Services > Enabled APIs & services.", + "Terraform": "```hcl\nresource \"google_project_service\" \"\" {\n project = \"\"\n service = \"cloudasset.googleapis.com\" # Enables Cloud Asset Inventory API to pass the check\n}\n```" }, "Recommendation": { - "Text": "Ensure that Cloud Asset Inventory is enabled for all your GCP projects in order to efficiently manage the history and the inventory of your cloud resources. Google Cloud Asset Inventory is a fully managed metadata inventory service that allows you to view, monitor, analyze, and gain insights for your Google Cloud and Anthos assets. Cloud Asset Inventory is disabled by default in each GCP project.", - "Url": "https://cloud.google.com/asset-inventory/docs" + "Text": "Enable **Cloud Asset Inventory** across all projects *and, if applicable, at the organization level* to maintain authoritative asset and IAM histories. Centralize analysis, retain records per policy, and use the data to enforce **least privilege** and **defense in depth**.", + "Url": "https://hub.prowler.com/check/iam_cloud_asset_inventory_enabled" } }, - "Categories": [], + "Categories": [ + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.metadata.json b/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.metadata.json index a9bbfb1b9f..9e6ba83ff9 100644 --- a/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "iam_no_service_roles_at_project_level", - "CheckTitle": "Ensure That IAM Users Are Not Assigned the Service Account User or Service Account Token Creator Roles at Project Level", + "CheckTitle": "Project has no IAM users assigned the Service Account User or Service Account Token Creator roles at project level", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "IAM Policy", - "ResourceGroup": "IAM", - "Description": "It is recommended to assign the `Service Account User (iam.serviceAccountUser)` and `Service Account Token Creator (iam.serviceAccountTokenCreator)` roles to a user for a specific service account rather than assigning the role to a user at project level.", - "Risk": "The Service Account User (iam.serviceAccountUser) role allows an IAM user to attach a service account to a long-running job service such as an App Engine App or Dataflow Job, whereas the Service Account Token Creator (iam.serviceAccountTokenCreator) role allows a user to directly impersonate the identity of a service account.", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/check-for-iam-users-with-service-roles.html", + "ResourceType": "cloudresourcemanager.googleapis.com/Project", + "Description": "**Google Cloud IAM policies** are inspected for **project-level grants** of `roles/iam.serviceAccountUser` and `roles/iam.serviceAccountTokenCreator` to principals. The focus is on bindings that enable attaching or impersonating service accounts at the project scope rather than on individual service accounts.", + "Risk": "**Project-wide impersonation rights** enable **privilege escalation** and **lateral movement**. Holders can act as any service account, access data across services, modify resources, and persist access. New service accounts inherit exposure, undermining confidentiality and integrity.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/check-for-iam-users-with-service-roles.html", + "https://cloud.google.com/iam/docs/granting-changing-revoking-access", + "https://cloud.google.com/iam/docs/best-practices-service-accounts?ref=alphasec.io" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_3", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_3#terraform" + "Other": "1. In Google Cloud Console, go to IAM & Admin > IAM\n2. Use the filter to find Role: Service Account User\n3. Remove all project-level bindings for this role and click Save\n4. Repeat steps 2-3 for Role: Service Account Token Creator\n5. Do not add these roles at the project level; if needed, grant them on specific service accounts only (IAM & Admin > Service Accounts > select account > Permissions > Grant access)", + "Terraform": "```hcl\n# Grant required access at the service account level instead of the project level\nresource \"google_service_account_iam_member\" \"\" {\n service_account_id = \"projects//serviceAccounts/@.iam.gserviceaccount.com\" # CRITICAL: scope grant to a specific service account, not the project\n role = \"roles/iam.serviceAccountUser\" # CRITICAL: this role is granted only at the service account level\n member = \"user:@example.com\"\n}\n```" }, "Recommendation": { - "Text": "Ensure that the Service Account User and Service Account Token Creator roles are assigned to a user for a specific GCP service account rather than to a user at the GCP project level, in order to implement the principle of least privilege (POLP). The principle of least privilege (also known as the principle of minimal privilege) is the practice of providing every user the minimal amount of access required to perform its tasks. Google Cloud Platform (GCP) IAM users should not have assigned the Service Account User or Service Account Token Creator roles at the GCP project level. Instead, these roles should be allocated to a user associated with a specific service account, providing that user access to the service account only.", - "Url": "https://cloud.google.com/iam/docs/granting-changing-revoking-access" + "Text": "Assign `roles/iam.serviceAccountUser` and `roles/iam.serviceAccountTokenCreator` only on the specific service account, not at project scope. Enforce **least privilege** and **separation of duties** with per-SA grants, conditional bindings, and time-bound access. Prefer **short-lived impersonation**; review grants regularly.", + "Url": "https://hub.prowler.com/check/iam_no_service_roles_at_project_level" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured.metadata.json b/prowler/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured.metadata.json index d52ddff8d8..7a8156131d 100644 --- a/prowler/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "iam_organization_essential_contacts_configured", - "CheckTitle": "Ensure Essential Contacts is Configured for Organization", + "CheckTitle": "Organization has Essential Contacts configured", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Organization", - "ResourceGroup": "governance", - "Description": "It is recommended that Essential Contacts is configured to designate email addresses for Google Cloud services to notify of important technical or security information.", - "Risk": "Google Cloud Platform (GCP) services, such as Cloud Billing, send out billing notifications to share important information with the cloud platform users. By default, these types of notifications are sent to members with certain Identity and Access Management (IAM) roles such as 'roles/owner' and 'roles/billing.admin'. With Essential Contacts, you can specify exactly who receives important notifications by providing your own list of contacts (i.e. email addresses).", + "ResourceType": "cloudresourcemanager.googleapis.com/Organization", + "Description": "Google Cloud organization has **Essential Contacts** defined at the organization level for categories such as `SECURITY`, `BILLING`, `LEGAL`, `SUSPENSION`, `TECHNICAL`, or `PRODUCT_UPDATES`.\n\nEvaluates whether at least one contact is configured.", + "Risk": "Missing **Essential Contacts** means security, abuse, and billing notices can go unnoticed or to inappropriate recipients, slowing response.\n\nConsequences: data exposure via unaddressed alerts (C), unauthorized changes persisting (I), and suspensions/outages from unresolved issues (A).", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/essential-contacts.html", + "https://docs.cloud.google.com/resource-manager/docs/manage-essential-contacts?hl=es", + "https://cloud.google.com/resource-manager/docs/managing-notification-contacts" + ], "Remediation": { "Code": { - "CLI": "gcloud essential-contacts create --email= --notification-categories= --organization=", + "CLI": "gcloud essential-contacts create --email= --notification-categories=all --organization=", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/essential-contacts.html", - "Terraform": "" + "Other": "1. In the Google Cloud console, go to Essential Contacts\n2. In the resource selector, choose your Organization\n3. Click Add contact\n4. Enter the contact email and select category All\n5. Click Save", + "Terraform": "```hcl\nresource \"google_essential_contacts_contact\" \"\" {\n parent = \"organizations/\" # Critical: set at org level to satisfy the check\n email = \"\" # Critical: creates the essential contact\n notification_category_subscriptions = [\"ALL\"] # Critical: required; ensures the contact is created\n}\n```" }, "Recommendation": { - "Text": "It is recommended that Essential Contacts is configured to designate email addresses for Google Cloud services to notify of important technical or security information.", - "Url": "https://cloud.google.com/resource-manager/docs/managing-notification-contacts" + "Text": "Configure **Essential Contacts** at the organization (and inherit to folders/projects) with group aliases for `SECURITY`, `BILLING`, `LEGAL`, `SUSPENSION`, `TECHNICAL`, and `PRODUCT_UPDATES`.\n\nApply **least privilege** and **separation of duties**. Review quarterly, verify delivery, and restrict contacts to approved domains.", + "Url": "https://hub.prowler.com/check/iam_organization_essential_contacts_configured" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties.metadata.json b/prowler/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties.metadata.json index 9836c05d33..cf9ae4ba2b 100644 --- a/prowler/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "iam_role_kms_enforce_separation_of_duties", - "CheckTitle": "Enforce Separation of Duties for KMS-Related Roles", + "CheckTitle": "Project members are not assigned both Cloud KMS Admin and CryptoKey Encrypter/Decrypter roles", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "IAMRole", - "ResourceGroup": "IAM", - "Description": "Ensure that separation of duties is enforced for all Cloud Key Management Service (KMS) related roles. The principle of separation of duties (also known as segregation of duties) has as its primary objective the prevention of fraud and human error. This objective is achieved by dismantling the tasks and the associated privileges for a specific business process among multiple users/identities. Google Cloud provides predefined roles that can be used to implement the principle of separation of duties, where it is needed. The predefined Cloud KMS Admin role is meant for users to manage KMS keys but not to use them. The Cloud KMS CryptoKey Encrypter/Decrypter roles are meant for services who can use keys to encrypt and decrypt data, but not to manage them. To adhere to cloud security best practices, your IAM users should not have the Admin role and any of the CryptoKey Encrypter/Decrypter roles assigned at the same time.", - "Risk": "The principle of separation of duties can be enforced in order to eliminate the need for the IAM user/identity that has all the permissions needed to perform unwanted actions, such as using a cryptographic key to access and decrypt data which the user should not normally have access to.", + "ResourceType": "cloudresourcemanager.googleapis.com/Project", + "Description": "Project IAM assignments are analyzed for **Cloud KMS** separation of duties: principals simultaneously granted `roles/cloudkms.admin` and any of `roles/cloudkms.cryptoKeyEncrypterDecrypter`, `roles/cloudkms.cryptoKeyEncrypter`, or `roles/cloudkms.cryptoKeyDecrypter`.", + "Risk": "Combining key management and key usage undermines **confidentiality**, **integrity**, and **availability**:\n- Unauthorized decryption of sensitive data\n- Tampering with policies or rotation to conceal access\n- Disabling or destroying keys, causing outages\n\nThis concentration of power reduces oversight and auditability.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/enforce-separation-of-duties-for-kms-related-roles.html", + "https://cloud.google.com/kms/docs/separation-of-duties" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud projects remove-iam-policy-binding --member= --role=roles/cloudkms.admin", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/enforce-separation-of-duties-for-kms-related-roles.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to IAM & Admin > IAM\n2. Locate the principal listed in the finding and click Edit principal\n3. Remove either \"Cloud KMS Admin\" or any of the \"Cloud KMS CryptoKey Encrypter/Decrypter\" roles from the project\n4. Click Save", + "Terraform": "```hcl\nresource \"google_project_iam_binding\" \"\" {\n project = \"\"\n role = \"roles/cloudkms.admin\" # Critical: ensure the offending principal is NOT bound as KMS Admin\n members = [\n \"user:\" # Critical: exclude any member who also has CryptoKey* roles to enforce separation of duties\n ]\n}\n```" }, "Recommendation": { - "Text": "It is recommended that the principle of 'Separation of Duties' is enforced while assigning KMS related roles to users.", - "Url": "https://cloud.google.com/kms/docs/separation-of-duties" + "Text": "Apply **least privilege** and **separation of duties**:\n- Never combine `roles/cloudkms.admin` with any `roles/cloudkms.cryptoKey*`\n- Isolate key management and usage in dedicated projects\n- Require approvals, log all key access, and monitor\n- Avoid broad `roles/owner` on key scopes", + "Url": "https://hub.prowler.com/check/iam_role_kms_enforce_separation_of_duties" } }, - "Categories": [], + "Categories": [ + "identity-access", + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties.metadata.json b/prowler/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties.metadata.json index ce5395abc8..8ad11f2a5f 100644 --- a/prowler/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "iam_role_sa_enforce_separation_of_duties", - "CheckTitle": "Enforce Separation of Duties for Service-Account Related Roles", + "CheckTitle": "Project enforces separation of duties for Service Account Admin and Service Account User roles", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "IAMRole", - "ResourceGroup": "IAM", - "Description": "Ensure that separation of duties (also known as segregation of duties - SoD) is enforced for all Google Cloud Platform (GCP) service-account related roles. The security principle of separation of duties has as its primary objective the prevention of fraud and human error. This objective is achieved by disbanding the tasks and associated privileges for a specific business process among multiple users/members. To follow security best practices, your GCP service accounts should not have the Service Account Admin and Service Account User roles assigned at the same time.", - "Risk": "The principle of separation of duties should be enforced in order to eliminate the need for high-privileged IAM members, as the permissions granted to these members can allow them to perform malicious or unwanted actions.", + "ResourceType": "cloudresourcemanager.googleapis.com/Project", + "Description": "Google Cloud IAM policies are evaluated to find principals granted both `roles/iam.serviceAccountAdmin` and `roles/iam.serviceAccountUser` within a project. **Service-account related roles** are expected to be segregated so that service account lifecycle management is distinct from their use or impersonation.", + "Risk": "With both roles, a principal can create or modify service accounts and then use or attach them to workloads, enabling unchecked impersonation. This endangers confidentiality (expanded data access), integrity (policy/workload changes), and availability (persistence or sabotage via privileged automation).", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/enforce-separation-of-duties-for-service-account-roles.html", + "https://docs.cloud.google.com/iam/docs/service-account-overview", + "https://cloud.google.com/iam/docs/understanding-roles" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/enforce-separation-of-duties-for-service-account-roles.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_10#terraform" + "Other": "1. In Google Cloud Console, go to IAM & Admin > IAM\n2. Click the View by Role tab\n3. Select the role Service Account Admin (roles/iam.serviceAccountAdmin)\n4. Remove all listed principals from this role and click Save\n5. Select the role Service Account User (roles/iam.serviceAccountUser)\n6. Remove all listed principals from this role and click Save", + "Terraform": "```hcl\n# Remove all project-level principals from Service Account User\nresource \"google_project_iam_binding\" \"sa_user_none\" {\n project = \"\"\n role = \"roles/iam.serviceAccountUser\" # critical: target role to clear at project level\n members = [] # critical: empty list removes the binding (no members)\n}\n\n# Remove all project-level principals from Service Account Admin\nresource \"google_project_iam_binding\" \"sa_admin_none\" {\n project = \"\"\n role = \"roles/iam.serviceAccountAdmin\" # critical: target role to clear at project level\n members = [] # critical: empty list removes the binding (no members)\n}\n```" }, "Recommendation": { - "Text": "Ensure that separation of duties (also known as segregation of duties - SoD) is enforced for all Google Cloud Platform (GCP) service-account related roles. The security principle of separation of duties has as its primary objective the prevention of fraud and human error. This objective is achieved by disbanding the tasks and associated privileges for a specific business process among multiple users/members. To follow security best practices, your GCP service accounts should not have the Service Account Admin and Service Account User roles assigned at the same time.", - "Url": "https://cloud.google.com/iam/docs/understanding-roles" + "Text": "Enforce separation of duties: assign `roles/iam.serviceAccountAdmin` for lifecycle tasks and `roles/iam.serviceAccountUser` for attach/impersonate, never both to one principal.\n- Apply **least privilege** with narrow scope and conditions\n- Use temporary elevation/approvals\n- Regularly audit IAM bindings and logs", + "Url": "https://hub.prowler.com/check/iam_role_sa_enforce_separation_of_duties" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_sa_no_administrative_privileges/iam_sa_no_administrative_privileges.metadata.json b/prowler/providers/gcp/services/iam/iam_sa_no_administrative_privileges/iam_sa_no_administrative_privileges.metadata.json index 467da6f96e..3de2ce9cd0 100644 --- a/prowler/providers/gcp/services/iam/iam_sa_no_administrative_privileges/iam_sa_no_administrative_privileges.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_sa_no_administrative_privileges/iam_sa_no_administrative_privileges.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "iam_sa_no_administrative_privileges", - "CheckTitle": "Ensure Service Account does not have admin privileges", + "CheckTitle": "Service account has no administrative privileges", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "ServiceAccount", - "ResourceGroup": "IAM", - "Description": "Ensure Service Account does not have admin privileges", - "Risk": "Enrolling ServiceAccount with Admin rights gives full access to an assigned application or a VM. A ServiceAccount Access holder can perform critical actions, such as delete and update change settings, without user intervention.", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/restrict-admin-access-for-service-accounts.html", + "ResourceType": "iam.googleapis.com/ServiceAccount", + "Description": "Google Cloud service accounts with **high-privilege IAM roles** are identified, including `roles/owner`, `roles/editor`, or any role containing `admin`. The evaluation looks for service accounts bound to these roles in IAM policies across the project hierarchy.", + "Risk": "Over-privileged service accounts jeopardize the CIA triad:\n- Confidentiality: data can be read and exfiltrated\n- Integrity: configs, IAM, and code can be altered\n- Availability: resources can be deleted or halted\n\nCompromise via key theft or impersonation enables lateral movement and persistence.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/restrict-admin-access-for-service-accounts.html", + "https://cloud.google.com/iam/docs/manage-access-service-accounts" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud projects remove-iam-policy-binding --member=serviceAccount: --role=", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_4", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_4#terraform" + "Other": "1. In the Google Cloud console, go to IAM & Admin > IAM\n2. Select the project (or switch to the folder/organization) where the role is granted\n3. Find the service account by email and click Edit principal\n4. Remove roles: Owner, Editor, and any role with \"Admin\" in the name\n5. Click Save\n6. Repeat at folder/organization level if the role was inherited", + "Terraform": "" }, "Recommendation": { - "Text": "Ensure that your Google Cloud user-managed service accounts are not using privileged (administrator) roles, in order to implement the principle of least privilege and prevent any accidental or intentional modifications that may lead to data leaks and/or data loss.", - "Url": "https://cloud.google.com/iam/docs/manage-access-service-accounts" + "Text": "Apply **least privilege**: replace `roles/owner`, `roles/editor`, and roles containing `admin` with narrowly scoped predefined or custom roles. Use **separation of duties**, **temporary elevation**, and **IAM Conditions** to limit scope and time. Prefer **impersonation** over long-lived keys and monitor SA usage.", + "Url": "https://hub.prowler.com/check/iam_sa_no_administrative_privileges" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_sa_no_user_managed_keys/iam_sa_no_user_managed_keys.metadata.json b/prowler/providers/gcp/services/iam/iam_sa_no_user_managed_keys/iam_sa_no_user_managed_keys.metadata.json index 009a58b5fe..601f797806 100644 --- a/prowler/providers/gcp/services/iam/iam_sa_no_user_managed_keys/iam_sa_no_user_managed_keys.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_sa_no_user_managed_keys/iam_sa_no_user_managed_keys.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "iam_sa_no_user_managed_keys", - "CheckTitle": "Ensure That There Are Only GCP-Managed Service Account Keys for Each Service Account", + "CheckTitle": "Service account has no user-managed keys", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "ServiceAccountKey", - "ResourceGroup": "IAM", - "Description": "Ensure That There Are Only GCP-Managed Service Account Keys for Each Service Account", - "Risk": "Anyone who has access to the keys will be able to access resources through the service account. GCP-managed keys are used by Cloud Platform services such as App Engine and Compute Engine. These keys cannot be downloaded. Google will keep the keys and automatically rotate them on an approximately weekly basis. User-managed keys are created, downloadable, and managed by users.", + "ResourceType": "iam.googleapis.com/ServiceAccount", + "Description": "**IAM service accounts** do not have keys of type `USER_MANAGED`; only Google-managed keys (or no keys) are present.", + "Risk": "**User-managed keys** are downloadable and long-lived, increasing theft and reuse risk. An attacker with a key can impersonate the service account, perform unauthorized API calls, exfiltrate data, and alter resources, impacting **confidentiality** and **integrity**, and potentially **availability**. Copies in repos or logs can evade centralized rotation and revocation.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/delete-user-managed-service-account-keys.html", + "https://cloud.google.com/iam/docs/creating-managing-service-account-keys" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud iam service-accounts keys delete --iam-account=", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-user-managed-service-account-keys.html", + "Other": "1. In the Google Cloud console, go to IAM & Admin > Service Accounts\n2. Select your project and click the affected service account\n3. Open the Keys tab\n4. For each key with Type \"User-managed\", click Delete and confirm\n5. Verify no User-managed keys remain for that service account\n6. Repeat for any other affected service accounts", "Terraform": "" }, "Recommendation": { - "Text": "It is recommended to prevent user-managed service account keys.", - "Url": "https://cloud.google.com/iam/docs/creating-managing-service-account-keys" + "Text": "Avoid **user-managed keys**. Use **service account impersonation** or **Workload Identity Federation** for short-lived credentials and **least privilege**. Enforce `iam.disableServiceAccountKeyCreation`, restrict who can create keys, and monitor usage. *If exceptions are unavoidable*, tightly scope, rotate aggressively, and store keys securely.", + "Url": "https://hub.prowler.com/check/iam_sa_no_user_managed_keys" } }, - "Categories": [], + "Categories": [ + "secrets", + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_rotate_90_days/iam_sa_user_managed_key_rotate_90_days.metadata.json b/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_rotate_90_days/iam_sa_user_managed_key_rotate_90_days.metadata.json index 15fd25b66a..f322d86ff7 100644 --- a/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_rotate_90_days/iam_sa_user_managed_key_rotate_90_days.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_rotate_90_days/iam_sa_user_managed_key_rotate_90_days.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "iam_sa_user_managed_key_rotate_90_days", - "CheckTitle": "Ensure User-Managed/External Keys for Service Accounts Are Rotated Every 90 Days", + "CheckTitle": "Service account user-managed key has been rotated within the last 90 days", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "ServiceAccountKey", - "ResourceGroup": "IAM", - "Description": "Ensure User-Managed/External Keys for Service Accounts Are Rotated Every 90 Days", - "Risk": "Service Account keys should be rotated to ensure that data cannot be accessed with an old key that might have been lost, cracked, or stolen.", + "ResourceType": "iam.googleapis.com/ServiceAccountKey", + "Description": "**GCP IAM service account user-managed keys** are evaluated by last rotation. Keys of type `USER_MANAGED` older than `90` days are identified; those rotated within `90` days align with the expected rotation cadence.", + "Risk": "**Stale service account keys** extend exposure of **long-lived credentials**. A leaked or retained key can grant persistent API access, enabling **data exfiltration**, tampering, and lateral movement. Weak rotation reduces revocation effectiveness and erodes **confidentiality** and **integrity**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/rotate-service-account-user-managed-keys.html", + "https://cloud.google.com/iam/docs/creating-managing-service-account-keys" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud iam service-accounts keys delete --iam-account=@.iam.gserviceaccount.com", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/rotate-service-account-user-managed-keys.html", + "Other": "1. In Google Cloud Console, go to IAM & Admin > Service Accounts\n2. Open the service account, then go to the Keys tab\n3. If the workload still needs a key: click Add key > Create new key (JSON), download it, and update the workload to use it\n4. Delete the user-managed key(s) older than 90 days by clicking Delete next to each\n5. Re-run the check to confirm only recent keys remain", "Terraform": "" }, "Recommendation": { - "Text": "It is recommended that all Service Account keys are regularly rotated.", - "Url": "https://cloud.google.com/iam/docs/creating-managing-service-account-keys" + "Text": "Rotate **user-managed keys** at least every `90` days and prefer **Workload Identity Federation** or other short-lived credentials over static keys.\n- Minimize key count; remove unused keys\n- Enforce **least privilege** on service accounts\n- Automate rotation and alert on aged keys\n- Set key expiry as **defense in depth**", + "Url": "https://hub.prowler.com/check/iam_sa_user_managed_key_rotate_90_days" } }, - "Categories": [], + "Categories": [ + "secrets" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.metadata.json b/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.metadata.json index 8d05b4bde5..6c659baf36 100644 --- a/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.metadata.json @@ -1,30 +1,37 @@ { "Provider": "gcp", "CheckID": "iam_sa_user_managed_key_unused", - "CheckTitle": "Ensure That There Are No Unused Service Account Keys for Each Service Account", + "CheckTitle": "User-managed service account key was used within the allowed inactivity period", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "ServiceAccountKey", - "ResourceGroup": "IAM", - "Description": "Ensure That There Are No Unused Service Account Keys for Each Service Account.", - "Risk": "Anyone who has access to the keys will be able to access resources through the service account. GCP-managed keys are used by Cloud Platform services such as App Engine and Compute Engine. These keys cannot be downloaded. Google will keep the keys and automatically rotate them on an approximately weekly basis. User-managed keys are created, downloadable, and managed by users.", - "RelatedUrl": "https://cloud.google.com/iam/docs/service-account-overview#identify-unused", + "ResourceType": "iam.googleapis.com/ServiceAccountKey", + "Description": "**User-managed service account keys** with no recorded activity during the last `max_unused_account_days` are identified using key-usage metrics per service account.", + "Risk": "**Stale user-managed keys** expand exposure of long-lived credentials. If leaked, an attacker can authenticate as the service account off-platform, bypass network controls, access data, alter resources, and persist-compromising confidentiality and integrity, and risking availability via destructive changes.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/delete-user-managed-service-account-keys.html", + "https://cloud.google.com/iam/docs/creating-managing-service-account-keys", + "https://docs.cloud.google.com/iam/docs/samples/iam-delete-key", + "https://cloud.google.com/iam/docs/service-account-overview#identify-unused" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud iam service-accounts keys delete --iam-account=", "NativeIaC": "", - "Other": "", + "Other": "1. In Google Cloud Console, go to IAM & Admin > Service Accounts\n2. Select your project and click the service account with the unused user-managed key\n3. Open the Keys tab\n4. Find the unused key (Type: User-managed), click Delete, and confirm", "Terraform": "" }, "Recommendation": { - "Text": "It is recommended to prevent user-managed service account keys.", - "Url": "https://cloud.google.com/iam/docs/creating-managing-service-account-keys" + "Text": "Prefer **managed workload identities** and **service account impersonation** over user-managed keys. Enforce `iam.disableServiceAccountKeyCreation`, remove unused keys, and use short key lifetimes with rotation when unavoidable. Apply **least privilege**, monitor key usage, and enforce **separation of duties** to limit blast radius.", + "Url": "https://hub.prowler.com/check/iam_sa_user_managed_key_unused" } }, - "Categories": [], + "Categories": [ + "secrets" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.metadata.json b/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.metadata.json index 1d7c4aabd2..03ccf30274 100644 --- a/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "iam_service_account_unused", - "CheckTitle": "Ensure That There Are No Unused Service Accounts", + "CheckTitle": "Service account was used within the configured maximum unused period", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "ServiceAccount", - "ResourceGroup": "IAM", - "Description": "Ensure That There Are No Unused Service Accounts.", - "Risk": "A malicious actor could make use of privilege escalation or impersonation to access an unused Service Account that is over-privileged.", - "RelatedUrl": "https://cloud.google.com/iam/docs/service-account-overview#identify-unused", + "ResourceType": "iam.googleapis.com/ServiceAccount", + "Description": "Google Cloud service accounts are evaluated for **recent usage** within a configurable window (default `180` days) using usage telemetry.\n\nIt highlights which accounts show activity versus those with **no observed use** in that period.", + "Risk": "Dormant but permissioned service accounts threaten **confidentiality** and **integrity** via:\n- **Impersonation/privilege escalation** through stale roles or leaked keys\n- **Lateral movement** and persistent access\nThey also weaken **accountability**, obscuring audit trails when reactivated unnoticed.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/iam/docs/best-practices-service-accounts?ref=alphasec.io", + "https://cloud.google.com/iam/docs/service-account-overview#identify-unused" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud auth print-access-token --impersonate-service-account=", "NativeIaC": "", - "Other": "", + "Other": "1. In the Google Cloud console, open the IAM Service Account Credentials API reference for \"GenerateAccessToken\" and click \"Try this method\" (APIs Explorer)\n2. Set name to: projects/-/serviceAccounts/\n3. Add scope: https://www.googleapis.com/auth/cloud-platform\n4. Click Execute (use an identity with roles/iam.serviceAccountTokenCreator on the service account)\n5. The generated token records recent usage for the service account, changing the finding to PASS", "Terraform": "" }, "Recommendation": { - "Text": "It is recommended to disable or remove unused Service Accounts.", - "Url": "https://cloud.google.com/iam/docs/service-account-overview#identify-unused" + "Text": "Apply **least privilege** and **reduce attack surface**:\n- Verify inactivity, then *disable* and later delete unused accounts\n- Revoke role bindings and keys; favor short-lived impersonation over keys\n- Avoid powerful defaults; enforce separation of duties\n- Continuously monitor usage and alert on dormancy", + "Url": "https://hub.prowler.com/check/iam_service_account_unused" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json b/prowler/providers/gcp/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json index 03adbd16ef..d67cb1e8c5 100644 --- a/prowler/providers/gcp/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json +++ b/prowler/providers/gcp/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json @@ -1,31 +1,35 @@ { "Provider": "gcp", "CheckID": "kms_key_not_publicly_accessible", - "CheckTitle": "Check for Publicly Accessible Cloud KMS Keys", + "CheckTitle": "Cloud KMS key has no public IAM access", "CheckType": [], "ServiceName": "kms", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "CryptoKey", - "ResourceGroup": "security", - "Description": "Check for Publicly Accessible Cloud KMS Keys", - "Risk": "Ensure that the IAM policy associated with your Cloud Key Management Service (KMS) keys is restricting anonymous and/or public access", + "ResourceType": "cloudkms.googleapis.com/CryptoKey", + "Description": "**Cloud KMS crypto keys** are evaluated for **public principals** in their IAM bindings, specifically `allUsers` and `allAuthenticatedUsers`.\n\nThe finding reflects whether these memberships are present on a key.", + "Risk": "Granting **public principals** access lets anyone on the Internet or any Google account use permissions on the key.\n- **Confidentiality** loss via unauthorized `decrypt`\n- **Integrity** compromise via illicit `sign`\n- **Availability** impact from disable, rotation, or destruction", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudKMS/publicly-accessible-kms-cryptokeys.html", + "https://cloud.google.com/kms/docs/iam" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudKMS/publicly-accessible-kms-cryptokeys.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/ensure-that-cloud-kms-cryptokeys-are-not-anonymously-or-publicly-accessible#terraform" + "Other": "1. In Google Cloud Console, go to Security > Key Management > Key rings\n2. Open the key ring, then select the affected key\n3. Click the Permissions tab\n4. Remove principals \"allUsers\" and \"allAuthenticatedUsers\" from all roles\n5. Click Save", + "Terraform": "```hcl\n# Replace the IAM policy on the KMS key to remove any public members\nresource \"google_kms_crypto_key_iam_policy\" \"\" {\n crypto_key_id = \"\"\n \n policy_data = jsonencode({\n bindings = [] # Critical: empty bindings remove all IAM principals, eliminating allUsers/allAuthenticatedUsers\n })\n}\n```" }, "Recommendation": { - "Text": "To deny access from anonymous and public users, remove the bindings for 'allUsers' and 'allAuthenticatedUsers' members from the KMS key's IAM policy.", - "Url": "https://cloud.google.com/kms/docs/iam" + "Text": "Remove `allUsers` and `allAuthenticatedUsers` from key IAM. Grant access only to specific groups or service accounts with **least privilege** at the key scope. Enforce **separation of duties** between admins and users, and regularly review inherited bindings and audit logs.", + "Url": "https://hub.prowler.com/check/kms_key_not_publicly_accessible" } }, "Categories": [ - "internet-exposed" + "internet-exposed", + "identity-access" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled.metadata.json b/prowler/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled.metadata.json index b497709ad2..5efe894b04 100644 --- a/prowler/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled.metadata.json +++ b/prowler/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "kms_key_rotation_enabled", - "CheckTitle": "Ensure KMS keys are rotated within a period of 90 days", + "CheckTitle": "KMS key is rotated at least annually", "CheckType": [], "ServiceName": "kms", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "CryptoKey", - "ResourceGroup": "security", - "Description": "Ensure KMS keys are rotated within a period of 90 days", - "Risk": "Ensure that all your Cloud Key Management Service (KMS) keys are rotated within a period of 90 days in order to meet security and compliance requirements", + "ResourceType": "cloudkms.googleapis.com/CryptoKey", + "Description": "Google Cloud KMS customer-managed keys have **automatic rotation** enabled or a rotation interval `365` days.\n\nThe evaluation reviews each key's rotation settings to confirm periodic creation of new key versions.", + "Risk": "Without timely rotation, a stolen key can decrypt an expanding volume of data, eroding **confidentiality**. Prolonged key lifetimes widen windows for misuse, impact **integrity** of protected workloads, and make emergency rollover harder, risking **availability** disruptions.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudKMS/rotate-kms-encryption-keys.html", + "https://cloud.google.com/iam/docs/manage-access-service-accounts" + ], "Remediation": { "Code": { - "CLI": "gcloud kms keys update new --keyring= --location= --nextrotation-time= --rotation-period=", + "CLI": "gcloud kms keys update --keyring= --location= --rotation-period=365d --next-rotation-time=", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudKMS/rotate-kms-encryption-keys.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/bc_gcp_general_4#terraform" + "Other": "1. In Google Cloud Console, go to Security > Key Management > Key rings\n2. Open the key ring and select the key\n3. Click Edit rotation schedule (or Set rotation schedule)\n4. Set Rotation period to 365 days or less\n5. Set Next rotation date/time\n6. Click Save", + "Terraform": "```hcl\nresource \"google_kms_crypto_key\" \"\" {\n name = \"\"\n key_ring = \"\"\n purpose = \"ENCRYPT_DECRYPT\"\n\n rotation_period = \"31536000s\" # Critical: sets automatic rotation to 365 days (<= 365 ensures PASS)\n}\n```" }, "Recommendation": { - "Text": "After a successful key rotation, the older key version is required in order to decrypt the data encrypted by that previous key version.", - "Url": "https://cloud.google.com/iam/docs/manage-access-service-accounts" + "Text": "Enable **auto-rotation** for customer-managed keys with an interval `365` days.\n\nAdopt a **key lifecycle** policy: enforce **least privilege** on key usage, apply **separation of duties** between key admins and users, monitor key access, and rehearse emergency rotation to minimize blast radius.", + "Url": "https://hub.prowler.com/check/kms_key_rotation_enabled" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.metadata.json index 1d87991ba3..a8da3973fd 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for Audit Configuration Changes.", + "CheckTitle": "Log metric filter for audit configuration changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for Audit Configuration Changes.", - "Risk": "Admin Activity audit logs and Data Access audit logs produced by the Google Cloud Audit Logs service can be extremely useful for security analysis, resource change tracking, and compliance auditing.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "**Cloud Logging** log-based metrics capture **audit configuration changes** (e.g., `SetIamPolicy` with `auditConfigDeltas`), and an associated **Cloud Monitoring alert policy** notifies when such log entries occur.", + "Risk": "Unmonitored **Audit Config** changes can reduce or disable **Admin Activity/Data Access** logging, creating blind spots. Adversaries could suppress evidence, evade detection, and alter permissions unnoticed, degrading **confidentiality**, **integrity**, and the **availability** of forensic telemetry.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-audit-configuration-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-audit-configuration-changes-monitoring.html", - "Terraform": "" + "Other": "1. In Cloud Console, go to Logging > Logs-based metrics and click Create metric\n2. Set Metric type to Counter, Name to , and Filter to:\n protoPayload.methodName=\"SetIamPolicy\" AND protoPayload.serviceData.policyDelta.auditConfigDeltas:*\n3. Save the metric\n4. Go to Monitoring > Alerting > Create policy > Add condition\n5. Select Metric as the condition type, then choose metric: logging.googleapis.com/user/\n6. Set condition to is above 0 for 1 minute and click Done\n7. Name the policy and click Create (notification channels are optional for this check)", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"metric\" {\n name = \"\"\n # Critical: this filter captures audit config changes via SetIamPolicy\n filter = \"protoPayload.methodName=\\\"SetIamPolicy\\\" AND protoPayload.serviceData.policyDelta.auditConfigDeltas:*\"\n}\n\nresource \"google_monitoring_alert_policy\" \"alert\" {\n display_name = \"\"\n combiner = \"OR\"\n\n conditions {\n display_name = \"\"\n condition_threshold {\n # Critical: associates the alert with the log-based metric created above\n filter = \"metric.type=\\\"logging.googleapis.com/user/\\\"\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"60s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "By using Google Cloud alerting policies to detect audit configuration changes, you make sure that the recommended state of audit configuration is well maintained so that all the activities performed within your GCP project are available for security analysis and auditing at any point in time.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Create a log-based metric for **audit configuration changes** and pair it with a **log-based alert policy** that notifies responders.\n- Enforce **least privilege** on logging/IAM changes\n- Apply **change control** and **separation of duties**\n- Route alerts to durable channels and include response runbooks for **defense in depth**", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.metadata.json index e2188d51f6..552f0fc538 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for Cloud Storage IAM Permission Changes.", + "CheckTitle": "Log metric filter for Cloud Storage IAM permission changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for Cloud Storage IAM Permission Changes.", - "Risk": "Monitoring changes to cloud storage bucket permissions may reduce the time needed to detect and correct permissions on sensitive cloud storage buckets and objects inside the bucket.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "**Cloud Logging** defines a log-based metric for **Cloud Storage IAM changes** using filter `resource.type=\"gcs_bucket\" AND protoPayload.methodName=\"storage.setIamPermissions\"`, and a **Cloud Monitoring alert policy** that references that metric.", + "Risk": "Lack of alerting on bucket IAM changes degrades **confidentiality and integrity**. Adversaries or misconfigurations can:\n- grant broad/public access\n- persist access by adding roles\n- read, alter, or delete data\nDelays in detection enable **data exfiltration**, tampering, and disruptive actions.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-bucket-permission-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-bucket-permission-changes-monitoring.html", - "Terraform": "" + "Other": "1. In Google Cloud console, go to Logging > Logs-based metrics\n2. Click Create metric\n3. Name: \n4. In Filter, paste: resource.type=\"gcs_bucket\" AND protoPayload.methodName=\"storage.setIamPermissions\"\n5. Click Create\n6. In the metrics list, click the three dots for the new metric and select Create alert from metric\n7. Keep condition as Count > 0 for Most recent value and click Save", + "Terraform": "```hcl\n# Create a logs-based metric for GCS IAM permission changes\nresource \"google_logging_metric\" \"\" {\n name = \"\"\n filter = \"resource.type=\\\"gcs_bucket\\\" AND protoPayload.methodName=\\\"storage.setIamPermissions\\\"\" # CRITICAL: matches required filter for detection\n}\n\n# Alert policy referencing the above metric\nresource \"google_monitoring_alert_policy\" \"\" {\n display_name = \"\"\n combiner = \"OR\"\n conditions {\n display_name = \"\"\n condition_threshold {\n filter = \"metric.type=\\\"logging.googleapis.com/user/${google_logging_metric..name}\\\"\" # CRITICAL: ties alert to the metric so check passes\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that a metric filter and alarm be established for Cloud Storage Bucket IAM changes.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Establish a **log-based metric** for bucket IAM permission changes with filter `resource.type=\"gcs_bucket\" AND protoPayload.methodName=\"storage.setIamPermissions\"` and link a **log-based alert policy** with clear notifications. Enforce **least privilege** and **separation of duties**, and routinely review alerts and audit logs to prevent and contain unauthorized access.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging", + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json index 9cdbbc51e8..d2eb0c8ef3 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json @@ -13,7 +13,7 @@ "Risk": "Without monitoring for Compute Engine configuration changes, **unauthorized modifications** to compute resources may go undetected. Attackers can establish **persistence** through instance modifications, escalate privileges via IAM policy changes, disable security controls, or pivot to other resources. This compromises **confidentiality**, **integrity**, and **availability** of workloads and may enable **data exfiltration** or **lateral movement**.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/gcp-compute-engine-configuration-changes.html", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/gcp-compute-engine-configuration-changes.html", "https://cloud.google.com/logging/docs/audit", "https://cloud.google.com/monitoring/alerts" ], diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.metadata.json index e6237e21de..e86597895a 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for Custom Role Changes.", + "CheckTitle": "Log metric filter for IAM custom role changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for Custom Role Changes.", - "Risk": "Google Cloud IAM provides predefined roles that give granular access to specific Google Cloud Platform resources and prevent unwanted access to other resources.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "Cloud projects are assessed for log-based metrics that filter `resource.type=\"iam_role\"` and the methods `CreateRole`, `DeleteRole`, `UpdateRole`, and for an associated Cloud Monitoring **alert policy** that references those metrics.", + "Risk": "Without alerts on custom role changes, privilege modifications can go unnoticed, enabling **privilege escalation**, unauthorized data access (confidentiality), permission tampering (integrity), and accidental revocations that disrupt services (availability). Insider misuse or compromised admins can silently reshape access across projects.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-custom-role-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-custom-role-changes-monitoring.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Logging > Logs-based metrics\n2. Click Create metric (Counter), set Name to \n3. In Filter, paste exactly: resource.type=\"iam_role\" AND (protoPayload.methodName=\"google.iam.admin.v1.CreateRole\" OR protoPayload.methodName=\"google.iam.admin.v1.DeleteRole\" OR protoPayload.methodName=\"google.iam.admin.v1.UpdateRole\") and Save\n4. Go to Monitoring > Alerting > Create policy\n5. Add Condition > Metric\n6. For Metric, search and select logging.googleapis.com/user/\n7. Set threshold > 0 and duration 0 min (or 0s), then Save the policy", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"\" {\n name = \"\"\n # Critical: log-based metric for IAM custom role create/update/delete\n filter = \"resource.type=\\\"iam_role\\\" AND (protoPayload.methodName=\\\"google.iam.admin.v1.CreateRole\\\" OR protoPayload.methodName=\\\"google.iam.admin.v1.DeleteRole\\\" OR protoPayload.methodName=\\\"google.iam.admin.v1.UpdateRole\\\")\"\n}\n\nresource \"google_monitoring_alert_policy\" \"\" {\n display_name = \"\"\n conditions {\n condition_threshold {\n # Critical: alert policy targets the user log-based metric by name\n filter = \"metric.type=\\\"logging.googleapis.com/user/\\\"\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that a metric filter and alarm be established for changes to Identity and Access Management (IAM) role creation, deletion and updating activities.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Define log-based metrics capturing `resource.type=\"iam_role\"` events for `CreateRole`, `DeleteRole`, and `UpdateRole`, and attach **alert policies** to notify responders.\n\nEnforce **least privilege**, **separation of duties**, and **change control** for role management, and retain **audit logs** for investigation.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.metadata.json index c537ee2302..eaa437aa3c 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", - "CheckTitle": "Ensure Log Metric Filter and Alerts Exist for Project Ownership Assignments/Changes.", + "CheckTitle": "Log metric filter for project ownership assignments/changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure Log Metric Filter and Alerts Exist for Project Ownership Assignments/Changes.", - "Risk": "Project ownership has the highest level of privileges on a GCP project. These privileges include viewer permissions on all GCP services inside the project, permission to modify the state of all GCP services within the project, set up billing and manage roles and permissions for the project and all the resources inside the project.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "Cloud Logging contains a **log-based metric** targeting project ownership changes in Cloud Resource Manager events, and Cloud Monitoring has an **alerting policy** tied to that metric. It detects metrics matching `roles/owner` additions/removals or ownership invites, and whether an alert references that metric.", + "Risk": "Lack of alerts on ownership changes enables **privilege escalation** and **project takeover**. Attackers can add/remove `roles/owner`, causing unauthorized data access (confidentiality), unauthorized config/billing changes (integrity), and resource deletion or lockout (availability) without timely detection.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-ownership-assignments-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-ownership-assignments-monitoring.html", - "Terraform": "" + "Other": "1. In the Google Cloud console, go to Logging > Logs-based metrics and click Create metric\n2. Set Name to \n3. In Filter, paste:\n (protoPayload.serviceName=\"cloudresourcemanager.googleapis.com\") AND (ProjectOwnership OR projectOwnerInvitee) OR (protoPayload.serviceData.policyDelta.bindingDeltas.action=\"REMOVE\" AND protoPayload.serviceData.policyDelta.bindingDeltas.role=\"roles/owner\") OR (protoPayload.serviceData.policyDelta.bindingDeltas.action=\"ADD\" AND protoPayload.serviceData.policyDelta.bindingDeltas.role=\"roles/owner\")\n4. Click Create\n5. Go to Monitoring > Alerting > Create policy > Add condition\n6. Choose Metric, then select metric type logging.googleapis.com/user/\n7. Set condition to Greater than 0 with a duration of 0 minutes, then click Add\n8. Click Create policy (notification channels optional)\n9. Verify the alert condition filter contains ", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"\" {\n name = \"\"\n # CRITICAL: Detects project ownership assignments/changes\n filter = <<-EOT\n(protoPayload.serviceName=\"cloudresourcemanager.googleapis.com\") AND (ProjectOwnership OR projectOwnerInvitee) OR (protoPayload.serviceData.policyDelta.bindingDeltas.action=\"REMOVE\" AND protoPayload.serviceData.policyDelta.bindingDeltas.role=\"roles/owner\") OR (protoPayload.serviceData.policyDelta.bindingDeltas.action=\"ADD\" AND protoPayload.serviceData.policyDelta.bindingDeltas.role=\"roles/owner\")\nEOT\n}\n\nresource \"google_monitoring_alert_policy\" \"\" {\n display_name = \"\"\n combiner = \"OR\"\n\n conditions {\n condition_threshold {\n # CRITICAL: References the log-based metric so an alert is associated with it\n filter = \"metric.type=\\\"logging.googleapis.com/user/${google_logging_metric..name}\\\"\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Using Google Cloud alerting policies to detect ownership assignments/changes will help you maintain the right access permissions for each IAM member created within your project, follow the security principle of least privilege, and prevent any accidental or intentional changes that may lead to unauthorized actions.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Create a log-based metric for ownership assignment/removal events and link it to an alerting policy that notifies a monitored channel. Minimize use of `roles/owner` per **least privilege**, require approvals and separation of duties, and apply **defense in depth** with centralized monitoring of IAM changes across projects.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging", + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.metadata.json index 98a4eaa3c7..261dc25eb0 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for SQL Instance Configuration Changes.", + "CheckTitle": "Log metric filter for Cloud SQL instance configuration changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for SQL Instance Configuration Changes.", - "Risk": "Monitoring changes to SQL instance configuration changes may reduce the time needed to detect and correct misconfigurations done on the SQL server.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "**Cloud Logging** has a log-based metric matching Cloud SQL instance updates (`protoPayload.methodName=\"cloudsql.instances.update\"`) and a **Cloud Monitoring** alert policy references that metric to notify on configuration changes.", + "Risk": "Without this visibility, **unauthorized or accidental Cloud SQL configuration changes** can persist undetected. Attackers or insiders might open public access, relax TLS, alter authorized networks, or disable backups, degrading **confidentiality**, **integrity**, and **availability** and delaying containment.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-network-route-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-network-route-changes-monitoring.html", - "Terraform": "" + "Other": "1. In Google Cloud console, go to Logging > Logs-based metrics and click Create metric\n2. Set Name to and Filter to: protoPayload.methodName=\"cloudsql.instances.update\" then click Create\n3. Go to Monitoring > Alerting > Create policy\n4. Click Add condition > Metric\n5. For Metric, select logging.googleapis.com/user/\n6. Set threshold to Greater than 0 over 0 minutes and click Add\n7. Click Save policy", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"sql_config_changes\" {\n name = \"\"\n # Critical: captures Cloud SQL instance configuration updates\n filter = \"protoPayload.methodName=\\\"cloudsql.instances.update\\\"\"\n}\n\nresource \"google_monitoring_alert_policy\" \"sql_config_change_alert\" {\n display_name = \"\"\n combiner = \"OR\"\n\n conditions {\n condition_threshold {\n # Critical: reference the logs-based metric so the alert is associated\n filter = \"metric.type=\\\"logging.googleapis.com/user/${google_logging_metric.sql_config_changes.name}\\\"\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that a metric filter and alarm be established for SQL instance configuration changes.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Implement a **log-based metric** for Cloud SQL update events and attach a **Monitoring alert policy** that routes timely notifications. Apply **least privilege** for admin actions, enforce **change management** and **separation of duties**, and integrate alerts with on-call workflows to speed triage and prevent misconfigurations.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.metadata.json index 9214f1dfb9..e97042a2d3 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Firewall Rule Changes.", + "CheckTitle": "Log metric filter for VPC network firewall rule changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Firewall Rule Changes.", - "Risk": "Monitoring for Create or Update Firewall rule events gives insight to network access changes and may reduce the time it takes to detect suspicious activity.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "Cloud Logging has a log-based metric for **VPC firewall rule** changes, matching `resource.type=\"gce_firewall_rule\"` and `protoPayload.methodName` of `compute.firewalls.insert`, `compute.firewalls.patch`, or `compute.firewalls.delete`, and Cloud Monitoring includes an alerting policy that references this metric.", + "Risk": "Without alerts on firewall rule changes, unauthorized or accidental modifications can go unnoticed, exposing services or blocking critical traffic.\n\nConfidentiality suffers (opened ports), integrity is reduced (tampered controls), and availability can be impacted (outages), enabling lateral movement and data exfiltration.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-firewall-rule-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-firewall-rule-changes-monitoring.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Logging > Logs-based metrics\n2. Click Create metric, set Name to \n3. In Filter, paste: resource.type=\"gce_firewall_rule\" AND (protoPayload.methodName:\"compute.firewalls.patch\" OR protoPayload.methodName:\"compute.firewalls.insert\" OR protoPayload.methodName:\"compute.firewalls.delete\")\n4. Save the metric\n5. Go to Monitoring > Alerting > Create policy\n6. Add Condition > Metric threshold\n7. For Metric, select User-defined and choose logging.googleapis.com/user/\n8. Set condition to > 0 with duration 0 minutes (or the minimum allowed)\n9. Create the policy (notification channels are optional)", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"\" {\n name = \"\"\n # CRITICAL: this filter captures VPC firewall rule changes\n filter = \"resource.type=\\\"gce_firewall_rule\\\" AND (protoPayload.methodName:\\\"compute.firewalls.patch\\\" OR protoPayload.methodName:\\\"compute.firewalls.insert\\\" OR protoPayload.methodName:\\\"compute.firewalls.delete\\\")\"\n}\n\nresource \"google_monitoring_alert_policy\" \"\" {\n display_name = \"\"\n combiner = \"OR\"\n\n conditions {\n display_name = \"\"\n condition_threshold {\n # CRITICAL: reference the user log metric so the alert policy is associated\n # This makes the policy filter contain the metric name, satisfying the check\n filter = \"metric.type=\\\"logging.googleapis.com/user/${google_logging_metric..name}\\\"\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that a metric filter and alarm be established for Virtual Private Cloud (VPC) Network Firewall rule changes.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Establish a log-based metric for `gce_firewall_rule` insert/patch/delete events and tie it to an alerting policy that notifies responders.\n\nEnforce **least privilege** and change control on firewall updates, apply **separation of duties**, and monitor all projects for rapid, auditable detection of network control changes.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.metadata.json index 70c76b966f..4c89bdcb7e 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Changes.", + "CheckTitle": "Log metric filter for VPC network changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Changes.", - "Risk": "Monitoring changes to a VPC will help ensure VPC traffic flow is not getting impacted.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "Cloud projects are evaluated for a **log-based metric** with a linked **Cloud Monitoring alert** that targets **VPC network changes** on `gce_network` audit events: `compute.networks.insert`, `patch`, `delete`, `addPeering`, `removePeering`.\n\nIt checks that these changes are captured and generate notifications.", + "Risk": "Missing alerts on VPC changes lets **unauthorized or accidental modifications** go unnoticed, risking:\n- Data exposure via unintended peering or new networks\n- Segmentation bypass through routing/subnet edits\n- Outages from deletions or misconfigurations\n\nThis affects confidentiality, integrity, and availability.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-vpc-network-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-vpc-network-changes-monitoring.html", - "Terraform": "" + "Other": "1. In the Google Cloud Console, go to Logging > Logs-based metrics > + Create metric\n2. Name: \n3. In Filter, paste exactly:\n resource.type=\"gce_network\" AND (protoPayload.methodName:\"compute.networks.insert\" OR protoPayload.methodName:\"compute.networks.patch\" OR protoPayload.methodName:\"compute.networks.delete\" OR protoPayload.methodName:\"compute.networks.removePeering\" OR protoPayload.methodName:\"compute.networks.addPeering\")\n4. Click Create metric\n5. Go to Monitoring > Alerting > + Create policy > Add condition\n6. Select the metric logging.googleapis.com/user/\n7. Condition: Greater than, Threshold: 0, For: 0 minutes; click Add\n8. Name the policy and click Create policy", + "Terraform": "```hcl\n# Create a logs-based metric for VPC network changes\nresource \"google_logging_metric\" \"\" {\n name = \"\"\n # CRITICAL: Filter capturing VPC network create/modify/delete/peering changes\n filter = \"resource.type=\\\"gce_network\\\" AND (protoPayload.methodName:\\\"compute.networks.insert\\\" OR protoPayload.methodName:\\\"compute.networks.patch\\\" OR protoPayload.methodName:\\\"compute.networks.delete\\\" OR protoPayload.methodName:\\\"compute.networks.removePeering\\\" OR protoPayload.methodName:\\\"compute.networks.addPeering\\\")\"\n}\n\n# Alert policy associated with the logs-based metric\nresource \"google_monitoring_alert_policy\" \"\" {\n display_name = \"\"\n\n conditions {\n condition_threshold {\n # CRITICAL: Reference the logs-based metric so the policy is associated with it\n filter = \"metric.type=\\\"logging.googleapis.com/user/\\\"\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that a metric filter and alarm be established for Virtual Private Cloud (VPC) network changes.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Implement a **log-based metric** for VPC change audit events and attach an **alerting policy** that notifies accountable teams.\n\nApply **least privilege** for network admins, enforce **change approval**, and adopt **defense in depth** to prevent and quickly detect unintended network changes.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.metadata.json index 2fa5010aed..8e0dad31f9 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Route Changes.", + "CheckTitle": "Log metric filter for VPC network route changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Route Changes.", - "Risk": "Monitoring changes to route tables will help ensure that all VPC traffic flows through an expected path.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "**Cloud Logging** includes a **log-based metric** for **VPC route modifications** and a linked **Cloud Monitoring alert**.\n\nIt targets `gce_route` entries for `compute.routes.insert` and `compute.routes.delete` so route creations or deletions generate alertable signals.", + "Risk": "Without visibility into **route changes**, attackers or mistakes can:\n- Reroute traffic to bypass inspection **data exfiltration** (confidentiality)\n- Alter paths enabling **lateral movement** (integrity)\n- Blackhole networks causing **outages** (availability)", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-network-route-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-network-route-changes-monitoring.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Logging > Logs-based metrics and click Create metric\n2. Set Name to \n3. In Filter, paste: resource.type=\"gce_route\" AND (protoPayload.methodName:\"compute.routes.delete\" OR protoPayload.methodName:\"compute.routes.insert\")\n4. Click Create metric\n5. Go to Monitoring > Alerting > Create policy\n6. Add Condition: Metric threshold; Select metric logging.googleapis.com/user/; Set condition to > 0 for 0 minutes\n7. Click Create policy (skip notification channels if not needed)\n", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"\" {\n name = \"\"\n filter = \"resource.type=\\\"gce_route\\\" AND (protoPayload.methodName:\\\"compute.routes.delete\\\" OR protoPayload.methodName:\\\"compute.routes.insert\\\")\" # Critical: matches VPC route insert/delete events\n}\n\nresource \"google_monitoring_alert_policy\" \"\" {\n display_name = \"\"\n\n conditions {\n condition_threshold {\n filter = \"metric.type=\\\"logging.googleapis.com/user/\\\"\" # Critical: alert evaluates the log-based metric by name\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that a metric filter and alarm be established for Virtual Private Cloud (VPC) network route changes.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Create a **log-based metric** for `compute.routes.insert` and `compute.routes.delete`, and attach a **log-based alert** with reliable notifications.\n\nApply **least privilege** to route management, require **change approval**, and use **defense in depth** (egress filtering, private routing, durable audit logs).", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_sink_created/logging_sink_created.metadata.json b/prowler/providers/gcp/services/logging/logging_sink_created/logging_sink_created.metadata.json index beac1b934a..be7ce98c8f 100644 --- a/prowler/providers/gcp/services/logging/logging_sink_created/logging_sink_created.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_sink_created/logging_sink_created.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "logging_sink_created", - "CheckTitle": "Ensure there is at least one sink used to export copies of all the log entries.", + "CheckTitle": "Project has at least one logging sink exporting copies of all log entries", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Sink", - "ResourceGroup": "monitoring", - "Description": "Ensure there is at least one sink used to export copies of all the log entries.", - "Risk": "If sinks are not created, logs would be deleted after the configured retention period, and would not be backed up.", + "ResourceType": "logging.googleapis.com/LogSink", + "Description": "**Cloud Logging** project contains at least one **sink** that exports a copy of **all log entries** to a destination for centralized retention or processing", + "Risk": "Without exporting all logs, audit evidence can expire or be altered in-project, reducing the **availability** and **integrity** of telemetry. This hinders threat detection and forensics, prevents cross-project correlation, and can let attacker actions evade scrutiny after log rotation or deletion.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/export-all-log-entries.html", + "https://cloud.google.com/logging/docs/export" + ], "Remediation": { "Code": { - "CLI": "gcloud logging sinks create ", + "CLI": "gcloud logging sinks create logging.googleapis.com/projects/", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/export-all-log-entries.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Logging > Log Router\n2. Click Create sink\n3. Set Sink name to \n4. For Sink destination, select Google Cloud project and choose \n5. Leave the inclusion filter empty (exports all logs)\n6. Click Create", + "Terraform": "```hcl\n# Create a project-level logging sink exporting all logs\nresource \"google_logging_project_sink\" \"sink\" {\n name = \"\" # critical: creates the sink required by the check\n destination = \"logging.googleapis.com/projects/\" # critical: required destination\n # No filter set -> exports all log entries (fixes the finding)\n}\n```" }, "Recommendation": { - "Text": "It is recommended to create a sink that will export copies of all the log entries. This can help aggregate logs from multiple projects and export them to a Security Information and Event Management (SIEM).", - "Url": "https://cloud.google.com/logging/docs/export" + "Text": "Create a **centralized export sink** that routes all logs to a secured, durable, preferably **immutable** destination with extended retention. Apply **least privilege** to sink identities, separate duties by isolating destinations, use **defense in depth** (encryption, access controls), and monitor sink health for continuity.", + "Url": "https://hub.prowler.com/check/logging_sink_created" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/github/github_provider.py b/prowler/providers/github/github_provider.py index 384c0b9de0..9088b79c6f 100644 --- a/prowler/providers/github/github_provider.py +++ b/prowler/providers/github/github_provider.py @@ -1,3 +1,4 @@ +import logging import os from os import environ from typing import Union @@ -134,8 +135,6 @@ class GithubProvider(Provider): logger.info("Instantiating GitHub Provider...") # Mute GitHub library logs to reduce noise since it is already handled by the Prowler logger - import logging - logging.getLogger("github").setLevel(logging.CRITICAL) logging.getLogger("github.GithubRetry").setLevel(logging.CRITICAL) @@ -286,6 +285,18 @@ class GithubProvider(Provider): else: app_key = format_rsa_key(github_app_key_content) + # Check for incomplete GitHub App credentials (user provided only part of them) + elif (github_app_key or github_app_key_content) and not github_app_id: + raise GithubEnvironmentVariableError( + file=os.path.basename(__file__), + message="GitHub App authentication requires both --github-app-id and --github-app-key-path (or --github-app-key). Missing: --github-app-id", + ) + elif github_app_id and not (github_app_key or github_app_key_content): + raise GithubEnvironmentVariableError( + file=os.path.basename(__file__), + message="GitHub App authentication requires both --github-app-id and --github-app-key-path (or --github-app-key). Missing: --github-app-key-path or --github-app-key", + ) + else: # PAT logger.info( diff --git a/prowler/providers/github/services/organization/organization_service.py b/prowler/providers/github/services/organization/organization_service.py index 2f7e9bc4ea..187da72f68 100644 --- a/prowler/providers/github/services/organization/organization_service.py +++ b/prowler/providers/github/services/organization/organization_service.py @@ -76,6 +76,7 @@ class Organization(GithubService): id=user.id, name=user.login, mfa_required=None, # Users don't have MFA requirements like orgs + is_verified=None, ) logger.info( f"Added user '{user.login}' as organization for checks" @@ -195,7 +196,7 @@ class Organization(GithubService): if isinstance(base_permission_raw, str) else None ) - + is_verified = _extract_flag("is_verified", bool) organizations[org.id] = Org( id=org.id, name=org.login, @@ -216,6 +217,7 @@ class Organization(GithubService): "members_allowed_repository_creation_type" ], base_permission=base_permission, + is_verified=is_verified, ) @@ -231,3 +233,4 @@ class Org(BaseModel): members_can_create_internal_repositories: Optional[bool] = None members_allowed_repository_creation_type: Optional[str] = None base_permission: Optional[str] = None + is_verified: Optional[bool] = None diff --git a/prowler/providers/github/services/organization/organization_verified_badge/__init__.py b/prowler/providers/github/services/organization/organization_verified_badge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/services/organization/organization_verified_badge/organization_verified_badge.metadata.json b/prowler/providers/github/services/organization/organization_verified_badge/organization_verified_badge.metadata.json new file mode 100644 index 0000000000..83bcb7a9dd --- /dev/null +++ b/prowler/providers/github/services/organization/organization_verified_badge/organization_verified_badge.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "github", + "CheckID": "organization_verified_badge", + "CheckTitle": "Ensure GitHub organization has a verified badge", + "CheckType": [], + "ServiceName": "organization", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "GitHubOrganization", + "ResourceGroup": "governance", + "Description": "Checks whether a GitHub organization has a verified badge.", + "Risk": "Unverified organizations may be easier to impersonate, increasing the risk of phishing or trust abuse.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.github.com/en/organizations/managing-organization-settings/verifying-or-approving-a-domain-for-your-organization" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Verify the organization identity by completing GitHub organization verification.", + "Url": "https://hub.prowler.com/check/organization_verified_badge" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check uses the GitHub API field is_verified from organization metadata." +} diff --git a/prowler/providers/github/services/organization/organization_verified_badge/organization_verified_badge.py b/prowler/providers/github/services/organization/organization_verified_badge/organization_verified_badge.py new file mode 100644 index 0000000000..948c543fd0 --- /dev/null +++ b/prowler/providers/github/services/organization/organization_verified_badge/organization_verified_badge.py @@ -0,0 +1,31 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGithub +from prowler.providers.github.services.organization.organization_client import ( + organization_client, +) + + +class organization_verified_badge(Check): + """Check if GitHub organizations are verified.""" + + def execute(self) -> List[CheckReportGithub]: + findings: List[CheckReportGithub] = [] + + for org in organization_client.organizations.values(): + report = CheckReportGithub(metadata=self.metadata(), resource=org) + + if org.is_verified: + report.status = "PASS" + report.status_extended = ( + f"Organization {org.name} is verified on GitHub." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Organization {org.name} is not verified on GitHub." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/github/services/repository/repository_branch_delete_on_merge_enabled/repository_branch_delete_on_merge_enabled.py b/prowler/providers/github/services/repository/repository_branch_delete_on_merge_enabled/repository_branch_delete_on_merge_enabled.py index 145e4261e1..c7c8e12283 100644 --- a/prowler/providers/github/services/repository/repository_branch_delete_on_merge_enabled/repository_branch_delete_on_merge_enabled.py +++ b/prowler/providers/github/services/repository/repository_branch_delete_on_merge_enabled/repository_branch_delete_on_merge_enabled.py @@ -26,7 +26,10 @@ class repository_branch_delete_on_merge_enabled(Check): report.status = "FAIL" report.status_extended = f"Repository {repo.name} does not delete branches on merge in default branch ({repo.default_branch.name})." - if repo.delete_branch_on_merge: + if repo.delete_branch_on_merge is None: + report.status = "MANUAL" + report.status_extended = f"Repository {repo.name} branch deletion setting could not be checked in default branch ({repo.default_branch.name}) due to insufficient permissions. Requires Administration: Read and Write permission." + elif repo.delete_branch_on_merge: report.status = "PASS" report.status_extended = f"Repository {repo.name} does delete branches on merge in default branch ({repo.default_branch.name})." diff --git a/prowler/providers/github/services/repository/repository_service.py b/prowler/providers/github/services/repository/repository_service.py index 244de81e08..deb6864cb2 100644 --- a/prowler/providers/github/services/repository/repository_service.py +++ b/prowler/providers/github/services/repository/repository_service.py @@ -121,15 +121,22 @@ class Repository(GithubService): ) ): if self.provider.repositories: - logger.info( - f"Filtering for specific repositories: {self.provider.repositories}" - ) + qualified_repos = [] for repo_name in self.provider.repositories: - if not self._validate_repository_format(repo_name): + if self._validate_repository_format(repo_name): + qualified_repos.append(repo_name) + elif self.provider.organizations: + for org_name in self.provider.organizations: + qualified_repos.append(f"{org_name}/{repo_name}") + else: logger.warning( f"Repository name '{repo_name}' should be in 'owner/repo-name' format. Skipping." ) - continue + + logger.info( + f"Filtering for specific repositories: {qualified_repos}" + ) + for repo_name in qualified_repos: try: repo = client.get_repo(repo_name) self._process_repository(repo, repos) @@ -138,7 +145,7 @@ class Repository(GithubService): error, "accessing repository", repo_name ) - if self.provider.organizations: + elif self.provider.organizations: logger.info( f"Filtering for repositories in organizations: {self.provider.organizations}" ) @@ -234,11 +241,9 @@ class Repository(GithubService): codeowners_exists = None else: codeowners_exists = False - delete_branch_on_merge = ( - repo.delete_branch_on_merge - if repo.delete_branch_on_merge is not None - else False - ) + # GitHub API only returns delete_branch_on_merge with Administration: Read and Write + # With Read-only permission, it returns None - set to None for MANUAL status + delete_branch_on_merge = repo.delete_branch_on_merge require_pr = False approval_cnt = 0 diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 81202b5a84..9cc7207f20 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -1,4 +1,7 @@ import os +import re + +from typing_extensions import override from prowler.lib.logger import logger from prowler.lib.powershell.powershell import PowerShellSession @@ -46,6 +49,28 @@ class M365PowerShell(PowerShellSession): self.tenant_identity = identity self.init_credential(credentials) + @override + def _process_error(self, error_result: str) -> None: + """ + Process PowerShell errors with M365-specific handling. + + Detects cmdlet not found errors which typically indicate missing licensing + (e.g., Microsoft Defender for Office 365) or insufficient permissions. + + Args: + error_result (str): The error output from the PowerShell command. + """ + if "is not recognized as a name of a cmdlet" in error_result: + cmdlet_match = re.search(r"'([^']+)'.*is not recognized", error_result) + cmdlet_name = cmdlet_match.group(1) if cmdlet_match else "Unknown" + logger.warning( + f"PowerShell cmdlet '{cmdlet_name}' is not available. " + f"This may indicate missing module, licensing (e.g., Microsoft Defender for Office 365) " + f"or insufficient permissions. Related checks will be skipped." + ) + else: + super()._process_error(error_result) + def clean_certificate_content(self, cert_content: str) -> str: """ Clean certificate content for PowerShell consumption. @@ -823,6 +848,78 @@ class M365PowerShell(PowerShellSession): "Get-SharingPolicy | ConvertTo-Json -Depth 10", json_parse=True ) + def get_safe_attachments_policy(self) -> dict: + """ + Get Safe Attachments Policy. + + Retrieves the Safe Attachments policy settings for Microsoft Defender for Office 365. + + Returns: + dict: Safe Attachments policy settings in JSON format. + + Example: + >>> get_safe_attachments_policy() + { + "Name": "Built-In Protection Policy", + "Identity": "Built-In Protection Policy", + "Enable": true, + "Action": "Block", + "QuarantineTag": "AdminOnlyAccessPolicy" + } + """ + return self.execute( + "Get-SafeAttachmentPolicy | ConvertTo-Json -Depth 10", json_parse=True + ) + + def get_safe_attachments_rule(self) -> dict: + """ + Get Safe Attachments Rules. + + Retrieves the Safe Attachments rules that define which users, groups, + and domains are targeted by Safe Attachments policies. + + Returns: + dict: Safe Attachments rules in JSON format. + + Example: + >>> get_safe_attachments_rule() + { + "Name": "Custom Safe Attachments Rule", + "SafeAttachmentPolicy": "Custom Policy", + "State": "Enabled", + "Priority": 0, + "SentTo": ["user@contoso.com"], + "SentToMemberOf": ["group@contoso.com"], + "RecipientDomainIs": ["contoso.com"] + } + """ + return self.execute( + "Get-SafeAttachmentRule | ConvertTo-Json -Depth 10", json_parse=True + ) + + def get_advanced_threat_protection_policy(self) -> dict: + """ + Get Advanced Threat Protection Policy. + + Retrieves the current Advanced Threat Protection policy settings, + including Safe Attachments for SharePoint, OneDrive, and Teams, and Safe Documents settings. + + Returns: + dict: Advanced Threat Protection policy settings in JSON format. + + Example: + >>> get_advanced_threat_protection_policy() + { + "Identity": "Default", + "EnableATPForSPOTeamsODB": true, + "EnableSafeDocs": true, + "AllowSafeDocsOpen": false + } + """ + return self.execute( + "Get-AtpPolicyForO365 | ConvertTo-Json -Depth 10", json_parse=True + ) + def get_teams_protection_policy(self) -> dict: """ Get Teams Protection Policy. @@ -883,6 +980,57 @@ class M365PowerShell(PowerShellSession): json_parse=True, ) + def get_safe_links_policy(self) -> dict: + """ + Get Safe Links Policy. + + Retrieves the current Safe Links policy settings for Microsoft Defender for Office 365. + + Returns: + dict: Safe Links policy settings in JSON format. + + Example: + >>> get_safe_links_policy() + { + "Name": "Built-In Protection Policy", + "Identity": "Built-In Protection Policy", + "EnableSafeLinksForEmail": true, + "EnableSafeLinksForTeams": true, + "EnableSafeLinksForOffice": true, + "TrackClicks": true, + "AllowClickThrough": false, + "ScanUrls": true, + "EnableForInternalSenders": true, + "DeliverMessageAfterScan": true, + "DisableUrlRewrite": false + } + """ + return self.execute( + "Get-SafeLinksPolicy | ConvertTo-Json -Depth 10", json_parse=True + ) + + def get_safe_links_rule(self) -> dict: + """ + Get Safe Links Rule. + + Retrieves the current Safe Links rule settings for Microsoft Defender for Office 365. + + Returns: + dict: Safe Links rule settings in JSON format. + + Example: + >>> get_safe_links_rule() + { + "Name": "Safe Links Rule", + "State": "Enabled", + "Priority": 0, + "SafeLinksPolicy": "Policy Name" + } + """ + return self.execute( + "Get-SafeLinksRule | ConvertTo-Json -Depth 10", json_parse=True + ) + # This function is used to install the required M365 PowerShell modules in Docker containers def initialize_m365_powershell_modules(): diff --git a/prowler/providers/m365/m365_provider.py b/prowler/providers/m365/m365_provider.py index f6ef545a7c..0e3a6c2b26 100644 --- a/prowler/providers/m365/m365_provider.py +++ b/prowler/providers/m365/m365_provider.py @@ -1,5 +1,6 @@ import asyncio import base64 +import logging import os from argparse import ArgumentTypeError from os import getenv @@ -157,6 +158,9 @@ class M365Provider(Provider): """ logger.info("Setting M365 provider ...") + # Mute HPACK library logs to prevent token leakage in debug mode + logging.getLogger("hpack").setLevel(logging.CRITICAL) + logger.info("Checking if any credentials mode is set ...") # Validate the authentication arguments diff --git a/prowler/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/__init__.py b/prowler/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured.metadata.json b/prowler/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured.metadata.json new file mode 100644 index 0000000000..5b69d64b5f --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "m365", + "CheckID": "defender_atp_safe_attachments_and_docs_configured", + "CheckTitle": "ATP Safe Attachments policy has Safe Documents enabled and click-through blocked for SharePoint, OneDrive, and Microsoft Teams", + "CheckType": [], + "ServiceName": "defender", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Defender ATP Policy", + "ResourceGroup": "security", + "Description": "**Safe Attachments** for SharePoint, OneDrive, and Microsoft Teams protects organizations from inadvertently sharing malicious files. When enabled, files identified as malicious are blocked and cannot be opened, copied, moved, or shared until further actions are taken by the organization's security team.", + "Risk": "Without **Safe Attachments** enabled, users may download, sync, or access **malicious files** from SharePoint, OneDrive, or Teams, potentially leading to **malware infections** across the organization. If **Safe Documents** is disabled or users can bypass **Protected View**, malicious content in Office documents may execute and **compromise endpoints**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/defender-office-365/safe-attachments-about", + "https://learn.microsoft.com/en-us/defender-office-365/safe-documents-in-e5-plus-security-about" + ], + "Remediation": { + "Code": { + "CLI": "Set-AtpPolicyForO365 -EnableATPForSPOTeamsODB $true -EnableSafeDocs $true -AllowSafeDocsOpen $false", + "NativeIaC": "", + "Other": "1. Navigate to Microsoft 365 Defender portal at https://security.microsoft.com.\n2. Go to **Email & Collaboration** > **Policies & Rules** > **Threat policies**.\n3. Under **Policies**, select **Safe Attachments**.\n4. Click on **Global settings**.\n5. Enable **Turn on Defender for Office 365 for SharePoint, OneDrive, and Microsoft Teams**.\n6. Enable **Turn on Safe Documents for Office clients**.\n7. Disable **Allow people to click through Protected View even if Safe Documents identified the file as malicious**.\n8. Click **Save**.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable **Safe Attachments** for SharePoint, OneDrive, and Microsoft Teams along with **Safe Documents** to protect users from malicious files. Block users from bypassing **Protected View** warnings for files identified as malicious to maintain **defense-in-depth**.", + "Url": "https://hub.prowler.com/check/defender_atp_safe_attachments_and_docs_configured" + } + }, + "Categories": [ + "e5" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check requires Microsoft Defender for Office 365 Plan 1 or Plan 2 (included in E5 license). Safe Documents specifically requires Microsoft 365 E5 or Microsoft 365 E5 Security." +} diff --git a/prowler/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured.py b/prowler/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured.py new file mode 100644 index 0000000000..fe51d08b73 --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured.py @@ -0,0 +1,62 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.defender.defender_client import defender_client + + +class defender_atp_safe_attachments_and_docs_configured(Check): + """ + Check if Safe Attachments for SharePoint, OneDrive, and Teams is properly configured. + + This check verifies that the ATP (Advanced Threat Protection) policy for Office 365 has: + - EnableATPForSPOTeamsODB = True (Safe Attachments enabled for SPO/OneDrive/Teams) + - EnableSafeDocs = True (Safe Documents enabled) + - AllowSafeDocsOpen = False (Users cannot bypass Protected View for malicious files) + + - PASS: All three settings are properly configured. + - FAIL: One or more settings are not properly configured. + """ + + def execute(self) -> List[CheckReportM365]: + """ + Execute the check to verify Safe Attachments ATP policy configuration. + + Returns: + List[CheckReportM365]: A list of reports containing the result of the check. + """ + findings = [] + + if defender_client.advanced_threat_protection_policy: + policy = defender_client.advanced_threat_protection_policy + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.identity, + resource_id=policy.identity, + ) + + # Check all three required settings + is_atp_enabled = policy.enable_atp_for_spo_teams_odb + is_safe_docs_enabled = policy.enable_safe_docs + is_safe_docs_open_blocked = not policy.allow_safe_docs_open + + if is_atp_enabled and is_safe_docs_enabled and is_safe_docs_open_blocked: + # Case 1: ATP policy exists and is properly configured + report.status = "PASS" + report.status_extended = f"ATP policy {policy.identity} has Safe Attachments for SharePoint, OneDrive, and Teams properly configured with Safe Documents enabled and click-through blocked." + else: + # Case 2: ATP policy exists but is not properly configured + report.status = "FAIL" + issues = [] + if not is_atp_enabled: + issues.append("Safe Attachments for SPO/OneDrive/Teams is disabled") + if not is_safe_docs_enabled: + issues.append("Safe Documents is disabled") + if not is_safe_docs_open_blocked: + issues.append("users can bypass Protected View for malicious files") + report.status_extended = f"ATP policy {policy.identity} is not properly configured: {'; '.join(issues)}." + + findings.append(report) + + return findings diff --git a/prowler/providers/m365/services/defender/defender_safe_attachments_policy_enabled/__init__.py b/prowler/providers/m365/services/defender/defender_safe_attachments_policy_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled.metadata.json b/prowler/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled.metadata.json new file mode 100644 index 0000000000..0a4176a0df --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "m365", + "CheckID": "defender_safe_attachments_policy_enabled", + "CheckTitle": "Safe Attachments policy is enabled with secure configuration", + "CheckType": [], + "ServiceName": "defender", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Defender Safe Attachments Policy", + "ResourceGroup": "security", + "Description": "Safe Attachments provides an additional layer of protection by scanning email attachments in a secure environment before delivering them to recipients.\n\nThe Built-In Protection Policy should have **Enable=True**, **Action=Block**, and **QuarantineTag=AdminOnlyAccessPolicy** to ensure malicious attachments are blocked and quarantined with admin-only access.", + "Risk": "Without properly configured Safe Attachments policies, malicious email attachments could reach users' mailboxes and potentially compromise the organization through:\n\n- **Malware delivery** via infected documents\n- **Ransomware attacks** through weaponized attachments\n- **Data exfiltration** using malicious scripts", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/defender-office-365/safe-attachments-about", + "https://learn.microsoft.com/en-us/defender-office-365/preset-security-policies" + ], + "Remediation": { + "Code": { + "CLI": "Set-SafeAttachmentPolicy -Identity 'Built-In Protection Policy' -Enable $true -Action Block -QuarantineTag AdminOnlyAccessPolicy", + "NativeIaC": "", + "Other": "1. Go to Microsoft 365 Defender portal (https://security.microsoft.com)\n2. Navigate to Email & collaboration > Policies & rules > Threat policies\n3. Select Safe Attachments under Policies\n4. Click on Built-In Protection Policy\n5. Set Enable to True, Action to Block, and QuarantineTag to AdminOnlyAccessPolicy\n6. Save the configuration", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable Safe Attachments with the **Block** action to prevent malicious attachments from reaching users. Use **AdminOnlyAccessPolicy** for the quarantine tag to ensure only administrators can release quarantined items, following the principle of least privilege.", + "Url": "https://hub.prowler.com/check/defender_safe_attachments_policy_enabled" + } + }, + "Categories": [ + "email-security", + "e5" + ], + "DependsOn": [], + "RelatedTo": [ + "defender_malware_policy_common_attachments_filter_enabled" + ], + "Notes": "This check requires Microsoft Defender for Office 365 Plan 1 or higher license." +} diff --git a/prowler/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled.py b/prowler/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled.py new file mode 100644 index 0000000000..68714882f1 --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled.py @@ -0,0 +1,112 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.defender.defender_client import defender_client + + +class defender_safe_attachments_policy_enabled(Check): + """ + Check if Safe Attachments policy is properly configured in Microsoft Defender for Office 365. + + This check verifies that Safe Attachments policies have the following settings + configured according to CIS Microsoft 365 Foundations Benchmark: + + - Enable = True + - Action = Block + - QuarantineTag = AdminOnlyAccessPolicy + + Note: The Built-in Protection Policy has fixed settings that cannot be changed + and always provides baseline protection. + """ + + def execute(self) -> List[CheckReportM365]: + findings = [] + + if defender_client.safe_attachments_policies: + # Only Built-in Protection Policy exists (no custom policies with rules) + if not defender_client.safe_attachments_rules: + policy = next(iter(defender_client.safe_attachments_policies.values())) + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.name, + resource_id=policy.identity, + ) + + # Case 1: Only Built-in policy exists - always PASS (fixed settings) + report.status = "PASS" + report.status_extended = f"{policy.name} is the only Safe Attachments policy and provides baseline protection for all users." + findings.append(report) + + # Multiple Safe Attachments Policies (Built-in + custom policies) + else: + for ( + policy_name, + policy, + ) in defender_client.safe_attachments_policies.items(): + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy_name, + resource_id=policy.identity, + ) + + if policy.is_built_in_protection: + # Case 2: Built-in policy with custom policies - always PASS + report.status = "PASS" + report.status_extended = ( + f"{policy_name} provides baseline Safe Attachments protection, " + f"but could be overridden by a misconfigured custom policy for specific users." + ) + findings.append(report) + else: + # Custom policy - check configuration + rule = defender_client.safe_attachments_rules.get(policy_name) + if not rule: + continue + + included_resources = [] + if rule.users: + included_resources.append(f"users: {', '.join(rule.users)}") + if rule.groups: + included_resources.append( + f"groups: {', '.join(rule.groups)}" + ) + if rule.domains: + included_resources.append( + f"domains: {', '.join(rule.domains)}" + ) + # If no users, groups, or domains specified, the policy applies to all recipients + included_resources_str = ( + "; ".join(included_resources) + if included_resources + else "all users" + ) + + if self._is_policy_properly_configured(policy, rule): + # Case 2: Custom policy is properly configured + report.status = "PASS" + report.status_extended = ( + f"Custom Safe Attachments policy {policy_name} is properly configured and includes {included_resources_str}, " + f"with priority {rule.priority} (0 is the highest)." + ) + else: + # Case 3: Custom policy is not properly configured + report.status = "FAIL" + report.status_extended = ( + f"Custom Safe Attachments policy {policy_name} is not properly configured and includes {included_resources_str}, " + f"with priority {rule.priority} (0 is the highest)." + ) + findings.append(report) + + return findings + + def _is_policy_properly_configured(self, policy, rule) -> bool: + """Check if a custom policy is properly configured according to CIS recommendations.""" + return ( + rule.state.lower() == "enabled" + and policy.enable + and policy.action == "Block" + and policy.quarantine_tag == "AdminOnlyAccessPolicy" + ) diff --git a/prowler/providers/m365/services/defender/defender_safelinks_policy_enabled/__init__.py b/prowler/providers/m365/services/defender/defender_safelinks_policy_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled.metadata.json b/prowler/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled.metadata.json new file mode 100644 index 0000000000..cccebcfc7f --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "m365", + "CheckID": "defender_safelinks_policy_enabled", + "CheckTitle": "Safe Links policy is enabled and properly configured in Microsoft Defender for Office 365.", + "CheckType": [], + "ServiceName": "defender", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Defender Safe Links Policy", + "ResourceGroup": "security", + "Description": "Safe Links is a Microsoft Defender for Office 365 feature that provides URL scanning and rewriting of inbound email messages, as well as time-of-click verification of URLs and links in email messages, Teams, and Office apps.\n\nThis check verifies that the Safe Links policy is properly configured with all recommended settings enabled.", + "Risk": "Without properly configured Safe Links protection, users may be vulnerable to **phishing attacks** and **malicious URLs** in emails, Teams messages, and Office documents.\n\nAttackers could bypass security by using URLs that redirect to malicious content after initial scanning.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/defender-office-365/safe-links-about", + "https://learn.microsoft.com/en-us/defender-office-365/safe-links-policies-configure" + ], + "Remediation": { + "Code": { + "CLI": "Set-SafeLinksPolicy -Identity 'Built-In Protection Policy' -EnableSafeLinksForEmail $true -EnableSafeLinksForTeams $true -EnableSafeLinksForOffice $true -TrackClicks $true -AllowClickThrough $false -ScanUrls $true -EnableForInternalSenders $true -DeliverMessageAfterScan $true -DisableUrlRewrite $false", + "NativeIaC": "", + "Other": "1. Navigate to Microsoft 365 Defender at https://security.microsoft.com\n2. Click to expand Email & collaboration and select Policies & rules\n3. Select Threat policies\n4. Under Policies, select Safe Links\n5. Select or create a policy and configure these settings:\n - Enable Safe Links for Email: On\n - Enable Safe Links for Teams: On\n - Enable Safe Links for Office: On\n - Track user clicks: On\n - Let users click through to the original URL: Off\n - Scan URLs: On\n - Apply Safe Links to messages sent within the organization: On\n - Wait for URL scanning to complete before delivering the message: On\n - Do not rewrite URLs: Off\n6. Save the policy", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable and properly configure Safe Links policies to protect users from malicious URLs in emails, Teams, and Office applications. Ensure URL scanning and time-of-click verification are enabled across all communication channels.", + "Url": "https://hub.prowler.com/check/defender_safelinks_policy_enabled" + } + }, + "Categories": [ + "email-security", + "e5" + ], + "DependsOn": [], + "RelatedTo": [ + "defender_antiphishing_policy_configured" + ], + "Notes": "Safe Links requires Microsoft Defender for Office 365 Plan 1 (P1) or higher licensing." +} diff --git a/prowler/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled.py b/prowler/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled.py new file mode 100644 index 0000000000..575397309a --- /dev/null +++ b/prowler/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled.py @@ -0,0 +1,121 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.defender.defender_client import defender_client + + +class defender_safelinks_policy_enabled(Check): + """ + Check if Safe Links policy is enabled and properly configured in Microsoft Defender for Office 365. + + This check verifies that Safe Links policies have the following settings + configured according to CIS Microsoft 365 Foundations Benchmark: + + - EnableSafeLinksForEmail = True + - EnableSafeLinksForTeams = True + - EnableSafeLinksForOffice = True + - TrackClicks = True + - AllowClickThrough = False + - ScanUrls = True + - EnableForInternalSenders = True + - DeliverMessageAfterScan = True + - DisableUrlRewrite = False + + Note: The Built-in Protection Policy has fixed settings that cannot be changed + and always provides baseline protection. + """ + + def execute(self) -> List[CheckReportM365]: + findings = [] + + if defender_client.safe_links_policies: + # Only Built-in Protection Policy exists (no custom policies with rules) + if not defender_client.safe_links_rules: + policy = next(iter(defender_client.safe_links_policies.values())) + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.name, + resource_id=policy.identity, + ) + + # Case 1: Only Built-in policy exists - always PASS (fixed settings) + report.status = "PASS" + report.status_extended = f"{policy.name} is the only Safe Links policy and provides baseline protection for all users." + findings.append(report) + + # Multiple Safe Links Policies (Built-in + custom policies) + else: + for policy_name, policy in defender_client.safe_links_policies.items(): + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy_name, + resource_id=policy.identity, + ) + + if policy.is_built_in_protection: + # Case 2: Built-in policy with custom policies - always PASS + report.status = "PASS" + report.status_extended = ( + f"{policy_name} provides baseline Safe Links protection, " + f"but could be overridden by a misconfigured custom policy for specific users." + ) + findings.append(report) + else: + # Custom policy - check configuration + rule = defender_client.safe_links_rules.get(policy_name) + if not rule: + continue + + included_resources = [] + if rule.users: + included_resources.append(f"users: {', '.join(rule.users)}") + if rule.groups: + included_resources.append( + f"groups: {', '.join(rule.groups)}" + ) + if rule.domains: + included_resources.append( + f"domains: {', '.join(rule.domains)}" + ) + # If no users, groups, or domains specified, the policy applies to all recipients + included_resources_str = ( + "; ".join(included_resources) + if included_resources + else "all users" + ) + + if self._is_policy_properly_configured(policy, rule): + # Case 2: Custom policy is properly configured + report.status = "PASS" + report.status_extended = ( + f"Custom Safe Links policy {policy_name} is properly configured and includes {included_resources_str}, " + f"with priority {rule.priority} (0 is the highest)." + ) + else: + # Case 3: Custom policy is not properly configured + report.status = "FAIL" + report.status_extended = ( + f"Custom Safe Links policy {policy_name} is not properly configured and includes {included_resources_str}, " + f"with priority {rule.priority} (0 is the highest)." + ) + findings.append(report) + + return findings + + def _is_policy_properly_configured(self, policy, rule) -> bool: + """Check if a custom policy is properly configured according to CIS recommendations.""" + return ( + rule.state.lower() == "enabled" + and policy.enable_safe_links_for_email + and policy.enable_safe_links_for_teams + and policy.enable_safe_links_for_office + and policy.track_clicks + and not policy.allow_click_through + and policy.scan_urls + and policy.enable_for_internal_senders + and policy.deliver_message_after_scan + and not policy.disable_url_rewrite + ) diff --git a/prowler/providers/m365/services/defender/defender_service.py b/prowler/providers/m365/services/defender/defender_service.py index 09aa39986d..ba377e2b79 100644 --- a/prowler/providers/m365/services/defender/defender_service.py +++ b/prowler/providers/m365/services/defender/defender_service.py @@ -9,18 +9,38 @@ from prowler.providers.m365.m365_provider import M365Provider class Defender(M365Service): """ - Microsoft 365 Defender service implementation. + Microsoft Defender for Office 365 service class. - Provides access to Microsoft Defender for Office 365 configurations including - malware policies, spam filtering, anti-phishing, and Teams protection settings. + This class provides methods to retrieve and manage Microsoft Defender for Office 365 + security policies and configurations, including malware filters, spam policies, + anti-phishing settings, Safe Attachments, Safe Links, ATP (Advanced Threat Protection), + and Teams protection policies. + + Attributes: + malware_policies (list): List of malware filter policies. + outbound_spam_policies (dict): Dictionary of outbound spam filter policies. + outbound_spam_rules (dict): Dictionary of outbound spam filter rules. + antiphishing_policies (dict): Dictionary of anti-phishing policies. + antiphishing_rules (dict): Dictionary of anti-phishing rules. + connection_filter_policy: Connection filter policy configuration. + dkim_configurations (list): List of DKIM signing configurations. + inbound_spam_policies (list): List of inbound spam filter policies. + inbound_spam_rules (dict): Dictionary of inbound spam filter rules. + report_submission_policy: Report submission policy configuration. + safe_attachments_policies (dict): Dictionary of Safe Attachments policies. + safe_attachments_rules (dict): Dictionary of Safe Attachments rules. + advanced_threat_protection_policy: Advanced Threat Protection policy configuration. + safe_links_policies (dict): Dictionary of Safe Links policies. + safe_links_rules (dict): Dictionary of Safe Links rules. + teams_protection_policy: Teams protection policy configuration. """ def __init__(self, provider: M365Provider): """ - Initialize the Defender service. + Initialize the Defender service client. Args: - provider: The M365 provider instance. + provider: The M365Provider instance for authentication and configuration. """ super().__init__(provider) self.malware_policies = [] @@ -33,6 +53,11 @@ class Defender(M365Service): self.inbound_spam_policies = [] self.inbound_spam_rules = {} self.report_submission_policy = None + self.safe_attachments_policies = {} + self.safe_attachments_rules = {} + self.advanced_threat_protection_policy = None + self.safe_links_policies = {} + self.safe_links_rules = {} self.teams_protection_policy = None if self.powershell: if self.powershell.connect_exchange_online(): @@ -47,6 +72,13 @@ class Defender(M365Service): self.inbound_spam_policies = self._get_inbound_spam_filter_policy() self.inbound_spam_rules = self._get_inbound_spam_filter_rule() self.report_submission_policy = self._get_report_submission_policy() + self.safe_attachments_policies = self._get_safe_attachments_policies() + self.safe_attachments_rules = self._get_safe_attachments_rules() + self.advanced_threat_protection_policy = ( + self._get_advanced_threat_protection_policy() + ) + self.safe_links_policies = self._get_safe_links_policy() + self.safe_links_rules = self._get_safe_links_rule() self.teams_protection_policy = self._get_teams_protection_policy() self.powershell.close() @@ -366,10 +398,10 @@ class Defender(M365Service): def _get_report_submission_policy(self): """ - Retrieve the Defender report submission policy. + Get the Defender report submission policy. Returns: - ReportSubmissionPolicy: The report submission policy configuration. + ReportSubmissionPolicy: The report submission policy configuration or None. """ logger.info("Microsoft365 - Getting Defender report submission policy...") report_submission_policy = None @@ -408,6 +440,179 @@ class Defender(M365Service): ) return report_submission_policy + def _get_safe_attachments_policies(self): + """ + Retrieve Safe Attachments policies from Microsoft Defender for Office 365. + + Returns: + dict[str, SafeAttachmentsPolicy]: A dictionary of Safe Attachments policies keyed by name. + """ + logger.info("Microsoft365 - Getting Defender Safe Attachments policies...") + safe_attachments_policies = {} + try: + policies_data = self.powershell.get_safe_attachments_policy() + if not policies_data: + return safe_attachments_policies + if isinstance(policies_data, dict): + policies_data = [policies_data] + for policy in policies_data: + if policy: + policy_name = policy.get("Name", "") + is_built_in = policy_name == "Built-In Protection Policy" + safe_attachments_policies[policy_name] = SafeAttachmentsPolicy( + name=policy_name, + identity=policy.get("Identity", ""), + enable=policy.get("Enable", False), + action=policy.get("Action", ""), + quarantine_tag=policy.get("QuarantineTag", ""), + redirect=policy.get("Redirect", False), + redirect_address=policy.get("RedirectAddress", ""), + is_built_in_protection=is_built_in, + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return safe_attachments_policies + + def _get_safe_attachments_rules(self): + """ + Retrieve Safe Attachments rules from Microsoft Defender for Office 365. + + Returns: + dict[str, SafeAttachmentsRule]: A dictionary of Safe Attachments rules keyed by policy name. + """ + logger.info("Microsoft365 - Getting Defender Safe Attachments rules...") + safe_attachments_rules = {} + try: + rules_data = self.powershell.get_safe_attachments_rule() + if not rules_data: + return safe_attachments_rules + if isinstance(rules_data, dict): + rules_data = [rules_data] + for rule in rules_data: + if rule: + policy_name = rule.get("SafeAttachmentPolicy", "") + safe_attachments_rules[policy_name] = SafeAttachmentsRule( + state=rule.get("State", ""), + priority=rule.get("Priority", 0), + users=rule.get("SentTo"), + groups=rule.get("SentToMemberOf"), + domains=rule.get("RecipientDomainIs"), + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return safe_attachments_rules + + def _get_advanced_threat_protection_policy(self): + """ + Get the Advanced Threat Protection policy. + + Retrieves the ATP policy settings including Safe Attachments for SharePoint, + OneDrive, and Teams, as well as Safe Documents configuration. + + Returns: + AdvancedThreatProtectionPolicy: The Advanced Threat Protection policy configuration. + """ + logger.info("Microsoft365 - Getting Advanced Threat Protection policy...") + atp_policy = None + try: + policy = self.powershell.get_advanced_threat_protection_policy() + if policy: + atp_policy = AdvancedThreatProtectionPolicy( + identity=policy.get("Identity", "Default"), + enable_atp_for_spo_teams_odb=policy.get( + "EnableATPForSPOTeamsODB", False + ), + enable_safe_docs=policy.get("EnableSafeDocs", False), + allow_safe_docs_open=policy.get("AllowSafeDocsOpen", True), + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return atp_policy + + def _get_safe_links_policy(self): + """ + Get Safe Links policies from Microsoft Defender for Office 365. + + Returns: + dict: A dictionary mapping policy names to SafeLinksPolicy objects. + """ + logger.info("Microsoft365 - Getting Defender Safe Links policies...") + safe_links_policies = {} + try: + safe_links_policy_data = self.powershell.get_safe_links_policy() + if not safe_links_policy_data: + return safe_links_policies + if isinstance(safe_links_policy_data, dict): + safe_links_policy_data = [safe_links_policy_data] + for policy in safe_links_policy_data: + if policy: + safe_links_policies[policy.get("Name", "")] = SafeLinksPolicy( + name=policy.get("Name", ""), + identity=policy.get("Identity", ""), + enable_safe_links_for_email=policy.get( + "EnableSafeLinksForEmail", False + ), + enable_safe_links_for_teams=policy.get( + "EnableSafeLinksForTeams", False + ), + enable_safe_links_for_office=policy.get( + "EnableSafeLinksForOffice", False + ), + track_clicks=policy.get("TrackClicks", False), + allow_click_through=policy.get("AllowClickThrough", True), + scan_urls=policy.get("ScanUrls", False), + enable_for_internal_senders=policy.get( + "EnableForInternalSenders", False + ), + deliver_message_after_scan=policy.get( + "DeliverMessageAfterScan", False + ), + disable_url_rewrite=policy.get("DisableUrlRewrite", True), + is_built_in_protection=policy.get("IsBuiltInProtection", False), + is_default=policy.get("IsDefault", False), + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return safe_links_policies + + def _get_safe_links_rule(self): + """ + Get Safe Links rules from Microsoft Defender for Office 365. + + Returns: + dict: A dictionary mapping policy names to SafeLinksRule objects. + """ + logger.info("Microsoft365 - Getting Defender Safe Links rules...") + safe_links_rules = {} + try: + safe_links_rule_data = self.powershell.get_safe_links_rule() + if not safe_links_rule_data: + return safe_links_rules + if isinstance(safe_links_rule_data, dict): + safe_links_rule_data = [safe_links_rule_data] + for rule in safe_links_rule_data: + if rule: + safe_links_rules[rule.get("SafeLinksPolicy", "")] = SafeLinksRule( + state=rule.get("State", "Disabled"), + priority=rule.get("Priority", 0), + users=rule.get("SentTo", None), + groups=rule.get("SentToMemberOf", None), + domains=rule.get("RecipientDomainIs", None), + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return safe_links_rules + def _get_teams_protection_policy(self): """ Retrieve the Teams protection policy including ZAP settings. @@ -513,7 +718,7 @@ class InboundSpamRule(BaseModel): class ReportSubmissionPolicy(BaseModel): - """Model for Defender report submission policy settings.""" + """Model for Defender report submission policy configuration.""" report_junk_to_customized_address: bool report_not_junk_to_customized_address: bool @@ -525,6 +730,97 @@ class ReportSubmissionPolicy(BaseModel): report_chat_message_to_customized_address_enabled: bool +class SafeAttachmentsPolicy(BaseModel): + """ + Data model for Safe Attachments policy settings. + + Attributes: + name: The name of the policy. + identity: The unique identifier of the policy. + enable: Whether the policy is enabled. + action: The action to take on malicious attachments (Allow, Block, Replace, DynamicDelivery). + quarantine_tag: The quarantine policy applied to detected messages. + redirect: Whether to redirect messages with detected attachments. + redirect_address: The email address to redirect messages to. + is_built_in_protection: Whether this is the Built-in Protection Policy. + """ + + name: str + identity: str + enable: bool + action: str + quarantine_tag: str + redirect: bool + redirect_address: str + is_built_in_protection: bool = False + + +class SafeAttachmentsRule(BaseModel): + """ + Data model for Safe Attachments rule settings. + + Attributes: + state: The state of the rule (Enabled/Disabled). + priority: The priority of the rule (0 is highest). + users: List of users the rule applies to. + groups: List of groups the rule applies to. + domains: List of domains the rule applies to. + """ + + state: str + priority: int + users: Optional[list[str]] + groups: Optional[list[str]] + domains: Optional[list[str]] + + +class AdvancedThreatProtectionPolicy(BaseModel): + """ + Model for Advanced Threat Protection policy. + + Attributes: + identity: The identity of the ATP policy. + enable_atp_for_spo_teams_odb: Whether Safe Attachments is enabled for + SharePoint, OneDrive, and Microsoft Teams. + enable_safe_docs: Whether Safe Documents is enabled for clients in Protected View. + allow_safe_docs_open: Whether users can click through Protected View + even if Safe Documents identifies the file as malicious. + """ + + identity: str + enable_atp_for_spo_teams_odb: bool + enable_safe_docs: bool + allow_safe_docs_open: bool + + +class SafeLinksPolicy(BaseModel): + """Model for Defender Safe Links Policy configuration.""" + + name: str + identity: str + enable_safe_links_for_email: bool + enable_safe_links_for_teams: bool + enable_safe_links_for_office: bool + track_clicks: bool + allow_click_through: bool + scan_urls: bool + enable_for_internal_senders: bool + deliver_message_after_scan: bool + disable_url_rewrite: bool + is_built_in_protection: bool + is_default: bool + + +class SafeLinksRule(BaseModel): + """Model for Defender Safe Links Rule configuration.""" + + state: str + priority: int + users: Optional[list[str]] + groups: Optional[list[str]] + domains: Optional[list[str]] + + class TeamsProtectionPolicy(BaseModel): """Model for Teams protection policy settings including ZAP configuration.""" diff --git a/prowler/providers/openstack/openstack_provider.py b/prowler/providers/openstack/openstack_provider.py index fd8e2e005a..d892dc1abb 100644 --- a/prowler/providers/openstack/openstack_provider.py +++ b/prowler/providers/openstack/openstack_provider.py @@ -6,6 +6,7 @@ from colorama import Fore, Style from openstack import config, connect from openstack import exceptions as openstack_exceptions from openstack.connection import Connection as OpenStackConnection +from yaml import YAMLError, safe_load from prowler.config.config import ( default_config_file_path, @@ -49,6 +50,7 @@ class OpenstackProvider(Provider): def __init__( self, clouds_yaml_file: Optional[str] = None, + clouds_yaml_content: Optional[str] = None, clouds_yaml_cloud: Optional[str] = None, auth_url: Optional[str] = None, identity_api_version: Optional[str] = None, @@ -68,6 +70,7 @@ class OpenstackProvider(Provider): self._session = self.setup_session( clouds_yaml_file=clouds_yaml_file, + clouds_yaml_content=clouds_yaml_content, clouds_yaml_cloud=clouds_yaml_cloud, auth_url=auth_url, identity_api_version=identity_api_version, @@ -132,6 +135,7 @@ class OpenstackProvider(Provider): @staticmethod def setup_session( clouds_yaml_file: Optional[str] = None, + clouds_yaml_content: Optional[str] = None, clouds_yaml_cloud: Optional[str] = None, auth_url: Optional[str] = None, identity_api_version: Optional[str] = None, @@ -145,10 +149,16 @@ class OpenstackProvider(Provider): """Collect authentication information from clouds.yaml, explicit parameters, or environment variables. Authentication priority: - 1. clouds.yaml file (if clouds_yaml_file or clouds_yaml_cloud provided) + 1. clouds.yaml content/file (if clouds_yaml_content, clouds_yaml_file, or clouds_yaml_cloud provided) 2. Explicit parameters + environment variable fallback """ # Priority 1: clouds.yaml authentication + if clouds_yaml_content: + logger.info("Using clouds.yaml content string for authentication") + return OpenstackProvider._setup_session_from_clouds_yaml_content( + clouds_yaml_content=clouds_yaml_content, + clouds_yaml_cloud=clouds_yaml_cloud, + ) if clouds_yaml_file or clouds_yaml_cloud: logger.info("Using clouds.yaml configuration for authentication") return OpenstackProvider._setup_session_from_clouds_yaml( @@ -203,6 +213,73 @@ class OpenstackProvider(Provider): project_domain_name=resolved_project_domain, ) + @staticmethod + def _setup_session_from_clouds_yaml_content( + clouds_yaml_content: str, + clouds_yaml_cloud: Optional[str] = None, + ) -> OpenStackSession: + """Setup session from clouds.yaml content provided as a string. + + Parses the YAML content directly instead of writing to a temporary file, + following the same pattern as KubernetesProvider.setup_session(). + + Args: + clouds_yaml_content: The full YAML content of a clouds.yaml file. + clouds_yaml_cloud: Cloud name to use from the clouds.yaml content. + + Returns: + OpenStackSession configured from the provided clouds.yaml content. + + Raises: + OpenStackInvalidConfigError: If the YAML is malformed or missing required fields. + OpenStackCloudNotFoundError: If the specified cloud is not found in the content. + """ + if not clouds_yaml_cloud: + raise OpenStackInvalidConfigError( + message="Cloud name (--clouds-yaml-cloud) is required when using clouds.yaml content", + ) + + try: + parsed = safe_load(clouds_yaml_content) + except YAMLError as error: + raise OpenStackInvalidConfigError( + original_exception=error, + message=f"Failed to parse clouds.yaml content: {error}", + ) + + if not isinstance(parsed, dict) or "clouds" not in parsed: + raise OpenStackInvalidConfigError( + message="Invalid clouds.yaml content: missing 'clouds' key", + ) + + cloud_config = parsed["clouds"].get(clouds_yaml_cloud) + if not cloud_config: + raise OpenStackCloudNotFoundError( + message=f"Cloud '{clouds_yaml_cloud}' not found in clouds.yaml content", + ) + + auth_dict = cloud_config.get("auth", {}) + + required_fields = ["auth_url", "username", "password"] + missing_fields = [ + field for field in required_fields if not auth_dict.get(field) + ] + if missing_fields: + raise OpenStackInvalidConfigError( + message=f"Missing required fields in clouds.yaml for cloud '{clouds_yaml_cloud}': {', '.join(missing_fields)}", + ) + + return OpenStackSession( + auth_url=auth_dict.get("auth_url"), + identity_api_version=str(cloud_config.get("identity_api_version", "3")), + username=auth_dict.get("username"), + password=auth_dict.get("password"), + project_id=auth_dict.get("project_id") or auth_dict.get("project_name"), + region_name=cloud_config.get("region_name"), + user_domain_name=auth_dict.get("user_domain_name", "Default"), + project_domain_name=auth_dict.get("project_domain_name", "Default"), + ) + @staticmethod def _setup_session_from_clouds_yaml( clouds_yaml_file: Optional[str] = None, @@ -394,6 +471,7 @@ class OpenstackProvider(Provider): @staticmethod def test_connection( clouds_yaml_file: Optional[str] = None, + clouds_yaml_content: Optional[str] = None, clouds_yaml_cloud: Optional[str] = None, auth_url: Optional[str] = None, identity_api_version: Optional[str] = None, @@ -412,6 +490,7 @@ class OpenstackProvider(Provider): Args: clouds_yaml_file: Path to clouds.yaml configuration file + clouds_yaml_content: The full content of a clouds.yaml file as a string clouds_yaml_cloud: Cloud name from clouds.yaml to use auth_url: OpenStack Keystone authentication URL identity_api_version: Keystone API version (default: "3") @@ -456,6 +535,7 @@ class OpenstackProvider(Provider): # Setup session with provided credentials session = OpenstackProvider.setup_session( clouds_yaml_file=clouds_yaml_file, + clouds_yaml_content=clouds_yaml_content, clouds_yaml_cloud=clouds_yaml_cloud, auth_url=auth_url, identity_api_version=identity_api_version, diff --git a/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled.metadata.json new file mode 100644 index 0000000000..98142e24d1 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_config_drive_enabled", + "CheckTitle": "Compute instances have config drive enabled", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instances** (VMs) are evaluated to verify that **config drive** is enabled. Config drive provides metadata and user data via a virtual CD-ROM device instead of the metadata service (169.254.169.254). This improves security by eliminating network-based metadata access, which can be vulnerable to SSRF attacks and metadata service exploitation.", + "Risk": "Instances without config drive rely on the metadata service (169.254.169.254), vulnerable to SSRF attacks that extract credentials and SSH keys. Metadata service is vulnerable to spoofing in compromised networks and can become unavailable. Config drive eliminates this attack surface by providing metadata via virtual CD-ROM, removing dependency on network-accessible metadata.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/nova/queens/user/config-drive.html", + "https://cloudinit.readthedocs.io/en/latest/reference/datasources/configdrive.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack server show -c config_drive\nopenstack server image create --name backup-snapshot\nopenstack server rebuild --image --config-drive true", + "NativeIaC": "", + "Other": "1. Navigate to **Compute > Instances > Launch Instance**\n2. In **Configuration** or **Advanced Options** tab, enable **Config Drive**\n3. Complete instance launch\n\nNote: Config drive cannot be added to existing instances without rebuild", + "Terraform": "```hcl\n# Terraform: enable config drive for compute instance\nresource \"openstack_compute_instance_v2\" \"instance\" {\n name = \"secure-instance\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n security_groups = [\"default\"]\n # Enable config drive for secure metadata injection\n config_drive = true\n\n network {\n name = var.network_name\n }\n\n # Optional: cloud-init will automatically use config drive if available\n user_data = <<-EOF\n #cloud-config\n datasource_list: [ConfigDrive, OpenStack]\n EOF\n}\n```" + }, + "Recommendation": { + "Text": "Enable config drive on all instances to eliminate metadata service attack surface. Config drive provides metadata via virtual CD-ROM instead of network-accessible service (169.254.169.254), preventing SSRF attacks. Combine with firewall rules blocking metadata service access from applications. Use config drive in air-gapped environments where metadata service is unavailable.", + "Url": "https://hub.prowler.com/check/compute_instance_config_drive_enabled" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Config drive must be enabled at instance creation time and cannot be added to existing instances without rebuild. Some OpenStack deployments may not support config drive (e.g., bare metal provisioning). Config drive is read-only from the guest OS perspective." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled.py b/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled.py new file mode 100644 index 0000000000..f15936f6b6 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled.py @@ -0,0 +1,24 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.compute.compute_client import compute_client + + +class compute_instance_config_drive_enabled(Check): + """Ensure compute instances have config drive enabled for secure metadata injection.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + if instance.has_config_drive: + report.status = "PASS" + report.status_extended = f"Instance {instance.name} ({instance.id}) has config drive enabled for secure metadata injection." + else: + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) does not have config drive enabled (relies on metadata service)." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network.metadata.json new file mode 100644 index 0000000000..c9334f01f0 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_isolated_private_network", + "CheckTitle": "Compute instances are isolated in private networks", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instances** (VMs) are evaluated to verify **network isolation** by ensuring they have private IP addresses without mixed public/private exposure. Proper network segmentation requires instances to be deployed in private networks and accessed via controlled entry points (bastion hosts, VPN, load balancers) rather than direct public exposure.", + "Risk": "Instances with mixed public/private exposure or only public IPs lack network isolation, allowing unauthorized internet access that bypasses segmentation controls. Attackers can pivot from compromised public instances to internal infrastructure for lateral movement. Flat topology exposes internal services to internet attacks including DDoS and exploit attempts.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/neutron/latest/admin/intro-os-networking.html", + "https://docs.openstack.org/security-guide/networking/architecture.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack server remove floating ip ", + "NativeIaC": "", + "Other": "1. Navigate to **Compute > Instances**\n2. Select instance with public IP\n3. Click **Actions > Disassociate Floating IP**\n4. Confirm disassociation\n5. Access instance via bastion host or VPN instead", + "Terraform": "```hcl\n# Terraform: deploy instance in isolated private network\n\n# Private network (no external gateway)\nresource \"openstack_networking_network_v2\" \"private\" {\n name = \"private-network\"\n admin_state_up = true\n}\n\nresource \"openstack_networking_subnet_v2\" \"private_subnet\" {\n name = \"private-subnet\"\n network_id = openstack_networking_network_v2.private.id\n cidr = \"10.0.1.0/24\"\n ip_version = 4\n # No gateway_ip means no external routing\n}\n\n# Instance in private network (no public IP)\nresource \"openstack_compute_instance_v2\" \"app_server\" {\n name = \"app-server\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n security_groups = [\"app-tier-sg\"]\n\n # Attach to private network ONLY\n network {\n uuid = openstack_networking_network_v2.private.id\n }\n\n # Do NOT associate floating IP\n}\n\n# Security group for app tier (least privilege)\nresource \"openstack_networking_secgroup_v2\" \"app_tier_sg\" {\n name = \"app-tier-sg\"\n description = \"App tier security group - private network only\"\n}\n\n# Allow SSH from bastion host only (not internet)\nresource \"openstack_networking_secgroup_rule_v2\" \"app_ssh_from_bastion\" {\n direction = \"ingress\"\n ethertype = \"IPv4\"\n protocol = \"tcp\"\n port_range_min = 22\n port_range_max = 22\n remote_ip_prefix = var.bastion_private_ip # e.g., 10.0.0.5/32\n security_group_id = openstack_networking_secgroup_v2.app_tier_sg.id\n}\n\n# Allow app traffic from web tier only\nresource \"openstack_networking_secgroup_rule_v2\" \"app_backend\" {\n direction = \"ingress\"\n ethertype = \"IPv4\"\n protocol = \"tcp\"\n port_range_min = 8080\n port_range_max = 8080\n remote_ip_prefix = var.web_tier_cidr # e.g., 10.0.2.0/24\n security_group_id = openstack_networking_secgroup_v2.app_tier_sg.id\n}\n```" + }, + "Recommendation": { + "Text": "Deploy instances in private networks (RFC1918) with tiered architecture: web tier with public IPs, app/database tiers private only. Never mix public and private IPs on same instance. Use bastion hosts or VPN for operator access, security groups for least-privilege policies, and NAT gateways for outbound access. Enable flow logs to detect lateral movement attempts.", + "Url": "https://hub.prowler.com/check/compute_instance_isolated_private_network" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check flags instances with mixed public/private IPs or only public IPs as not properly isolated. Some architectures legitimately require dual-homed instances (e.g., NAT gateways, VPN endpoints). Review findings in context. Instances without any IPs are also flagged as they lack network configuration." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network.py b/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network.py new file mode 100644 index 0000000000..6d8b1a6500 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network.py @@ -0,0 +1,63 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.compute.compute_client import compute_client +from prowler.providers.openstack.services.compute.lib.ip import is_public_ip + + +class compute_instance_isolated_private_network(Check): + """Ensure compute instances are isolated in private networks without mixed public/private exposure.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + + private_ip_list = [] + public_ip_list = [] + + # Classify IPs from networks dict using actual IP validation + for ip_list in instance.networks.values(): + for ip in ip_list: + if is_public_ip(ip): + public_ip_list.append(ip) + else: + private_ip_list.append(ip) + + # Also check SDK fields for IPs not present in networks + seen_ips = set(private_ip_list + public_ip_list) + for ip in [ + instance.public_v4, + instance.public_v6, + instance.access_ipv4, + instance.access_ipv6, + ]: + if ip and ip not in seen_ips and is_public_ip(ip): + public_ip_list.append(ip) + for ip in [instance.private_v4, instance.private_v6]: + if ip and ip not in seen_ips and not is_public_ip(ip): + private_ip_list.append(ip) + + has_private_ips = bool(private_ip_list) + has_public_ips = bool(public_ip_list) + + # Determine status based on IP classification + if has_private_ips and not has_public_ips: + report.status = "PASS" + ip_display = ", ".join(private_ip_list) + report.status_extended = f"Instance {instance.name} ({instance.id}) is properly isolated in private network with private IPs ({ip_display}) and no public exposure." + elif has_public_ips and has_private_ips: + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) has mixed public and private network exposure (not properly isolated)." + elif has_public_ips and not has_private_ips: + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) has only public IP addresses (no private network isolation)." + else: + # No IPs at all (edge case) + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) has no network configuration (no IPs assigned)." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication.metadata.json new file mode 100644 index 0000000000..651676ee88 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_key_based_authentication", + "CheckTitle": "Compute instances use SSH key-based authentication", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instances** (VMs) are evaluated to verify that **SSH key-based authentication** is configured by checking for an assigned keypair. Password-based authentication is vulnerable to brute-force attacks, credential stuffing, and phishing. SSH keys provide cryptographic authentication resistant to these attacks.", + "Risk": "Instances without SSH key-based authentication are vulnerable to brute-force password attacks, credential stuffing, and password reuse. Attackers can test common passwords, intercept credentials, or exploit leaked passwords from other breaches. Successful SSH access enables malware injection, lateral movement, privilege escalation, and data exfiltration.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/security-guide/compute/hardening-the-virtualization-layers.html", + "https://docs.openstack.org/api-ref/compute/#keypairs-keypairs" + ], + "Remediation": { + "Code": { + "CLI": "chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys\nsudo systemctl restart sshd\nssh -i ~/.ssh/private_key username@instance_ip", + "NativeIaC": "", + "Other": "1. Navigate to **Compute > Instances > Launch Instance**\n2. In **Key Pair** tab, select existing keypair or create new one\n3. Launch instance with keypair attached", + "Terraform": "```hcl\n# Terraform: ensure compute instance uses SSH key-based authentication\n\n# Create or import keypair\nresource \"openstack_compute_keypair_v2\" \"keypair\" {\n name = \"production-keypair\"\n public_key = file(\"~/.ssh/id_rsa.pub\")\n}\n\nresource \"openstack_compute_instance_v2\" \"instance\" {\n name = \"secure-instance\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n # Critical: attach keypair for SSH key-based authentication\n key_pair = openstack_compute_keypair_v2.keypair.name\n security_groups = [\"default\"]\n\n network {\n name = var.network_name\n }\n\n # Optional: use cloud-init to enforce key-only authentication\n user_data = <<-EOF\n #cloud-config\n ssh_pwauth: false\n disable_root: true\n EOF\n}\n```" + }, + "Recommendation": { + "Text": "Always use SSH keys instead of passwords for instance authentication. Generate keys with strong algorithms (RSA 4096-bit, Ed25519). Protect private keys with passphrases and store securely. Disable password authentication in sshd_config. Rotate keypairs periodically and revoke old keys. Use bastion hosts or VPN for SSH access instead of direct internet exposure.", + "Url": "https://hub.prowler.com/check/compute_instance_key_based_authentication" + } + }, + "Categories": [ + "identity-access", + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check verifies keypair assignment via OpenStack metadata. It does not validate authorized_keys file contents or SSH daemon configuration inside the instance. Manual verification may be needed for instances launched without keypairs but later configured with keys." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication.py b/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication.py new file mode 100644 index 0000000000..10a1efb0a1 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication.py @@ -0,0 +1,24 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.compute.compute_client import compute_client + + +class compute_instance_key_based_authentication(Check): + """Ensure compute instances use SSH key-based authentication instead of passwords.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + if instance.key_name: + report.status = "PASS" + report.status_extended = f"Instance {instance.name} ({instance.id}) is configured with SSH key-based authentication (keypair: {instance.key_name})." + else: + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) does not have SSH key-based authentication configured (no keypair assigned)." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled.metadata.json new file mode 100644 index 0000000000..89766f6214 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_locked_status_enabled", + "CheckTitle": "Compute instances have locked status enabled", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instances** (VMs) are evaluated to verify that **locked status** is enabled. Locking an instance prevents unauthorized administrative operations (delete, resize, rebuild, etc.) without first unlocking it. This provides an additional layer of protection against accidental or malicious modifications.", + "Risk": "Instances without locked status can be subjected to unauthorized operations (deletion, resize, rebuild) by compromised accounts without additional barriers. Attackers can manipulate unlocked instances to destroy forensic evidence or disrupt production workloads. Accidental termination by operators also poses risk due to lack of change control barriers.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/api-ref/compute/#lock-server-lock", + "https://docs.openstack.org/security-guide/compute.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack server lock --reason \"Production instance - requires approval\"\nopenstack server show ", + "NativeIaC": "", + "Other": "1. Navigate to **Compute > Instances**\n2. Select the instance to protect\n3. Use CLI or API to lock the instance\n4. Verify locked status is enabled", + "Terraform": "```hcl\n# Note: Terraform openstack_compute_instance_v2 does not support locked status natively\n# Use null_resource with local-exec provisioner as workaround\n\nresource \"openstack_compute_instance_v2\" \"instance\" {\n name = \"production-instance\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n security_groups = [\"default\"]\n\n network {\n name = var.network_name\n }\n}\n\nresource \"null_resource\" \"lock_instance\" {\n depends_on = [openstack_compute_instance_v2.instance]\n\n provisioner \"local-exec\" {\n command = \"openstack server lock ${openstack_compute_instance_v2.instance.id}\"\n }\n}\n```" + }, + "Recommendation": { + "Text": "Lock production instances to prevent accidental or unauthorized modifications. Use --reason parameter to document lock purpose. Implement approval workflows requiring consent before unlocking. Apply locks to critical infrastructure (databases, authentication, logging). Use RBAC policies to restrict who can lock/unlock. Audit lock/unlock operations via OpenStack logs.", + "Url": "https://hub.prowler.com/check/compute_instance_locked_status_enabled" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Locked status prevents administrative operations like delete, resize, rebuild, and shelve. It does not prevent start/stop/reboot operations or guest OS-level changes. Lock operations require specific Nova API permissions." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled.py b/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled.py new file mode 100644 index 0000000000..b16dd4e4c2 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled.py @@ -0,0 +1,29 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.compute.compute_client import compute_client + + +class compute_instance_locked_status_enabled(Check): + """Ensure compute instances have locked status enabled to prevent unauthorized operations.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + if instance.is_locked: + report.status = "PASS" + reason = ( + f" (reason: {instance.locked_reason})" + if instance.locked_reason + else "" + ) + report.status_extended = f"Instance {instance.name} ({instance.id}) has locked status enabled{reason}." + else: + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) does not have locked status enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data.metadata.json new file mode 100644 index 0000000000..f5e3b95758 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_metadata_sensitive_data", + "CheckTitle": "Compute instance metadata does not contain sensitive data", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instance metadata** is evaluated to detect **sensitive data** such as passwords, API keys, secrets, and private keys. Instance metadata is accessible via the metadata service (169.254.169.254) to any process inside the instance. Storing secrets in metadata exposes them to SSRF attacks, compromised applications, and unauthorized access.", + "Risk": "Instance metadata containing sensitive data exposes credentials through the metadata service (169.254.169.254), accessible to any process inside the instance. Attackers exploiting SSRF, compromised applications, or insider threats can extract passwords, API keys, and private keys. Stolen credentials enable unauthorized modifications, privilege escalation, resource deletion, and cryptomining.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/nova/latest/user/metadata.html", + "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack server unset --property ", + "NativeIaC": "", + "Other": "1. Navigate to **Compute > Instances**\n2. Select instance with sensitive metadata\n3. Remove sensitive metadata keys using CLI command\n4. Rotate exposed credentials immediately\n5. Store secrets in Barbican or external secrets manager instead", + "Terraform": "```hcl\n# Terraform: use Barbican for secrets instead of metadata\n\n# Store secret in Barbican\nresource \"openstack_keymanager_secret_v1\" \"db_password\" {\n name = \"database-password\"\n payload = random_password.db_password.result\n payload_content_type = \"text/plain\"\n secret_type = \"passphrase\"\n}\n\nresource \"random_password\" \"db_password\" {\n length = 32\n special = true\n}\n\n# Instance WITHOUT sensitive data in metadata\nresource \"openstack_compute_instance_v2\" \"secure_instance\" {\n name = \"app-server\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n security_groups = [\"app-sg\"]\n\n # Safe metadata (non-sensitive labels only)\n metadata = {\n environment = \"production\"\n application = \"web-app\"\n cost_center = \"engineering\"\n barbican_secret_id = openstack_keymanager_secret_v1.db_password.secret_ref\n }\n\n network {\n name = \"private-network\"\n }\n\n # Use cloud-init to retrieve secrets from Barbican\n user_data = <<-EOF\n #!/bin/bash\n # Retrieve database password from Barbican\n SECRET_REF=\"${openstack_keymanager_secret_v1.db_password.secret_ref}\"\n DB_PASSWORD=$(openstack secret get \"$SECRET_REF\" --payload -f value)\n \n # Configure application with retrieved secret\n echo \"DATABASE_URL=://user:$DB_PASSWORD@db-host/dbname\" > /etc/app/config.env\n chmod 600 /etc/app/config.env\n EOF\n}\n\n# ANTI-PATTERN: DO NOT DO THIS\n# resource \"openstack_compute_instance_v2\" \"insecure_instance\" {\n# metadata = {\n# db_password = \"hardcoded-secret-123\" # NEVER store secrets in metadata\n# api_key = \"sk-1234567890abcdef\" # Exposed via metadata service\n# }\n# }\n```" + }, + "Recommendation": { + "Text": "Never store secrets in metadata; use Barbican (OpenStack Key Manager), Vault, or external secrets management instead. Retrieve secrets at runtime via APIs. Implement least privilege access to secrets with RBAC. Enable secrets audit logging. Use envelope encryption for secrets at rest. Implement automatic rotation every 90 days. Scan metadata for hardcoded secrets using tools like TruffleHog.", + "Url": "https://hub.prowler.com/check/compute_instance_metadata_sensitive_data" + } + }, + "Categories": [ + "secrets", + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check uses the detect-secrets library to scan for credentials. May produce false positives on metadata keys containing secret-like keywords. Findings should be reviewed manually. The audit_config allows configuring secrets_ignore_patterns to exclude specific patterns and detect_secrets_plugins to customize detection. Metadata is world-readable within instance via 169.254.169.254." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data.py b/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data.py new file mode 100644 index 0000000000..5df151c939 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data.py @@ -0,0 +1,58 @@ +import json +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.lib.utils.utils import detect_secrets_scan +from prowler.providers.openstack.services.compute.compute_client import compute_client + + +class compute_instance_metadata_sensitive_data(Check): + """Ensure compute instance metadata does not contain sensitive data like passwords or API keys.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + secrets_ignore_patterns = compute_client.audit_config.get( + "secrets_ignore_patterns", [] + ) + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + report.status = "PASS" + report.status_extended = f"Instance {instance.name} ({instance.id}) metadata does not contain sensitive data." + + if instance.metadata: + # Build metadata dict and parallel list of keys (similar to AWS ECS pattern) + dump_metadata = {} + original_metadata_keys = [] + for key, value in instance.metadata.items(): + dump_metadata[key] = value + original_metadata_keys.append(key) + + # Convert metadata dict to JSON string for detect-secrets scanning + metadata_json = json.dumps(dump_metadata, indent=2) + detect_secrets_output = detect_secrets_scan( + data=metadata_json, + excluded_secrets=secrets_ignore_patterns, + detect_secrets_plugins=compute_client.audit_config.get( + "detect_secrets_plugins" + ), + ) + + if detect_secrets_output: + # Map line numbers back to metadata keys using the parallel list + # Line numbering: line 1 = "{", line 2 = first key-value, etc. + secrets_string = ", ".join( + [ + f"{secret['type']} in metadata key '{original_metadata_keys[secret['line_number'] - 2]}'" + for secret in detect_secrets_output + if secret["line_number"] - 2 < len(original_metadata_keys) + ] + ) + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) metadata contains potential secrets -> {secrets_string}." + else: + report.status_extended = f"Instance {instance.name} ({instance.id}) has no metadata (no sensitive data exposure risk)." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed.metadata.json new file mode 100644 index 0000000000..c6eca07451 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_public_ip_exposed", + "CheckTitle": "Compute instances are not exposed to the internet", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instances** are evaluated to verify they are **not exposed to the internet** via public IPs (floating IPs or access IPs). Instances with public IPs are directly reachable from the internet, increasing attack surface. Best practices recommend using **bastion hosts**, **VPN gateways**, or **load balancers** instead.", + "Risk": "Instances with public IPs are directly reachable from the internet, enabling reconnaissance, port scanning, and vulnerability exploitation. Attackers can target instances for brute-force attacks, credential stuffing, and malware injection. Public exposure bypasses network segmentation and defense-in-depth. Compromised public instances become pivot points for lateral movement.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/neutron/latest/admin/intro-nat.html", + "https://docs.openstack.org/security-guide/networking/architecture.html", + "https://docs.openstack.org/neutron/latest/admin/intro-os-networking.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack server remove floating ip \nssh -J bastion private-instance", + "NativeIaC": "", + "Other": "1. Navigate to **Compute > Instances**\n2. Select instance with public IP\n3. Click **Actions > Disassociate Floating IP**\n4. Confirm disassociation\n5. Access via bastion host or VPN instead", + "Terraform": "```hcl\n# Terraform: deploy instance without public IP (private network only)\n\nresource \"openstack_compute_instance_v2\" \"private_instance\" {\n name = \"private-instance\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n security_groups = [\"internal-sg\"]\n\n # Attach to private network ONLY (no floating IP)\n network {\n name = \"private-network\"\n }\n\n # Do NOT create floating IP association\n}\n\n# Bastion host for SSH access (only instance with public IP)\nresource \"openstack_compute_instance_v2\" \"bastion\" {\n name = \"bastion-host\"\n image_id = var.bastion_image_id\n flavor_id = \"small\"\n key_pair = var.bastion_keypair\n security_groups = [\"bastion-sg\"] # Restrict SSH to corporate IPs only\n\n network {\n name = \"dmz-network\"\n }\n}\n\nresource \"openstack_networking_floatingip_v2\" \"bastion_fip\" {\n pool = \"public\"\n}\n\nresource \"openstack_compute_floatingip_associate_v2\" \"bastion_fip_assoc\" {\n floating_ip = openstack_networking_floatingip_v2.bastion_fip.address\n instance_id = openstack_compute_instance_v2.bastion.id\n}\n\n# Security group for bastion (least privilege)\nresource \"openstack_networking_secgroup_v2\" \"bastion_sg\" {\n name = \"bastion-sg\"\n description = \"Allow SSH from corporate IPs only\"\n}\n\nresource \"openstack_networking_secgroup_rule_v2\" \"bastion_ssh\" {\n direction = \"ingress\"\n ethertype = \"IPv4\"\n protocol = \"tcp\"\n port_range_min = 22\n port_range_max = 22\n remote_ip_prefix = var.corporate_cidr # e.g., 203.0.113.0/24\n security_group_id = openstack_networking_secgroup_v2.bastion_sg.id\n}\n```" + }, + "Recommendation": { + "Text": "Avoid public IPs on application/database instances; use private networks only. Deploy bastion hosts or VPN gateways for operator access. Use load balancers (Octavia, HAProxy) with public IPs for web traffic instead of direct instance exposure. Apply least-privilege security groups. Enable flow logs. Use cloud NAT gateways for outbound access without inbound exposure.", + "Url": "https://hub.prowler.com/check/compute_instance_public_ip_exposed" + } + }, + "Categories": [ + "internet-exposed", + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check flags instances with any public IP (floating IP, access IP v4/v6). Some workloads legitimately require public IPs (bastion hosts, NAT gateways, public-facing load balancers). Review findings in context of architecture requirements. Private RFC1918 IPs (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) are not flagged." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed.py b/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed.py new file mode 100644 index 0000000000..dfc37075cf --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed.py @@ -0,0 +1,62 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.compute.compute_client import compute_client +from prowler.providers.openstack.services.compute.lib.ip import is_public_ip + + +class compute_instance_public_ip_exposed(Check): + """Ensure compute instances are not exposed to the internet via public IP addresses.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + # Collect all potential public IP indicators + public_ips = [] + # Check SDK computed properties + if instance.public_v4: + public_ips.append(f"Public IPv4: {instance.public_v4}") + if instance.public_v6: + public_ips.append(f"Public IPv6: {instance.public_v6}") + if instance.access_ipv4: + public_ips.append(f"Access IPv4: {instance.access_ipv4}") + if instance.access_ipv6: + public_ips.append(f"Access IPv6: {instance.access_ipv6}") + + # Check networks for any additional public IPs (beyond first one captured in SDK attributes) + # This handles cases where instances have multiple public IPs on different networks + sdk_ips = { + instance.public_v4, + instance.public_v6, + instance.access_ipv4, + instance.access_ipv6, + } + for network_name, ip_list in instance.networks.items(): + for ip in ip_list: + # Check if IP is public and not already captured in SDK attributes + if is_public_ip(ip) and ip not in sdk_ips: + public_ips.append( + f"{network_name} network: {ip} (public range)" + ) + + # Remove duplicates while preserving order + seen = set() + unique_public_ips = [] + for ip in public_ips: + if ip not in seen: + seen.add(ip) + unique_public_ips.append(ip) + + if not unique_public_ips: + report.status = "PASS" + report.status_extended = f"Instance {instance.name} ({instance.id}) is not exposed to the internet (no public IP addresses or external network attachments detected)." + else: + report.status = "FAIL" + ip_list = ", ".join(unique_public_ips) + report.status_extended = f"Instance {instance.name} ({instance.id}) is exposed to the internet with public IP addresses: {ip_list}." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.metadata.json index 1301c26b1d..95ba7d46fd 100644 --- a/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.metadata.json +++ b/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.metadata.json @@ -10,23 +10,21 @@ "ResourceType": "OpenStackInstance", "ResourceGroup": "compute", "Description": "**OpenStack compute instances** (VMs) are evaluated to verify that at least one **security group** is attached. Security groups act as virtual firewalls, controlling ingress and egress traffic. Instances without security groups may have **unrestricted network access**, violating defense-in-depth principles.", - "Risk": "Compute instances without security groups are exposed to unrestricted network traffic, enabling:\n- **Confidentiality**: unauthorized access to services and data exfiltration\n- **Integrity**: injection attacks, tampering, and lateral movement across workloads\n- **Availability**: DDoS, port scanning, and resource exhaustion\nAttackers can probe open ports, exploit vulnerable services, and establish persistence without firewall barriers.", + "Risk": "Instances without security groups are exposed to unrestricted network traffic from any source. Attackers can probe open ports, exploit vulnerable services, conduct injection attacks, and tamper with data without firewall barriers. Lack of network access controls enables unauthorized access, data exfiltration, lateral movement, DDoS attacks, and resource exhaustion.", "RelatedUrl": "", "AdditionalURLs": [ "https://docs.openstack.org/nova/latest/user/security-groups.html", - "https://docs.openstack.org/api-ref/compute/", - "https://docs.openstack.org/neutron/latest/admin/intro-os-networking.html", "https://docs.openstack.org/security-guide/networking/architecture.html" ], "Remediation": { "Code": { "CLI": "openstack server add security group ", "NativeIaC": "", - "Other": "1. Access the Horizon dashboard\n2. Navigate to **Compute > Instances**\n3. Locate instances without security groups (check the Security Groups column)\n4. Select the instance, then **Actions > Edit Security Groups**\n5. Add at least one security group (e.g., `default` or a custom group with least-privilege rules)\n6. Click **Save** and verify the attachment\n7. Test connectivity to ensure proper ingress/egress rules", + "Other": "1. Navigate to **Compute > Instances**\n2. Select instance without security groups\n3. Click **Actions > Edit Security Groups**\n4. Add at least one security group\n5. Click **Save**", "Terraform": "```hcl\n# Terraform: ensure compute instance has security groups attached\nresource \"openstack_compute_instance_v2\" \"instance\" {\n name = \"example-instance\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n # Critical: attach at least one security group\n security_groups = [\"default\", \"web-sg\", \"app-sg\"]\n\n network {\n name = var.network_name\n }\n}\n```" }, "Recommendation": { - "Text": "Apply **defense-in-depth** for network security:\n- Attach **security groups** to all instances; never deploy without firewall rules\n- Follow **least privilege**: allow only required ports and sources; deny all by default\n- Use **separate security groups** per tier (web, app, database) with explicit rules\n- Avoid overly permissive rules like `0.0.0.0/0` ingress on sensitive ports\n- Implement **network segmentation** with isolated networks or VLANs\n- Enable **flow logs** for traffic analysis and anomaly detection\n- Regularly **audit** security group rules and remove stale or overly broad permissions", + "Text": "Attach security groups to all instances following least privilege principle: allow only required ports and sources, deny all by default. Use separate security groups per tier (web, app, database) with explicit rules. Avoid overly permissive rules like 0.0.0.0/0 ingress on sensitive ports. Implement network segmentation with isolated networks. Regularly audit and remove stale rules.", "Url": "https://hub.prowler.com/check/compute_instance_security_groups_attached" } }, diff --git a/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.py b/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.py index aa353883e4..70a4a0768f 100644 --- a/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.py +++ b/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.py @@ -12,23 +12,13 @@ class compute_instance_security_groups_attached(Check): for instance in compute_client.instances: report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) - report.resource_id = instance.id - report.resource_name = instance.name - report.region = instance.region - - if instance.security_groups and len(instance.security_groups) > 0: + if instance.security_groups: report.status = "PASS" sg_names = ", ".join(instance.security_groups) - report.status_extended = ( - f"Instance {instance.name} ({instance.id}) has security groups " - f"attached: {sg_names}." - ) + report.status_extended = f"Instance {instance.name} ({instance.id}) has security groups attached: {sg_names}." else: report.status = "FAIL" - report.status_extended = ( - f"Instance {instance.name} ({instance.id}) does not have any " - f"security groups attached." - ) + report.status_extended = f"Instance {instance.name} ({instance.id}) does not have any security groups attached." findings.append(report) diff --git a/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates.metadata.json new file mode 100644 index 0000000000..360dbdc759 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_trusted_image_certificates", + "CheckTitle": "Compute instances use trusted image certificates", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instances** (VMs) are evaluated to verify that **trusted image certificates** are configured. Trusted image certificates enable cryptographic validation of image signatures using Glance image signing (OpenStack Image Signature Verification). This ensures instances are launched from verified, untampered images signed by trusted authorities.", + "Risk": "Instances without trusted certificates can be launched from tampered images containing backdoors, rootkits, or malware. Attackers can inject malicious code into unsigned images, and without signature verification, Nova launches compromised images. Malicious images enable persistence, lateral movement, data exfiltration, service disruption, and cryptomining.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/glance/latest/user/signature.html", + "https://docs.openstack.org/nova/latest/admin/secure-boot.html", + "https://docs.openstack.org/api-ref/image/v2/index.html#image-signature-verification" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Enable Glance image signature verification in glance-api.conf\n2. Install Barbican for certificate storage\n3. Sign images using glance-manage or GPG tooling\n4. Upload certificates to Barbican\n5. Launch instances with trusted-image-certificate-id parameter\n\nNote: This requires administrator privileges and Glance/Nova configuration", + "Terraform": "```hcl\n# Terraform: launch instance with trusted image certificates\n\n# Note: Requires Glance image with signature metadata and Barbican certificate\n\nresource \"openstack_compute_instance_v2\" \"instance\" {\n name = \"secure-instance\"\n image_id = var.signed_image_id # Must be signed image\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n security_groups = [\"default\"]\n\n # Specify trusted certificate IDs for signature validation\n # Note: This requires Nova microversion 2.63+ support\n trusted_image_certificates = [\n var.image_signing_cert_id,\n var.backup_cert_id\n ]\n\n network {\n name = var.network_name\n }\n}\n\n# Ensure image has signature metadata\ndata \"openstack_images_image_v2\" \"signed_image\" {\n name = \"ubuntu-22.04-signed\"\n most_recent = true\n\n # Verify signature properties exist\n properties = {\n img_signature = \"required\"\n img_signature_hash_method = \"SHA-256\"\n img_signature_key_type = \"RSA-PSS\"\n img_signature_certificate_uuid = \"required\"\n }\n}\n```" + }, + "Recommendation": { + "Text": "Sign all images using GPG or X.509 certificates before uploading to Glance. Use Barbican to store signing certificates securely. Enforce trusted_image_certificates for production instances via policy. Enable signature verification in Nova (verify_glance_signatures=True). Restrict image upload permissions to authorized CI/CD pipelines. Implement certificate rotation every 12 months.", + "Url": "https://hub.prowler.com/check/compute_instance_trusted_image_certificates" + } + }, + "Categories": [ + "trust-boundaries", + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Trusted image certificates require OpenStack Glance image signature verification (microversion 2.63+), Barbican for certificate storage, and Nova configured to verify signatures. Many OpenStack deployments do not enable this feature by default. This check may produce false positives for clouds not using image signing." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates.py b/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates.py new file mode 100644 index 0000000000..60dab86a24 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates.py @@ -0,0 +1,25 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.compute.compute_client import compute_client + + +class compute_instance_trusted_image_certificates(Check): + """Ensure compute instances use trusted image certificates for image signature validation.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + if instance.trusted_image_certificates: + report.status = "PASS" + cert_ids = ", ".join(instance.trusted_image_certificates) + report.status_extended = f"Instance {instance.name} ({instance.id}) uses trusted image certificates: {cert_ids}." + else: + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) does not use trusted image certificates (image signature validation not enforced)." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_service.py b/prowler/providers/openstack/services/compute/compute_service.py index 8a372151ef..65233930b4 100644 --- a/prowler/providers/openstack/services/compute/compute_service.py +++ b/prowler/providers/openstack/services/compute/compute_service.py @@ -1,7 +1,8 @@ from __future__ import annotations +import ipaddress from dataclasses import dataclass -from typing import List +from typing import Dict, List from openstack import exceptions as openstack_exceptions @@ -27,8 +28,72 @@ class Compute(OpenStackService): sg_list = getattr(server, "security_groups", None) or [] security_groups = [sg.get("name", "") for sg in sg_list] + # Extract network information from addresses + networks_dict = {} + addresses_attr = getattr(server, "addresses", None) + if addresses_attr: + for net_name, addr_list in addresses_attr.items(): + # addr_list is a list of dicts like: + # [{'version': 4, 'addr': '57.128.163.151', 'OS-EXT-IPS:type': 'fixed'}] + ip_list = [] + if isinstance(addr_list, list): + for addr_dict in addr_list: + if isinstance(addr_dict, dict) and "addr" in addr_dict: + ip_list.append(addr_dict["addr"]) + elif isinstance(addr_dict, str): + # Fallback: if it's just a string IP + ip_list.append(addr_dict) + elif isinstance(addr_list, str): + # Fallback: single string IP + ip_list = [addr_list] + networks_dict[net_name] = ip_list + + # Extract trusted image certificates + trusted_certs = ( + getattr(server, "trusted_image_certificates", None) or [] + ) + + # Get SDK computed properties + public_v4 = getattr(server, "public_v4", "") + public_v6 = getattr(server, "public_v6", "") + private_v4 = getattr(server, "private_v4", "") + private_v6 = getattr(server, "private_v6", "") + + # Fallback: If SDK attributes are not populated, classify IPs from networks + # This handles clouds where SDK computed properties are not available + if ( + not (public_v4 or public_v6 or private_v4 or private_v6) + and networks_dict + ): + for network_name, ip_list in networks_dict.items(): + for ip_str in ip_list: + try: + ip_obj = ipaddress.ip_address(ip_str) + # Classify as private or public + if ip_obj.is_private: + # Assign first private IP found to appropriate field + if ip_obj.version == 4 and not private_v4: + private_v4 = ip_str + elif ip_obj.version == 6 and not private_v6: + private_v6 = ip_str + elif not ( + ip_obj.is_loopback + or ip_obj.is_link_local + or ip_obj.is_reserved + or ip_obj.is_multicast + ): + # Assign first public IP found to appropriate field + if ip_obj.version == 4 and not public_v4: + public_v4 = ip_str + elif ip_obj.version == 6 and not public_v6: + public_v6 = ip_str + except ValueError: + # Invalid IP address, skip + continue + self.instances.append( ComputeInstance( + # Basic instance information id=getattr(server, "id", ""), name=getattr(server, "name", ""), status=getattr(server, "status", ""), @@ -36,6 +101,27 @@ class Compute(OpenStackService): security_groups=security_groups, region=self.region, project_id=self.project_id, + # Access Control & Authentication + is_locked=getattr(server, "is_locked", False), + locked_reason=getattr(server, "locked_reason", ""), + key_name=getattr(server, "key_name", ""), + user_id=getattr(server, "user_id", ""), + # Network Exposure + access_ipv4=getattr(server, "access_ipv4", ""), + access_ipv6=getattr(server, "access_ipv6", ""), + public_v4=public_v4, + public_v6=public_v6, + private_v4=private_v4, + private_v6=private_v6, + networks=networks_dict, + # Configuration Security + has_config_drive=getattr(server, "has_config_drive", False), + metadata=getattr(server, "metadata", {}), + user_data=getattr(server, "user_data", ""), + # Image Trust + trusted_image_certificates=( + trusted_certs if isinstance(trusted_certs, list) else [] + ), ) ) except openstack_exceptions.SDKException as error: @@ -54,6 +140,7 @@ class Compute(OpenStackService): class ComputeInstance: """Represents an OpenStack compute instance (VM).""" + # Basic instance information id: str name: str status: str @@ -61,3 +148,26 @@ class ComputeInstance: security_groups: List[str] region: str project_id: str + + # Access Control & Authentication + is_locked: bool + locked_reason: str + key_name: str + user_id: str + + # Network Exposure + access_ipv4: str + access_ipv6: str + public_v4: str + public_v6: str + private_v4: str + private_v6: str + networks: Dict[str, List[str]] + + # Configuration Security + has_config_drive: bool + metadata: Dict[str, str] + user_data: str + + # Image Trust + trusted_image_certificates: List[str] diff --git a/prowler/providers/openstack/services/compute/lib/__init__.py b/prowler/providers/openstack/services/compute/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/lib/ip.py b/prowler/providers/openstack/services/compute/lib/ip.py new file mode 100644 index 0000000000..5f73466284 --- /dev/null +++ b/prowler/providers/openstack/services/compute/lib/ip.py @@ -0,0 +1,10 @@ +import ipaddress + + +def is_public_ip(ip_str: str) -> bool: + """Check if an IP address is public (globally routable, non-multicast).""" + try: + ip = ipaddress.ip_address(ip_str) + return ip.is_global and not ip.is_multicast + except ValueError: + return False diff --git a/prowler/providers/oraclecloud/config.py b/prowler/providers/oraclecloud/config.py index 95392e6b8e..902227e77c 100644 --- a/prowler/providers/oraclecloud/config.py +++ b/prowler/providers/oraclecloud/config.py @@ -13,42 +13,49 @@ OCI_USER_AGENT = "Prowler" # OCI Regions - Commercial Regions OCI_COMMERCIAL_REGIONS = { - "ap-chuncheon-1": "South Korea Central (Chuncheon)", - "ap-hyderabad-1": "India West (Hyderabad)", - "ap-melbourne-1": "Australia Southeast (Melbourne)", - "ap-mumbai-1": "India West (Mumbai)", - "ap-osaka-1": "Japan Central (Osaka)", - "ap-seoul-1": "South Korea North (Seoul)", - "ap-singapore-1": "Singapore (Singapore)", - "ap-sydney-1": "Australia East (Sydney)", - "ap-tokyo-1": "Japan East (Tokyo)", - "ca-montreal-1": "Canada Southeast (Montreal)", - "ca-toronto-1": "Canada Southeast (Toronto)", - "eu-amsterdam-1": "Netherlands Northwest (Amsterdam)", - "eu-frankfurt-1": "Germany Central (Frankfurt)", - "eu-madrid-1": "Spain Central (Madrid)", - "eu-marseille-1": "France South (Marseille)", - "eu-milan-1": "Italy Northwest (Milan)", - "eu-paris-1": "France Central (Paris)", - "eu-stockholm-1": "Sweden Central (Stockholm)", - "eu-zurich-1": "Switzerland North (Zurich)", - "il-jerusalem-1": "Israel Central (Jerusalem)", - "me-abudhabi-1": "UAE East (Abu Dhabi)", - "me-dubai-1": "UAE East (Dubai)", - "me-jeddah-1": "Saudi Arabia West (Jeddah)", - "mx-monterrey-1": "Mexico Northeast (Monterrey)", - "mx-queretaro-1": "Mexico Central (Queretaro)", - "sa-bogota-1": "Colombia (Bogota)", - "sa-santiago-1": "Chile (Santiago)", - "sa-saopaulo-1": "Brazil East (Sao Paulo)", - "sa-valparaiso-1": "Chile West (Valparaiso)", - "sa-vinhedo-1": "Brazil Southeast (Vinhedo)", - "uk-cardiff-1": "UK West (Cardiff)", - "uk-london-1": "UK South (London)", - "us-ashburn-1": "US East (Ashburn)", - "us-chicago-1": "US East (Chicago)", - "us-phoenix-1": "US West (Phoenix)", - "us-sanjose-1": "US West (San Jose)", + "af-johannesburg-1": "af-johannesburg-1", + "ap-batam-1": "ap-batam-1", + "ap-chuncheon-1": "ap-chuncheon-1", + "ap-hyderabad-1": "ap-hyderabad-1", + "ap-kulai-2": "ap-kulai-2", + "ap-melbourne-1": "ap-melbourne-1", + "ap-mumbai-1": "ap-mumbai-1", + "ap-osaka-1": "ap-osaka-1", + "ap-seoul-1": "ap-seoul-1", + "ap-singapore-1": "ap-singapore-1", + "ap-singapore-2": "ap-singapore-2", + "ap-sydney-1": "ap-sydney-1", + "ap-tokyo-1": "ap-tokyo-1", + "ca-montreal-1": "ca-montreal-1", + "ca-toronto-1": "ca-toronto-1", + "eu-amsterdam-1": "eu-amsterdam-1", + "eu-frankfurt-1": "eu-frankfurt-1", + "eu-madrid-1": "eu-madrid-1", + "eu-madrid-3": "eu-madrid-3", + "eu-marseille-1": "eu-marseille-1", + "eu-milan-1": "eu-milan-1", + "eu-paris-1": "eu-paris-1", + "eu-stockholm-1": "eu-stockholm-1", + "eu-turin-1": "eu-turin-1", + "eu-zurich-1": "eu-zurich-1", + "il-jerusalem-1": "il-jerusalem-1", + "me-abudhabi-1": "me-abudhabi-1", + "me-dubai-1": "me-dubai-1", + "me-jeddah-1": "me-jeddah-1", + "me-riyadh-1": "me-riyadh-1", + "mx-monterrey-1": "mx-monterrey-1", + "mx-queretaro-1": "mx-queretaro-1", + "sa-bogota-1": "sa-bogota-1", + "sa-santiago-1": "sa-santiago-1", + "sa-saopaulo-1": "sa-saopaulo-1", + "sa-valparaiso-1": "sa-valparaiso-1", + "sa-vinhedo-1": "sa-vinhedo-1", + "uk-cardiff-1": "uk-cardiff-1", + "uk-london-1": "uk-london-1", + "us-ashburn-1": "us-ashburn-1", + "us-chicago-1": "us-chicago-1", + "us-phoenix-1": "us-phoenix-1", + "us-sanjose-1": "us-sanjose-1", } # OCI Government Regions diff --git a/pyproject.toml b/pyproject.toml index 85fda311a2..8d8452ad65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,9 +44,10 @@ dependencies = [ "boto3==1.40.61", "botocore==1.40.61", "colorama==0.4.6", - "cryptography==44.0.1", + "cryptography==44.0.3", "dash==3.1.1", "dash-bootstrap-components==2.0.3", + "defusedxml>=0.7.1", "detect-secrets==1.5.0", "dulwich==0.23.0", "google-api-python-client==2.163.0", @@ -57,9 +58,9 @@ dependencies = [ "microsoft-kiota-abstractions==1.9.2", "msgraph-sdk==1.23.0", "numpy==2.0.2", - "openstacksdk==4.0.1", + "openstacksdk==4.2.0", "pandas==2.2.3", - "py-ocsf-models==0.5.0", + "py-ocsf-models==0.8.1", "pydantic (>=2.0,<3.0)", "pygithub==2.5.0", "python-dateutil (>=2.9.0.post0,<3.0.0)", diff --git a/skills/README.md b/skills/README.md index e5430a6671..1d689962af 100644 --- a/skills/README.md +++ b/skills/README.md @@ -53,6 +53,8 @@ Reusable patterns for common technologies: | `nextjs-15` | App Router, Server Actions, streaming | | `tailwind-4` | cn() utility, Tailwind 4 patterns | | `playwright` | Page Object Model, selectors | +| `vitest` | Unit testing, React Testing Library | +| `tdd` | Test-Driven Development workflow | | `pytest` | Fixtures, mocking, markers | | `django-drf` | ViewSets, Serializers, Filters | | `zod-4` | Zod 4 API patterns | diff --git a/skills/prowler-attack-paths-query/SKILL.md b/skills/prowler-attack-paths-query/SKILL.md index 9a68c8a3ea..35fb9341d3 100644 --- a/skills/prowler-attack-paths-query/SKILL.md +++ b/skills/prowler-attack-paths-query/SKILL.md @@ -18,9 +18,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, Task ## Overview -Attack Paths queries are openCypher queries that analyze cloud infrastructure graphs -(ingested via Cartography) to detect security risks like privilege escalation paths, -network exposure, and misconfigurations. +Attack Paths queries are openCypher queries that analyze cloud infrastructure graphs (ingested via Cartography) to detect security risks like privilege escalation paths, network exposure, and misconfigurations. Queries are written in **openCypher Version 9** to ensure compatibility with both Neo4j and Amazon Neptune. @@ -34,10 +32,11 @@ Queries can be created from: - The JSON index contains: `id`, `name`, `description`, `services`, `permissions`, `exploitationSteps`, `prerequisites`, etc. - Reference: https://github.com/DataDog/pathfinding.cloud - **Fetching a single path by ID** — The aggregated `paths.json` is too large for WebFetch + **Fetching a single path by ID** - The aggregated `paths.json` is too large for WebFetch (content gets truncated). Use Bash with `curl` and a JSON parser instead: Prefer `jq` (concise), fall back to `python3` (guaranteed in this Python project): + ```bash # With jq curl -s https://raw.githubusercontent.com/DataDog/pathfinding.cloud/main/docs/paths.json \ @@ -50,6 +49,7 @@ Queries can be created from: 2. **Listing Available Attack Paths** - Use Bash to list available paths from the JSON index: + ```bash # List all path IDs and names (jq) curl -s https://raw.githubusercontent.com/DataDog/pathfinding.cloud/main/docs/paths.json \ @@ -84,6 +84,7 @@ Example: `api/src/backend/api/attack_paths/queries/aws.py` ```python from api.attack_paths.queries.types import ( + AttackPathsQueryAttribution, AttackPathsQueryDefinition, AttackPathsQueryParameterDefinition, ) @@ -92,8 +93,13 @@ from tasks.jobs.attack_paths.config import PROWLER_FINDING_LABEL # {REFERENCE_ID} (e.g., EC2-001, GLUE-001) AWS_{QUERY_NAME} = AttackPathsQueryDefinition( id="aws-{kebab-case-name}", - name="Privilege Escalation: {permission1} + {permission2}", - description="{Detailed description of the Attack Paths}.", + name="{Human-friendly label} ({REFERENCE_ID})", + short_description="{Brief explanation of the attack, no technical permissions.}", + description="{Detailed description of the attack vector and impact.}", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - {REFERENCE_ID} - {permission1} + {permission2}", + link="https://pathfinding.cloud/paths/{reference_id_lowercase}", + ), provider="aws", cypher=f""" // Find principals with {permission1} @@ -114,7 +120,7 @@ AWS_{QUERY_NAME} = AttackPathsQueryDefinition( OR action = '*' ) - // Find target resources + // Find target resources (MUST chain from `aws` for provider isolation) MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: '{service}.amazonaws.com'}}) WHERE any(resource IN stmt.resource WHERE resource = '*' @@ -231,11 +237,13 @@ This informs query design by showing what data is actually available to query. Use the standard pattern (see above) with: - **id**: Auto-generated as `{provider}-{kebab-case-description}` -- **name**: Human-readable, e.g., "Privilege Escalation: {perm1} + {perm2}" -- **description**: Explain the attack vector and impact +- **name**: Short, human-friendly label. No raw IAM permissions. For sourced queries (e.g., pathfinding.cloud), append the reference ID in parentheses: `"EC2 Instance Launch with Privileged Role (EC2-001)"`. If the name already has parentheses, prepend the ID inside them: `"ECS Service Creation with Privileged Role (ECS-003 - Existing Cluster)"`. +- **short_description**: Brief explanation of the attack, no technical permissions. E.g., "Launch EC2 instances with privileged IAM roles to gain their permissions via IMDS." +- **description**: Full technical explanation of the attack vector and impact. Plain text only, no HTML or technical permissions here. - **provider**: Provider identifier (aws, azure, gcp, kubernetes, github) - **cypher**: The openCypher query with proper escaping - **parameters**: Optional list of user-provided parameters (use `parameters=[]` if none needed) +- **attribution**: Optional `AttackPathsQueryAttribution(text, link)` for sourced queries. The `text` includes the source, reference ID, and technical permissions (e.g., `"pathfinding.cloud - EC2-001 - iam:PassRole + ec2:RunInstances"`). The `link` is the URL with a lowercase ID (e.g., `"https://pathfinding.cloud/paths/ec2-001"`). Omit (defaults to `None`) for non-sourced queries. ### 5. Add Query to Provider List @@ -397,6 +405,16 @@ parameters=[ 6. **Validate schema first**: Ensure all node labels and properties exist in Cartography schema +7. **Chain all MATCHes from the root account node**: Every `MATCH` clause must connect to the `aws` variable (or another variable already bound to the account's subgraph). The tenant database contains data from multiple providers — an unanchored `MATCH` would return nodes from all providers, breaking provider isolation. + + ```cypher + // WRONG: matches ALL AWSRoles across all providers in the tenant DB + MATCH (role:AWSRole) WHERE role.name = 'admin' + + // CORRECT: scoped to the specific account's subgraph + MATCH (aws)--(role:AWSRole) WHERE role.name = 'admin' + ``` + --- ## openCypher Compatibility diff --git a/skills/prowler-changelog/SKILL.md b/skills/prowler-changelog/SKILL.md index 85a7fa4ba0..94f119e2ba 100644 --- a/skills/prowler-changelog/SKILL.md +++ b/skills/prowler-changelog/SKILL.md @@ -74,6 +74,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash - One entry per PR (can link multiple PRs for related changes) - No period at the end - Do NOT start with redundant verbs (section header already provides the action) +- **CRITICAL: Preserve section order** — when adding a new section to the UNRELEASED block, insert it in the correct position relative to existing sections (Added → Changed → Deprecated → Removed → Fixed → Security). Never append a new section at the top or bottom without checking order ### Semantic Versioning Rules @@ -177,6 +178,13 @@ This maintains chronological order within each section (oldest at top, newest at ### Bad Entries ```markdown +# BAD - Wrong section order (Fixed before Added) +### 🐞 Fixed +- Some bug fix [(#123)](...) + +### 🚀 Added +- Some new feature [(#456)](...) + - Fixed bug. # Too vague, has period - Added new feature for users # Missing PR link, redundant verb - Add search bar [(#123)] # Redundant verb (section already says "Added") diff --git a/skills/prowler-readme-table/SKILL.md b/skills/prowler-readme-table/SKILL.md new file mode 100644 index 0000000000..d6f9c6e780 --- /dev/null +++ b/skills/prowler-readme-table/SKILL.md @@ -0,0 +1,110 @@ +--- +name: prowler-readme-table +description: > + Updates the "Prowler at a Glance" table in README.md with accurate provider statistics. + Trigger: When updating README.md provider stats, checks count, services count, compliance frameworks, or categories. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" + scope: [root] + auto_invoke: + - "Updating README.md provider statistics table" + - "Updating checks, services, compliance, or categories count in README.md" +allowed-tools: Read, Edit, Bash, Glob, Grep +--- + +## When to Use + +Use this skill when updating the **Prowler at a Glance** table in the root `README.md`. This table tracks the number of checks, services, compliance frameworks, and categories for each supported provider. + +## Procedure + +### Step 1: Collect Stats via CLI + +Run the following command for **each provider** and **each metric**: + +```bash +python3 prowler-cli.py --list- +``` + +**Providers:** `aws`, `azure`, `gcp`, `kubernetes`, `github`, `m365`, `oraclecloud`, `alibabacloud`, `cloudflare`, `mongodbatlas`, `openstack`, `nhn` + +**Metrics:** `checks`, `services`, `compliance`, `categories` + +The CLI output ends with a summary line like: + +``` +There are 572 available checks. +There is 1 available Compliance Framework. +``` + +Extract the number from the summary line. Note that singular results use "There is" instead of "There are". + +### Step 2: Batch Extraction + +Use this one-liner to collect all stats at once (handles both singular and plural output): + +```bash +for provider in aws azure gcp kubernetes github m365 oraclecloud alibabacloud cloudflare mongodbatlas openstack nhn; do + for metric in checks services compliance categories; do + result=$(python3 prowler-cli.py $provider --list-$metric 2>&1 | sed -n 's/.*There \(are\|is\) .*\x1b\[33m\([0-9]*\)\x1b\[0m.*/\2/p') + echo "$provider $metric: $result" + done +done +``` + +### Step 3: Update the Table + +Edit the table in `README.md` (located in the `# Prowler at a Glance` section) with the collected numbers. + +**Table format:** + +```markdown +| Provider | Checks | Services | [Compliance Frameworks](...) | [Categories](...) | Support | Interface | +|---|---|---|---|---|---|---| +| AWS | 572 | 83 | 41 | 17 | Official | UI, API, CLI | +``` + +### Provider Name Mapping + +| CLI Provider | Table Display Name | +|---|---| +| `aws` | AWS | +| `azure` | Azure | +| `gcp` | GCP | +| `kubernetes` | Kubernetes | +| `github` | GitHub | +| `m365` | M365 | +| `oraclecloud` | OCI | +| `alibabacloud` | Alibaba Cloud | +| `cloudflare` | Cloudflare | +| `mongodbatlas` | MongoDB Atlas | +| `openstack` | OpenStack | +| `nhn` | NHN | + +### Special Rows (No CLI stats) + +These providers delegate to external tools and do NOT use CLI stats: + +| Provider | Checks Column | Services | Compliance | Categories | +|---|---|---|---|---| +| IaC | `[See trivy docs.](https://trivy.dev/latest/docs/coverage/iac/)` | N/A | N/A | N/A | +| LLM | `[See promptfoo docs.](https://www.promptfoo.dev/docs/red-team/plugins/)` | N/A | N/A | N/A | + +### Support and Interface Columns + +- **Support**: `Official` for all providers except `NHN` which is `Unofficial` +- **Interface**: Most providers use `UI, API, CLI`. Exceptions with `CLI` only: `Cloudflare`, `OpenStack`, `NHN`, `LLM` + +## Rules + +- **ALWAYS** use the CLI (`python3 prowler-cli.py`) to obtain numbers. Do NOT count files manually. +- **NEVER** commit changes unless explicitly asked. +- **NEVER** modify the IaC or LLM rows (they link to external docs). +- Verify the CLI is working by running one provider first before batch-processing all. + +## Resources + +- **CLI entry point**: `prowler-cli.py` in the repository root +- **Table location**: `README.md`, section `# Prowler at a Glance` (around line 100) diff --git a/skills/tdd/SKILL.md b/skills/tdd/SKILL.md new file mode 100644 index 0000000000..60887c342c --- /dev/null +++ b/skills/tdd/SKILL.md @@ -0,0 +1,371 @@ +--- +name: tdd +description: > + Test-Driven Development workflow for ALL Prowler components (UI, SDK, API). + Trigger: ALWAYS when implementing features, fixing bugs, or refactoring - regardless of component. + This is a MANDATORY workflow, not optional. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "2.0" + scope: [root, ui, api, prowler] + auto_invoke: + - "Implementing feature" + - "Fixing bug" + - "Refactoring code" + - "Working on task" + - "Modifying component" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task +--- + +## TDD Cycle (MANDATORY) + +``` ++-----------------------------------------+ +| RED -> GREEN -> REFACTOR | +| ^ | | +| +------------------------+ | ++-----------------------------------------+ +``` + +**The question is NOT "should I write tests?" but "what tests do I need?"** + +--- + +## The Three Laws of TDD + +1. **No production code** until you have a failing test +2. **No more test** than necessary to fail +3. **No more code** than necessary to pass + +--- + +## Detect Your Stack + +Before starting, identify which component you're working on: + +| Working in | Stack | Runner | Test pattern | Details | +|------------|-------|--------|-------------|---------| +| `ui/` | TypeScript / React | Vitest + RTL | `*.test.{ts,tsx}` (co-located) | See `vitest` skill | +| `prowler/` | Python | pytest + moto | `*_test.py` (suffix) in `tests/` | See `prowler-test-sdk` skill | +| `api/` | Python / Django | pytest + django | `test_*.py` (prefix) in `api/src/backend/**/tests/` | See `prowler-test-api` skill | + +--- + +## Phase 0: Assessment (ALWAYS FIRST) + +Before writing ANY code: + +### UI (`ui/`) + +```bash +# 1. Find existing tests +fd "*.test.tsx" ui/components/feature/ + +# 2. Check coverage +pnpm test:coverage -- components/feature/ + +# 3. Read existing tests +``` + +### SDK (`prowler/`) + +```bash +# 1. Find existing tests +fd "*_test.py" tests/providers/aws/services/ec2/ + +# 2. Run specific test +poetry run pytest tests/providers/aws/services/ec2/ec2_ami_public/ -v + +# 3. Read existing tests +``` + +### API (`api/`) + +```bash +# 1. Find existing tests +fd "test_*.py" api/src/backend/api/tests/ + +# 2. Run specific test +poetry run pytest api/src/backend/api/tests/test_models.py -v + +# 3. Read existing tests +``` + +### Decision Tree (All Stacks) + +``` ++------------------------------------------+ +| Does test file exist for this code? | ++----------+-----------------------+-------+ + | NO | YES + v v ++------------------+ +------------------+ +| CREATE test file | | Check coverage | +| -> Phase 1: RED | | for your change | ++------------------+ +--------+---------+ + | + +--------+--------+ + | Missing cases? | + +---+---------+---+ + | YES | NO + v v + +-----------+ +-----------+ + | ADD tests | | Proceed | + | Phase 1 | | Phase 2 | + +-----------+ +-----------+ +``` + +--- + +## Phase 1: RED - Write Failing Tests + +### For NEW Functionality + +**UI (Vitest)** + +```typescript +describe("PriceCalculator", () => { + it("should return 0 for quantities below threshold", () => { + // Given + const quantity = 3; + + // When + const result = calculateDiscount(quantity); + + // Then + expect(result).toBe(0); + }); +}); +``` + +**SDK (pytest)** + +```python +class Test_ec2_ami_public: + @mock_aws + def test_no_public_amis(self): + # Given - No AMIs exist + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch("prowler...ec2_service", new=EC2(aws_provider)): + from prowler...ec2_ami_public import ec2_ami_public + + # When + check = ec2_ami_public() + result = check.execute() + + # Then + assert len(result) == 0 +``` + +**API (pytest-django)** + +```python +@pytest.mark.django_db +class TestResourceModel: + def test_create_resource_with_tags(self, providers_fixture): + # Given + provider, *_ = providers_fixture + tenant_id = provider.tenant_id + + # When + resource = Resource.objects.create( + tenant_id=tenant_id, provider=provider, + uid="arn:aws:ec2:us-east-1:123456789:instance/i-1234", + name="test", region="us-east-1", service="ec2", type="instance", + ) + + # Then + assert resource.uid == "arn:aws:ec2:us-east-1:123456789:instance/i-1234" +``` + +**Run -> MUST fail:** Test references code that doesn't exist yet. + +### For BUG FIXES + +Write a test that **reproduces the bug** first: + +**UI:** `expect(() => render()).not.toThrow();` + +**SDK:** `assert result[0].status == "FAIL" # Currently returns PASS incorrectly` + +**API:** `assert response.status_code == 403 # Currently returns 200` + +**Run -> Should FAIL (reproducing the bug)** + +### For REFACTORING + +Capture ALL current behavior BEFORE refactoring: + +``` +# Any stack: run ALL existing tests, they should PASS +# This is your safety net - if any fail after refactoring, you broke something +``` + +**Run -> All should PASS (baseline)** + +--- + +## Phase 2: GREEN - Minimum Code + +Write the MINIMUM code to make the test pass. Hardcoding is valid for the first test. + +**UI:** + +```typescript +// Test expects calculateDiscount(100, 10) === 10 +function calculateDiscount() { + return 10; // FAKE IT - hardcoded is valid for first test +} +``` + +**Python (SDK/API):** + +```python +# Test expects check.execute() returns 0 results +def execute(self): + return [] # FAKE IT - hardcoded is valid for first test +``` + +**This passes. But we're not done...** + +--- + +## Phase 3: Triangulation (CRITICAL) + +**One test allows faking. Multiple tests FORCE real logic.** + +Add tests with different inputs that break the hardcoded value: + +| Scenario | Required? | +|----------|-----------| +| Happy path | YES | +| Zero/empty values | YES | +| Boundary values | YES | +| Different valid inputs | YES (breaks fake) | +| Error conditions | YES | + +**UI:** + +```typescript +it("should calculate 10% discount", () => { + expect(calculateDiscount(100, 10)).toBe(10); +}); + +// ADD - breaks the fake: +it("should calculate 15% on 200", () => { + expect(calculateDiscount(200, 15)).toBe(30); +}); + +it("should return 0 for 0% rate", () => { + expect(calculateDiscount(100, 0)).toBe(0); +}); +``` + +**Python:** + +```python +def test_single_public_ami(self): + # Different input -> breaks hardcoded empty list + assert len(result) == 1 + assert result[0].status == "FAIL" + +def test_private_ami(self): + assert result[0].status == "PASS" +``` + +**Now fake BREAKS -> Real implementation required.** + +--- + +## Phase 4: REFACTOR + +Tests GREEN -> Improve code quality WITHOUT changing behavior. + +- Extract functions/methods +- Improve naming +- Add types/validation +- Reduce duplication + +**Run tests after EACH change -> Must stay GREEN** + +--- + +## Quick Reference + +``` ++------------------------------------------------+ +| TDD WORKFLOW | ++------------------------------------------------+ +| 0. ASSESS: What tests exist? What's missing? | +| | +| 1. RED: Write ONE failing test | +| +-- Run -> Must fail with clear error | +| | +| 2. GREEN: Write MINIMUM code to pass | +| +-- Fake It is valid for first test | +| | +| 3. TRIANGULATE: Add tests that break the fake | +| +-- Different inputs, edge cases | +| | +| 4. REFACTOR: Improve with confidence | +| +-- Tests stay green throughout | +| | +| 5. REPEAT: Next behavior/requirement | ++------------------------------------------------+ +``` + +--- + +## Anti-Patterns (NEVER DO) + +``` +# ANY language: + +# 1. Code first, tests after +def new_feature(): ... # Then writing tests = USELESS + +# 2. Skip triangulation +# Single test allows faking forever + +# 3. Test implementation details +assert component.state.is_loading == True # BAD - test behavior, not internals +assert mock_service.call_count == 3 # BAD - brittle coupling + +# 4. All tests at once before any code +# Write ONE test, make it pass, THEN write the next + +# 5. Giant test methods +# Each test should verify ONE behavior +``` + +--- + +## Commands by Stack + +### UI (`ui/`) + +```bash +pnpm test # Watch mode +pnpm test:run # Single run (CI) +pnpm test:coverage # Coverage report +pnpm test ComponentName # Filter by name +``` + +### SDK (`prowler/`) + +```bash +poetry run pytest tests/path/ -v # Run specific tests +poetry run pytest tests/path/ -v -k "test_name" # Filter by name +poetry run pytest -n auto tests/ # Parallel run +poetry run pytest --cov=./prowler tests/ # Coverage +``` + +### API (`api/`) + +```bash +poetry run pytest -x --tb=short # Run all (stop on first fail) +poetry run pytest api/src/backend/api/tests/test_file.py # Specific file +poetry run pytest -k "test_name" -v # Filter by name +``` diff --git a/skills/vitest/SKILL.md b/skills/vitest/SKILL.md new file mode 100644 index 0000000000..11190cbaef --- /dev/null +++ b/skills/vitest/SKILL.md @@ -0,0 +1,201 @@ +--- +name: vitest +description: > + Vitest unit testing patterns with React Testing Library. + Trigger: When writing unit tests for React components, hooks, or utilities. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" + scope: [root, ui] + auto_invoke: + - "Writing Vitest tests" + - "Writing React component tests" + - "Writing unit tests for UI" + - "Testing hooks or utilities" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task +--- + +> **For E2E tests**: Use `prowler-test-ui` skill (Playwright). +> This skill covers **unit/integration tests** with Vitest + React Testing Library. + +## Test Structure (REQUIRED) + +Use **Given/When/Then** (AAA) pattern with comments: + +```typescript +it("should update user name when form is submitted", async () => { + // Given - Arrange + const user = userEvent.setup(); + const onSubmit = vi.fn(); + render(); + + // When - Act + await user.type(screen.getByLabelText(/name/i), "John"); + await user.click(screen.getByRole("button", { name: /submit/i })); + + // Then - Assert + expect(onSubmit).toHaveBeenCalledWith({ name: "John" }); +}); +``` + +--- + +## Describe Block Organization + +```typescript +describe("ComponentName", () => { + describe("when [condition]", () => { + it("should [expected behavior]", () => {}); + }); +}); +``` + +**Group by behavior, NOT by method.** + +--- + +## Query Priority (REQUIRED) + +| Priority | Query | Use Case | +|----------|-------|----------| +| 1 | `getByRole` | Buttons, inputs, headings | +| 2 | `getByLabelText` | Form fields | +| 3 | `getByPlaceholderText` | Inputs without label | +| 4 | `getByText` | Static text | +| 5 | `getByTestId` | Last resort only | + +```typescript +// ✅ GOOD +screen.getByRole("button", { name: /submit/i }); +screen.getByLabelText(/email/i); + +// ❌ BAD +container.querySelector(".btn-primary"); +``` + +--- + +## userEvent over fireEvent (REQUIRED) + +```typescript +// ✅ ALWAYS use userEvent +const user = userEvent.setup(); +await user.click(button); +await user.type(input, "hello"); + +// ❌ NEVER use fireEvent for interactions +fireEvent.click(button); +``` + +--- + +## Async Testing Patterns + +```typescript +// ✅ findBy for elements that appear async +const element = await screen.findByText(/loaded/i); + +// ✅ waitFor for assertions +await waitFor(() => { + expect(screen.getByText(/success/i)).toBeInTheDocument(); +}); + +// ✅ ONE assertion per waitFor +await waitFor(() => expect(mockFn).toHaveBeenCalled()); +await waitFor(() => expect(screen.getByText(/done/i)).toBeVisible()); + +// ❌ NEVER multiple assertions in waitFor +await waitFor(() => { + expect(mockFn).toHaveBeenCalled(); + expect(screen.getByText(/done/i)).toBeVisible(); // Slower failures +}); +``` + +--- + +## Mocking + +```typescript +// Basic mock +const handleClick = vi.fn(); + +// Mock with return value +const fetchUser = vi.fn().mockResolvedValue({ name: "John" }); + +// Always clean up +afterEach(() => { + vi.restoreAllMocks(); +}); +``` + +### vi.spyOn vs vi.mock + +| Method | When to Use | +|--------|-------------| +| `vi.spyOn` | Observe without replacing (PREFERRED) | +| `vi.mock` | Replace entire module (use sparingly) | + +--- + +## Common Matchers + +```typescript +// Presence +expect(element).toBeInTheDocument(); +expect(element).toBeVisible(); + +// State +expect(button).toBeDisabled(); +expect(input).toHaveValue("text"); +expect(checkbox).toBeChecked(); + +// Content +expect(element).toHaveTextContent(/hello/i); +expect(element).toHaveAttribute("href", "/home"); + +// Functions +expect(fn).toHaveBeenCalledWith(arg1, arg2); +expect(fn).toHaveBeenCalledTimes(2); +``` + +--- + +## What NOT to Test + +```typescript +// ❌ Internal state +expect(component.state.isLoading).toBe(true); + +// ❌ Third-party libraries +expect(axios.get).toHaveBeenCalled(); + +// ❌ Static content (unless conditional) +expect(screen.getByText("Welcome")).toBeInTheDocument(); + +// ✅ User-visible behavior +expect(screen.getByRole("button")).toBeDisabled(); +``` + +--- + +## File Organization + +``` +components/ +├── Button/ +│ ├── Button.tsx +│ ├── Button.test.tsx # Co-located +│ └── index.ts +``` + +--- + +## Commands + +```bash +pnpm test # Watch mode +pnpm test:run # Single run +pnpm test:coverage # With coverage +pnpm test Button # Filter by name +``` diff --git a/tests/lib/cli/parser_test.py b/tests/lib/cli/parser_test.py index 324b9b6c20..f2192d8d0b 100644 --- a/tests/lib/cli/parser_test.py +++ b/tests/lib/cli/parser_test.py @@ -17,7 +17,7 @@ prowler_command = "prowler" # capsys # https://docs.pytest.org/en/7.1.x/how-to/capture-stdout-stderr.html -prowler_default_usage_error = "usage: prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,dashboard,iac} ..." +prowler_default_usage_error = "usage: prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,dashboard,iac,image} ..." def mock_get_available_providers(): @@ -29,6 +29,7 @@ def mock_get_available_providers(): "m365", "github", "iac", + "image", "nhn", "mongodbatlas", "oraclecloud", diff --git a/tests/lib/outputs/compliance/cis/cis_m365_test.py b/tests/lib/outputs/compliance/cis/cis_m365_test.py index e961cfca93..12ed0ea729 100644 --- a/tests/lib/outputs/compliance/cis/cis_m365_test.py +++ b/tests/lib/outputs/compliance/cis/cis_m365_test.py @@ -97,8 +97,8 @@ class TestM365CIS: assert output_data_manual.Provider == "m365" assert output_data_manual.Framework == CIS_4_0_M365.Framework assert output_data_manual.Name == CIS_4_0_M365.Name - assert output_data_manual.TenantId == TENANT_ID - assert output_data_manual.Location == LOCATION + assert output_data_manual.TenantId == "" + assert output_data_manual.Location == "" assert output_data_manual.Description == CIS_4_0_M365.Description assert output_data_manual.Requirements_Id == CIS_4_0_M365.Requirements[1].Id assert ( @@ -184,6 +184,6 @@ class TestM365CIS: mock_file.seek(0) content = mock_file.read() - expected_csv = f"PROVIDER;DESCRIPTION;TENANTID;LOCATION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_SECTION;REQUIREMENTS_ATTRIBUTES_SUBSECTION;REQUIREMENTS_ATTRIBUTES_PROFILE;REQUIREMENTS_ATTRIBUTES_ASSESSMENTSTATUS;REQUIREMENTS_ATTRIBUTES_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_RATIONALESTATEMENT;REQUIREMENTS_ATTRIBUTES_IMPACTSTATEMENT;REQUIREMENTS_ATTRIBUTES_REMEDIATIONPROCEDURE;REQUIREMENTS_ATTRIBUTES_AUDITPROCEDURE;REQUIREMENTS_ATTRIBUTES_ADDITIONALINFORMATION;REQUIREMENTS_ATTRIBUTES_DEFAULTVALUE;REQUIREMENTS_ATTRIBUTES_REFERENCES;STATUS;STATUSEXTENDED;RESOURCEID;RESOURCENAME;CHECKID;MUTED;FRAMEWORK;NAME\r\nm365;The CIS Microsoft 365 Foundations Benchmark provides prescriptive guidance for configuring security options for Microsoft 365 with an emphasis on foundational, testable, and architecture agnostic settings.;00000000-0000-0000-0000-000000000000;global;{datetime.now()};2.1.3;Ensure MFA Delete is enabled on S3 buckets;2.1. Simple Storage Service (S3);;Level 1;Automated;Once MFA Delete is enabled on your sensitive and classified S3 bucket it requires the user to have two forms of authentication.;Adding MFA delete to an S3 bucket, requires additional authentication when you change the version state of your bucket or you delete and object version adding another layer of security in the event your security credentials are compromised or unauthorized access is granted.;;Perform the steps below to enable MFA delete on an S3 bucket.Note:-You cannot enable MFA Delete using the AWS Management Console. You must use the AWS CLI or API.-You must use your 'root' account to enable MFA Delete on S3 buckets.**From Command line:**1. Run the s3api put-bucket-versioning command aws s3api put-bucket-versioning --profile my-root-profile --bucket Bucket_Name --versioning-configuration Status=Enabled,MFADelete=Enabled --mfa arn:aws:iam::aws_account_id:mfa/root-account-mfa-device passcode;Perform the steps below to confirm MFA delete is configured on an S3 Bucket**From Console:**1. Login to the S3 console at `https://console.aws.amazon.com/s3/`2. Click the `Check` box next to the Bucket name you want to confirm3. In the window under `Properties`4. Confirm that Versioning is `Enabled`5. Confirm that MFA Delete is `Enabled`**From Command Line:**1. Run the `get-bucket-versioning aws s3api get-bucket-versioning --bucket my-bucket Output example: Enabled Enabled\ If the Console or the CLI output does not show Versioning and MFA Delete `enabled` refer to the remediation below.;;By default, MFA Delete is not enabled on S3 buckets.;https://docs.aws.amazon.com/AmazonS3/latest/dev/Versioning.html#MultiFactorAuthenticationDelete:https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMFADelete.html:https://aws.amazon.com/blogs/security/securing-access-to-aws-using-mfa-part-3/:https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_lost-or-broken.html;PASS;;;;service_test_check_id;False;CIS;CIS Microsoft 365 Foundations Benchmark v4.0.0\r\nm365;The CIS Microsoft 365 Foundations Benchmark provides prescriptive guidance for configuring security options for Microsoft 365 with an emphasis on foundational, testable, and architecture agnostic settings.;00000000-0000-0000-0000-000000000000;global;{datetime.now()};2.1.4;Ensure that the controller manager pod specification file permissions are set to 600 or more restrictive;1.1 Control Plane Node Configuration Files;;Level 1;Automated;Ensure that the controller manager pod specification file has permissions of `600` or more restrictive.;The controller manager pod specification file controls various parameters that set the behavior of the Controller Manager on the master node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.;;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/manifests/kube-controller-manager.yaml ```;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/manifests/kube-controller-manager.yaml ``` Verify that the permissions are `600` or more restrictive.;;By default, the `kube-controller-manager.yaml` file has permissions of `640`.;https://kubernetes.io/docs/admin/kube-apiserver/;MANUAL;Manual check;manual_check;Manual check;manual;False;CIS;CIS Microsoft 365 Foundations Benchmark v4.0.0\r\n" + expected_csv = f"PROVIDER;DESCRIPTION;TENANTID;LOCATION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_SECTION;REQUIREMENTS_ATTRIBUTES_SUBSECTION;REQUIREMENTS_ATTRIBUTES_PROFILE;REQUIREMENTS_ATTRIBUTES_ASSESSMENTSTATUS;REQUIREMENTS_ATTRIBUTES_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_RATIONALESTATEMENT;REQUIREMENTS_ATTRIBUTES_IMPACTSTATEMENT;REQUIREMENTS_ATTRIBUTES_REMEDIATIONPROCEDURE;REQUIREMENTS_ATTRIBUTES_AUDITPROCEDURE;REQUIREMENTS_ATTRIBUTES_ADDITIONALINFORMATION;REQUIREMENTS_ATTRIBUTES_DEFAULTVALUE;REQUIREMENTS_ATTRIBUTES_REFERENCES;STATUS;STATUSEXTENDED;RESOURCEID;RESOURCENAME;CHECKID;MUTED;FRAMEWORK;NAME\r\nm365;The CIS Microsoft 365 Foundations Benchmark provides prescriptive guidance for configuring security options for Microsoft 365 with an emphasis on foundational, testable, and architecture agnostic settings.;00000000-0000-0000-0000-000000000000;global;{datetime.now()};2.1.3;Ensure MFA Delete is enabled on S3 buckets;2.1. Simple Storage Service (S3);;Level 1;Automated;Once MFA Delete is enabled on your sensitive and classified S3 bucket it requires the user to have two forms of authentication.;Adding MFA delete to an S3 bucket, requires additional authentication when you change the version state of your bucket or you delete and object version adding another layer of security in the event your security credentials are compromised or unauthorized access is granted.;;Perform the steps below to enable MFA delete on an S3 bucket.Note:-You cannot enable MFA Delete using the AWS Management Console. You must use the AWS CLI or API.-You must use your 'root' account to enable MFA Delete on S3 buckets.**From Command line:**1. Run the s3api put-bucket-versioning command aws s3api put-bucket-versioning --profile my-root-profile --bucket Bucket_Name --versioning-configuration Status=Enabled,MFADelete=Enabled --mfa arn:aws:iam::aws_account_id:mfa/root-account-mfa-device passcode;Perform the steps below to confirm MFA delete is configured on an S3 Bucket**From Console:**1. Login to the S3 console at `https://console.aws.amazon.com/s3/`2. Click the `Check` box next to the Bucket name you want to confirm3. In the window under `Properties`4. Confirm that Versioning is `Enabled`5. Confirm that MFA Delete is `Enabled`**From Command Line:**1. Run the `get-bucket-versioning aws s3api get-bucket-versioning --bucket my-bucket Output example: Enabled Enabled\ If the Console or the CLI output does not show Versioning and MFA Delete `enabled` refer to the remediation below.;;By default, MFA Delete is not enabled on S3 buckets.;https://docs.aws.amazon.com/AmazonS3/latest/dev/Versioning.html#MultiFactorAuthenticationDelete:https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMFADelete.html:https://aws.amazon.com/blogs/security/securing-access-to-aws-using-mfa-part-3/:https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_lost-or-broken.html;PASS;;;;service_test_check_id;False;CIS;CIS Microsoft 365 Foundations Benchmark v4.0.0\r\nm365;The CIS Microsoft 365 Foundations Benchmark provides prescriptive guidance for configuring security options for Microsoft 365 with an emphasis on foundational, testable, and architecture agnostic settings.;;;{datetime.now()};2.1.4;Ensure that the controller manager pod specification file permissions are set to 600 or more restrictive;1.1 Control Plane Node Configuration Files;;Level 1;Automated;Ensure that the controller manager pod specification file has permissions of `600` or more restrictive.;The controller manager pod specification file controls various parameters that set the behavior of the Controller Manager on the master node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.;;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/manifests/kube-controller-manager.yaml ```;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/manifests/kube-controller-manager.yaml ``` Verify that the permissions are `600` or more restrictive.;;By default, the `kube-controller-manager.yaml` file has permissions of `640`.;https://kubernetes.io/docs/admin/kube-apiserver/;MANUAL;Manual check;manual_check;Manual check;manual;False;CIS;CIS Microsoft 365 Foundations Benchmark v4.0.0\r\n" assert content == expected_csv diff --git a/tests/providers/alibabacloud/services/oss/oss_service_test.py b/tests/providers/alibabacloud/services/oss/oss_service_test.py index ea906f587e..e71c2eca9e 100644 --- a/tests/providers/alibabacloud/services/oss/oss_service_test.py +++ b/tests/providers/alibabacloud/services/oss/oss_service_test.py @@ -74,3 +74,26 @@ def test_list_buckets_respects_audit_filters(): oss._list_buckets() assert list(oss.buckets.keys()) == [] + + +def test_list_buckets_rejects_xxe_payload(): + oss = _build_oss_service() + xxe_payload = """ + + ]> + + + + &xxe; + 2025-01-01T00:00:00.000Z + oss-cn-hangzhou + + + """ + + with patch("requests.get") as get_mock: + get_mock.return_value = MagicMock(status_code=200, text=xxe_payload) + oss._list_buckets() + + assert oss.buckets == {} diff --git a/tests/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible_test.py b/tests/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible_test.py index c8abdaaf5d..1637d1344e 100644 --- a/tests/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible_test.py +++ b/tests/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible_test.py @@ -129,6 +129,116 @@ class Test_kms_key_not_publicly_accessible: assert result[0].resource_id == key["KeyId"] assert result[0].resource_arn == key["Arn"] + @mock_aws + def test_kms_key_public_accessible_with_describe_key(self): + # Generate KMS Client + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + # Create KMS key with public policy allowing kms:DescribeKey + key = kms_client.create_key( + MultiRegion=False, + Policy=json.dumps( + { + "Version": "2012-10-17", + "Id": "key-default-1", + "Statement": [ + { + "Sid": "AllowDescribeKeyPermissionForClusterOperator", + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "kms:DescribeKey", + "Resource": "*", + } + ], + } + ), + )["KeyMetadata"] + + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_key_not_publicly_accessible.kms_key_not_publicly_accessible.kms_client", + new=KMS(aws_provider), + ), + ): + # Test Check + from prowler.providers.aws.services.kms.kms_key_not_publicly_accessible.kms_key_not_publicly_accessible import ( + kms_key_not_publicly_accessible, + ) + + check = kms_key_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"KMS key {key['KeyId']} may be publicly accessible." + ) + assert result[0].resource_id == key["KeyId"] + assert result[0].resource_arn == key["Arn"] + + @mock_aws + def test_kms_key_public_accessible_with_decrypt(self): + # Generate KMS Client + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + # Create KMS key with public policy allowing kms:Decrypt + key = kms_client.create_key( + MultiRegion=False, + Policy=json.dumps( + { + "Version": "2012-10-17", + "Id": "key-default-1", + "Statement": [ + { + "Sid": "AllowDecryptPermissionPublicly", + "Effect": "Allow", + "Principal": "*", + "Action": "kms:Decrypt", + "Resource": "*", + } + ], + } + ), + )["KeyMetadata"] + + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_key_not_publicly_accessible.kms_key_not_publicly_accessible.kms_client", + new=KMS(aws_provider), + ), + ): + # Test Check + from prowler.providers.aws.services.kms.kms_key_not_publicly_accessible.kms_key_not_publicly_accessible import ( + kms_key_not_publicly_accessible, + ) + + check = kms_key_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"KMS key {key['KeyId']} may be publicly accessible." + ) + assert result[0].resource_id == key["KeyId"] + assert result[0].resource_arn == key["Arn"] + @mock_aws def test_kms_key_empty_principal(self): # Generate KMS Client diff --git a/tests/providers/github/services/organization/organization_verified_badge/organization_verified_badge_test.py b/tests/providers/github/services/organization/organization_verified_badge/organization_verified_badge_test.py new file mode 100644 index 0000000000..5bea0aa034 --- /dev/null +++ b/tests/providers/github/services/organization/organization_verified_badge/organization_verified_badge_test.py @@ -0,0 +1,137 @@ +from unittest import mock + +from prowler.providers.github.services.organization.organization_service import Org +from tests.providers.github.github_fixtures import set_mocked_github_provider + + +class Test_organization_verified_badge: + def test_no_organizations(self): + organization_client = mock.MagicMock + organization_client.organizations = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge import ( + organization_verified_badge, + ) + + check = organization_verified_badge() + result = check.execute() + assert len(result) == 0 + + def test_organization_is_verified_true_pass(self): + organization_client = mock.MagicMock + org_name = "test-organization" + organization_client.organizations = { + 1: Org( + id=1, + name=org_name, + is_verified=True, + ), + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge import ( + organization_verified_badge, + ) + + check = organization_verified_badge() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == 1 + assert result[0].resource_name == org_name + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Organization {org_name} is verified on GitHub." + ) + + def test_organization_is_verified_false_fail(self): + organization_client = mock.MagicMock + org_name = "test-organization" + organization_client.organizations = { + 1: Org( + id=1, + name=org_name, + is_verified=False, + ), + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge import ( + organization_verified_badge, + ) + + check = organization_verified_badge() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == 1 + assert result[0].resource_name == org_name + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Organization {org_name} is not verified on GitHub." + ) + + def test_organization_is_verified_none_edge_case(self): + organization_client = mock.MagicMock + org_name = "test-organization" + organization_client.organizations = { + 1: Org( + id=1, + name=org_name, + is_verified=None, + ), + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge import ( + organization_verified_badge, + ) + + check = organization_verified_badge() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == 1 + assert result[0].resource_name == org_name + # Treat none like not verified (false) + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Organization {org_name} is not verified on GitHub." + ) diff --git a/tests/providers/github/services/repository/repository_branch_delete_on_merge_enabled/repository_branch_delete_on_merge_enabled_test.py b/tests/providers/github/services/repository/repository_branch_delete_on_merge_enabled/repository_branch_delete_on_merge_enabled_test.py index e10050e0cc..079fea224c 100644 --- a/tests/providers/github/services/repository/repository_branch_delete_on_merge_enabled/repository_branch_delete_on_merge_enabled_test.py +++ b/tests/providers/github/services/repository/repository_branch_delete_on_merge_enabled/repository_branch_delete_on_merge_enabled_test.py @@ -149,3 +149,64 @@ class Test_repository_branch_delete_on_merge_enabled_test: result[0].status_extended == f"Repository {repo_name} does delete branches on merge in default branch ({default_branch})." ) + + def test_branch_deletion_insufficient_permissions(self): + repository_client = mock.MagicMock + repo_name = "repo3" + default_branch = "main" + repository_client.repositories = { + 3: Repo( + id=3, + name=repo_name, + owner="account-name", + full_name="account-name/repo3", + default_branch=Branch( + name=default_branch, + protected=False, + default_branch=True, + require_pull_request=False, + approval_count=0, + required_linear_history=False, + allow_force_pushes=True, + branch_deletion=True, + status_checks=False, + enforce_admins=False, + require_code_owner_reviews=False, + require_signed_commits=False, + conversation_resolution=False, + ), + private=False, + archived=False, + pushed_at=datetime.now(timezone.utc), + securitymd=False, + codeowners_exists=False, + secret_scanning_enabled=False, + dependabot_alerts_enabled=False, + delete_branch_on_merge=None, # Insufficient permissions + ), + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.repository.repository_branch_delete_on_merge_enabled.repository_branch_delete_on_merge_enabled.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_branch_delete_on_merge_enabled.repository_branch_delete_on_merge_enabled import ( + repository_branch_delete_on_merge_enabled, + ) + + check = repository_branch_delete_on_merge_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == 3 + assert result[0].resource_name == repo_name + assert result[0].status == "MANUAL" + assert ( + result[0].status_extended + == f"Repository {repo_name} branch deletion setting could not be checked in default branch ({default_branch}) due to insufficient permissions. Requires Administration: Read and Write permission." + ) diff --git a/tests/providers/github/services/repository/repository_service_test.py b/tests/providers/github/services/repository/repository_service_test.py index e005e3a05b..932bf2c766 100644 --- a/tests/providers/github/services/repository/repository_service_test.py +++ b/tests/providers/github/services/repository/repository_service_test.py @@ -245,19 +245,61 @@ class Test_Repository_Scoping: self.mock_repo2.get_branch.side_effect = Exception("404 Not Found") self.mock_repo2.get_dependabot_alerts.side_effect = Exception("404 Not Found") - def test_combined_repository_and_organization_scoping(self): - """Test that both repository and organization scoping can be used together""" + def test_qualified_repo_with_organization_skips_org_fetch(self): + """Test that a fully qualified repo with --organization does not fetch all org repos""" provider = set_mocked_github_provider() provider.repositories = ["owner1/repo1"] provider.organizations = ["org2"] mock_client = MagicMock() - # Repository lookup mock_client.get_repo.return_value = self.mock_repo1 - # Organization lookup - mock_org = MagicMock() - mock_org.get_repos.return_value = [self.mock_repo2] - mock_client.get_organization.return_value = mock_org + + with patch( + "prowler.providers.github.services.repository.repository_service.GithubService.__init__" + ): + repository_service = Repository(provider) + repository_service.clients = [mock_client] + repository_service.provider = provider + + repos = repository_service._list_repositories() + + assert len(repos) == 1 + assert 1 in repos + assert repos[1].name == "repo1" + mock_client.get_repo.assert_called_once_with("owner1/repo1") + mock_client.get_organization.assert_not_called() + + def test_unqualified_repo_qualified_with_organization(self): + """Test that an unqualified repo name is qualified with the organization""" + provider = set_mocked_github_provider() + provider.repositories = ["repo1"] + provider.organizations = ["owner1"] + + mock_client = MagicMock() + mock_client.get_repo.return_value = self.mock_repo1 + + with patch( + "prowler.providers.github.services.repository.repository_service.GithubService.__init__" + ): + repository_service = Repository(provider) + repository_service.clients = [mock_client] + repository_service.provider = provider + + repos = repository_service._list_repositories() + + assert len(repos) == 1 + assert 1 in repos + assert repos[1].name == "repo1" + mock_client.get_repo.assert_called_once_with("owner1/repo1") + + def test_unqualified_repo_qualified_with_multiple_organizations(self): + """Test that an unqualified repo is qualified with each organization""" + provider = set_mocked_github_provider() + provider.repositories = ["repo1"] + provider.organizations = ["org1", "org2"] + + mock_client = MagicMock() + mock_client.get_repo.side_effect = [self.mock_repo1, self.mock_repo2] with patch( "prowler.providers.github.services.repository.repository_service.GithubService.__init__" @@ -269,12 +311,56 @@ class Test_Repository_Scoping: repos = repository_service._list_repositories() assert len(repos) == 2 - assert 1 in repos - assert 2 in repos - assert repos[1].name == "repo1" - assert repos[2].name == "repo2" - mock_client.get_repo.assert_called_once_with("owner1/repo1") - mock_client.get_organization.assert_called_once_with("org2") + mock_client.get_repo.assert_any_call("org1/repo1") + mock_client.get_repo.assert_any_call("org2/repo1") + + def test_unqualified_repo_without_organization_is_skipped(self): + """Test that an unqualified repo without --organization is skipped with a warning""" + provider = set_mocked_github_provider() + provider.repositories = ["repo1"] + provider.organizations = [] + + mock_client = MagicMock() + + with patch( + "prowler.providers.github.services.repository.repository_service.GithubService.__init__" + ): + repository_service = Repository(provider) + repository_service.clients = [mock_client] + repository_service.provider = provider + + with patch( + "prowler.providers.github.services.repository.repository_service.logger" + ) as mock_logger: + repos = repository_service._list_repositories() + + assert len(repos) == 0 + mock_logger.warning.assert_called_with( + "Repository name 'repo1' should be in 'owner/repo-name' format. Skipping." + ) + mock_client.get_repo.assert_not_called() + + def test_mixed_qualified_and_unqualified_repos_with_organization(self): + """Test mix of qualified and unqualified repos with --organization""" + provider = set_mocked_github_provider() + provider.repositories = ["repo1", "owner2/repo2"] + provider.organizations = ["org1"] + + mock_client = MagicMock() + mock_client.get_repo.side_effect = [self.mock_repo1, self.mock_repo2] + + with patch( + "prowler.providers.github.services.repository.repository_service.GithubService.__init__" + ): + repository_service = Repository(provider) + repository_service.clients = [mock_client] + repository_service.provider = provider + + repos = repository_service._list_repositories() + + assert len(repos) == 2 + mock_client.get_repo.assert_any_call("org1/repo1") + mock_client.get_repo.assert_any_call("owner2/repo2") class Test_Repository_Validation: diff --git a/tests/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured_test.py b/tests/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured_test.py new file mode 100644 index 0000000000..bbf11c03f2 --- /dev/null +++ b/tests/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured_test.py @@ -0,0 +1,303 @@ +from unittest import mock + +from prowler.providers.m365.services.defender.defender_service import ( + AdvancedThreatProtectionPolicy, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_defender_atp_safe_attachments_and_docs_configured: + """Tests for defender_atp_safe_attachments_and_docs_configured check.""" + + def test_no_atp_policy(self): + """Test when no ATP policy exists (advanced_threat_protection_policy is None).""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_client.audited_domain = DOMAIN + defender_client.advanced_threat_protection_policy = 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.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured import ( + defender_atp_safe_attachments_and_docs_configured, + ) + + check = defender_atp_safe_attachments_and_docs_configured() + result = check.execute() + + assert len(result) == 0 + + def test_atp_policy_all_settings_compliant(self): + """Test when ATP policy is properly configured (PASS case).""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_client.audited_domain = DOMAIN + defender_client.advanced_threat_protection_policy = ( + AdvancedThreatProtectionPolicy( + identity="Default", + enable_atp_for_spo_teams_odb=True, + enable_safe_docs=True, + allow_safe_docs_open=False, + ) + ) + + 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.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured import ( + defender_atp_safe_attachments_and_docs_configured, + ) + + check = defender_atp_safe_attachments_and_docs_configured() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "ATP policy Default has Safe Attachments for SharePoint, OneDrive, and Teams properly configured with Safe Documents enabled and click-through blocked." + ) + assert result[0].resource_id == "Default" + assert result[0].resource_name == "Default" + assert result[0].location == "global" + + def test_atp_policy_spo_teams_odb_disabled(self): + """Test when Safe Attachments for SPO/OneDrive/Teams is disabled (FAIL case).""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_client.audited_domain = DOMAIN + defender_client.advanced_threat_protection_policy = ( + AdvancedThreatProtectionPolicy( + identity="Default", + enable_atp_for_spo_teams_odb=False, + enable_safe_docs=True, + allow_safe_docs_open=False, + ) + ) + + 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.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured import ( + defender_atp_safe_attachments_and_docs_configured, + ) + + check = defender_atp_safe_attachments_and_docs_configured() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "ATP policy Default is not properly configured: Safe Attachments for SPO/OneDrive/Teams is disabled." + ) + assert result[0].resource_id == "Default" + assert result[0].resource_name == "Default" + assert result[0].location == "global" + + def test_atp_policy_safe_docs_disabled(self): + """Test when Safe Documents is disabled (FAIL case).""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_client.audited_domain = DOMAIN + defender_client.advanced_threat_protection_policy = ( + AdvancedThreatProtectionPolicy( + identity="Default", + enable_atp_for_spo_teams_odb=True, + enable_safe_docs=False, + allow_safe_docs_open=False, + ) + ) + + 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.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured import ( + defender_atp_safe_attachments_and_docs_configured, + ) + + check = defender_atp_safe_attachments_and_docs_configured() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "ATP policy Default is not properly configured: Safe Documents is disabled." + ) + assert result[0].resource_id == "Default" + assert result[0].resource_name == "Default" + assert result[0].location == "global" + + def test_atp_policy_safe_docs_open_allowed(self): + """Test when users can bypass Protected View for malicious files (FAIL case).""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_client.audited_domain = DOMAIN + defender_client.advanced_threat_protection_policy = ( + AdvancedThreatProtectionPolicy( + identity="Default", + enable_atp_for_spo_teams_odb=True, + enable_safe_docs=True, + allow_safe_docs_open=True, + ) + ) + + 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.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured import ( + defender_atp_safe_attachments_and_docs_configured, + ) + + check = defender_atp_safe_attachments_and_docs_configured() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "ATP policy Default is not properly configured: users can bypass Protected View for malicious files." + ) + assert result[0].resource_id == "Default" + assert result[0].resource_name == "Default" + assert result[0].location == "global" + + def test_atp_policy_all_settings_non_compliant(self): + """Test when all three settings are non-compliant (FAIL case).""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_client.audited_domain = DOMAIN + defender_client.advanced_threat_protection_policy = ( + AdvancedThreatProtectionPolicy( + identity="Default", + enable_atp_for_spo_teams_odb=False, + enable_safe_docs=False, + allow_safe_docs_open=True, + ) + ) + + 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.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured import ( + defender_atp_safe_attachments_and_docs_configured, + ) + + check = defender_atp_safe_attachments_and_docs_configured() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "ATP policy Default is not properly configured: Safe Attachments for SPO/OneDrive/Teams is disabled; Safe Documents is disabled; users can bypass Protected View for malicious files." + ) + assert result[0].resource_id == "Default" + assert result[0].resource_name == "Default" + assert result[0].location == "global" + + def test_atp_policy_custom_identity(self): + """Test with a custom policy identity name.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_client.audited_domain = DOMAIN + defender_client.advanced_threat_protection_policy = ( + AdvancedThreatProtectionPolicy( + identity="CustomPolicy", + enable_atp_for_spo_teams_odb=True, + enable_safe_docs=True, + allow_safe_docs_open=False, + ) + ) + + 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.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_atp_safe_attachments_and_docs_configured.defender_atp_safe_attachments_and_docs_configured import ( + defender_atp_safe_attachments_and_docs_configured, + ) + + check = defender_atp_safe_attachments_and_docs_configured() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "ATP policy CustomPolicy has Safe Attachments for SharePoint, OneDrive, and Teams properly configured with Safe Documents enabled and click-through blocked." + ) + assert result[0].resource_id == "CustomPolicy" + assert result[0].resource_name == "CustomPolicy" + assert result[0].location == "global" diff --git a/tests/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled_test.py b/tests/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled_test.py new file mode 100644 index 0000000000..b9bcc45ee1 --- /dev/null +++ b/tests/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled_test.py @@ -0,0 +1,468 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_defender_safe_attachments_policy_enabled: + """Tests for the defender_safe_attachments_policy_enabled check.""" + + def test_no_safe_attachments_policies(self): + """Test when no Safe Attachments policies exist.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import ( + defender_safe_attachments_policy_enabled, + ) + + defender_client.safe_attachments_policies = {} + defender_client.safe_attachments_rules = {} + + check = defender_safe_attachments_policy_enabled() + result = check.execute() + assert len(result) == 0 + + def test_case1_only_builtin_policy(self): + """Case 1: Only Built-in Protection Policy exists - always PASS.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import ( + defender_safe_attachments_policy_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + SafeAttachmentsPolicy, + ) + + defender_client.safe_attachments_policies = { + "Built-In Protection Policy": SafeAttachmentsPolicy( + name="Built-In Protection Policy", + identity="Built-In-Protection-Policy-ID", + enable=True, + action="Block", + quarantine_tag="AdminOnlyAccessPolicy", + redirect=False, + redirect_address="", + is_built_in_protection=True, + ) + } + defender_client.safe_attachments_rules = {} + + check = defender_safe_attachments_policy_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "is the only Safe Attachments policy" in result[0].status_extended + assert "baseline protection for all users" in result[0].status_extended + + def test_case2_builtin_and_custom_properly_configured(self): + """Case 2: Built-in + custom policy properly configured - both PASS.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import ( + defender_safe_attachments_policy_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + SafeAttachmentsPolicy, + SafeAttachmentsRule, + ) + + defender_client.safe_attachments_policies = { + "Built-In Protection Policy": SafeAttachmentsPolicy( + name="Built-In Protection Policy", + identity="Built-In-Protection-Policy-ID", + enable=True, + action="Block", + quarantine_tag="AdminOnlyAccessPolicy", + redirect=False, + redirect_address="", + is_built_in_protection=True, + ), + "Custom Policy": SafeAttachmentsPolicy( + name="Custom Policy", + identity="Custom-Policy-ID", + enable=True, + action="Block", + quarantine_tag="AdminOnlyAccessPolicy", + redirect=False, + redirect_address="", + is_built_in_protection=False, + ), + } + defender_client.safe_attachments_rules = { + "Custom Policy": SafeAttachmentsRule( + state="Enabled", + priority=0, + users=["user@example.com"], + groups=["Engineering"], + domains=["example.com"], + ) + } + + check = defender_safe_attachments_policy_enabled() + result = check.execute() + + assert len(result) == 2 + + # Built-in policy PASS + builtin_result = next( + r for r in result if r.resource_name == "Built-In Protection Policy" + ) + assert builtin_result.status == "PASS" + assert ( + "provides baseline Safe Attachments protection" + in builtin_result.status_extended + ) + + # Custom policy PASS + custom_result = next( + r for r in result if r.resource_name == "Custom Policy" + ) + assert custom_result.status == "PASS" + assert "is properly configured" in custom_result.status_extended + assert "users: user@example.com" in custom_result.status_extended + assert "groups: Engineering" in custom_result.status_extended + assert "domains: example.com" in custom_result.status_extended + assert "priority 0" in custom_result.status_extended + + def test_case3_builtin_pass_custom_misconfigured(self): + """Case 3: Built-in PASS + custom policy misconfigured - Built-in PASS, custom FAIL.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import ( + defender_safe_attachments_policy_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + SafeAttachmentsPolicy, + SafeAttachmentsRule, + ) + + defender_client.safe_attachments_policies = { + "Built-In Protection Policy": SafeAttachmentsPolicy( + name="Built-In Protection Policy", + identity="Built-In-Protection-Policy-ID", + enable=True, + action="Block", + quarantine_tag="AdminOnlyAccessPolicy", + redirect=False, + redirect_address="", + is_built_in_protection=True, + ), + "Custom Policy": SafeAttachmentsPolicy( + name="Custom Policy", + identity="Custom-Policy-ID", + enable=False, # Misconfigured + action="Allow", # Misconfigured + quarantine_tag="DefaultFullAccessPolicy", # Misconfigured + redirect=False, + redirect_address="", + is_built_in_protection=False, + ), + } + defender_client.safe_attachments_rules = { + "Custom Policy": SafeAttachmentsRule( + state="Enabled", + priority=0, + users=["user@example.com"], + groups=None, + domains=None, + ) + } + + check = defender_safe_attachments_policy_enabled() + result = check.execute() + + assert len(result) == 2 + + # Built-in policy still PASS + builtin_result = next( + r for r in result if r.resource_name == "Built-In Protection Policy" + ) + assert builtin_result.status == "PASS" + + # Custom policy FAIL + custom_result = next( + r for r in result if r.resource_name == "Custom Policy" + ) + assert custom_result.status == "FAIL" + assert "is not properly configured" in custom_result.status_extended + assert "priority 0" in custom_result.status_extended + + def test_custom_policy_without_rule_skipped(self): + """Test that custom policies without associated rules are skipped.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import ( + defender_safe_attachments_policy_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + SafeAttachmentsPolicy, + SafeAttachmentsRule, + ) + + defender_client.safe_attachments_policies = { + "Built-In Protection Policy": SafeAttachmentsPolicy( + name="Built-In Protection Policy", + identity="Built-In-Protection-Policy-ID", + enable=True, + action="Block", + quarantine_tag="AdminOnlyAccessPolicy", + redirect=False, + redirect_address="", + is_built_in_protection=True, + ), + "Custom Policy Without Rule": SafeAttachmentsPolicy( + name="Custom Policy Without Rule", + identity="Custom-Policy-ID", + enable=True, + action="Block", + quarantine_tag="AdminOnlyAccessPolicy", + redirect=False, + redirect_address="", + is_built_in_protection=False, + ), + } + # Rule for a different policy + defender_client.safe_attachments_rules = { + "Other Policy": SafeAttachmentsRule( + state="Enabled", + priority=0, + users=["user@example.com"], + groups=None, + domains=None, + ) + } + + check = defender_safe_attachments_policy_enabled() + result = check.execute() + + # Only Built-in policy should be in results + assert len(result) == 1 + assert result[0].resource_name == "Built-In Protection Policy" + assert result[0].status == "PASS" + + def test_custom_policy_with_disabled_rule(self): + """Test when custom policy has proper settings but disabled rule (FAIL).""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import ( + defender_safe_attachments_policy_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + SafeAttachmentsPolicy, + SafeAttachmentsRule, + ) + + defender_client.safe_attachments_policies = { + "Built-In Protection Policy": SafeAttachmentsPolicy( + name="Built-In Protection Policy", + identity="Built-In-Protection-Policy-ID", + enable=True, + action="Block", + quarantine_tag="AdminOnlyAccessPolicy", + redirect=False, + redirect_address="", + is_built_in_protection=True, + ), + "Custom Policy": SafeAttachmentsPolicy( + name="Custom Policy", + identity="Custom-Policy-ID", + enable=True, + action="Block", + quarantine_tag="AdminOnlyAccessPolicy", + redirect=False, + redirect_address="", + is_built_in_protection=False, + ), + } + defender_client.safe_attachments_rules = { + "Custom Policy": SafeAttachmentsRule( + state="Disabled", # Disabled rule + priority=0, + users=["user@example.com"], + groups=None, + domains=None, + ) + } + + check = defender_safe_attachments_policy_enabled() + result = check.execute() + + assert len(result) == 2 + + # Built-in policy PASS + builtin_result = next( + r for r in result if r.resource_name == "Built-In Protection Policy" + ) + assert builtin_result.status == "PASS" + + # Custom policy FAIL because rule is disabled + custom_result = next( + r for r in result if r.resource_name == "Custom Policy" + ) + assert custom_result.status == "FAIL" + assert "is not properly configured" in custom_result.status_extended + + def test_custom_policy_applies_to_all_users_when_no_scope(self): + """Test that custom policy with no users/groups/domains shows 'all users'.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safe_attachments_policy_enabled.defender_safe_attachments_policy_enabled import ( + defender_safe_attachments_policy_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + SafeAttachmentsPolicy, + SafeAttachmentsRule, + ) + + defender_client.safe_attachments_policies = { + "Built-In Protection Policy": SafeAttachmentsPolicy( + name="Built-In Protection Policy", + identity="Built-In-Protection-Policy-ID", + enable=True, + action="Block", + quarantine_tag="AdminOnlyAccessPolicy", + redirect=False, + redirect_address="", + is_built_in_protection=True, + ), + "Houston Safe Attachments Policy test": SafeAttachmentsPolicy( + name="Houston Safe Attachments Policy test", + identity="Houston-Policy-ID", + enable=False, # Misconfigured + action="Allow", + quarantine_tag="DefaultFullAccessPolicy", + redirect=False, + redirect_address="", + is_built_in_protection=False, + ), + } + defender_client.safe_attachments_rules = { + "Houston Safe Attachments Policy test": SafeAttachmentsRule( + state="Enabled", + priority=0, + users=None, # No users specified + groups=None, # No groups specified + domains=None, # No domains specified - applies to ALL users + ) + } + + check = defender_safe_attachments_policy_enabled() + result = check.execute() + + assert len(result) == 2 + + # Custom policy should show "all users" in status_extended + custom_result = next( + r + for r in result + if r.resource_name == "Houston Safe Attachments Policy test" + ) + assert custom_result.status == "FAIL" + assert "is not properly configured" in custom_result.status_extended + assert "all users" in custom_result.status_extended + assert "priority 0" in custom_result.status_extended diff --git a/tests/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled_test.py b/tests/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled_test.py new file mode 100644 index 0000000000..6982d1788b --- /dev/null +++ b/tests/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled_test.py @@ -0,0 +1,521 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_defender_safelinks_policy_enabled: + """Tests for the defender_safelinks_policy_enabled check.""" + + def test_no_safe_links_policies(self): + """Test when no Safe Links policies exist.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import ( + defender_safelinks_policy_enabled, + ) + + defender_client.safe_links_policies = {} + defender_client.safe_links_rules = {} + + check = defender_safelinks_policy_enabled() + result = check.execute() + assert len(result) == 0 + + def test_case1_only_builtin_policy(self): + """Case 1: Only Built-in Protection Policy exists - always PASS.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import ( + defender_safelinks_policy_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + SafeLinksPolicy, + ) + + defender_client.safe_links_policies = { + "Built-In Protection Policy": SafeLinksPolicy( + name="Built-In Protection Policy", + identity="Built-In-Protection-Policy-ID", + enable_safe_links_for_email=True, + enable_safe_links_for_teams=True, + enable_safe_links_for_office=True, + track_clicks=True, + allow_click_through=False, + scan_urls=True, + enable_for_internal_senders=True, + deliver_message_after_scan=True, + disable_url_rewrite=False, + is_built_in_protection=True, + is_default=False, + ) + } + defender_client.safe_links_rules = {} + + check = defender_safelinks_policy_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "is the only Safe Links policy" in result[0].status_extended + assert "baseline protection for all users" in result[0].status_extended + + def test_case2_builtin_and_custom_properly_configured(self): + """Case 2: Built-in + custom policy properly configured - both PASS.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import ( + defender_safelinks_policy_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + SafeLinksPolicy, + SafeLinksRule, + ) + + defender_client.safe_links_policies = { + "Built-In Protection Policy": SafeLinksPolicy( + name="Built-In Protection Policy", + identity="Built-In-Protection-Policy-ID", + enable_safe_links_for_email=True, + enable_safe_links_for_teams=True, + enable_safe_links_for_office=True, + track_clicks=True, + allow_click_through=False, + scan_urls=True, + enable_for_internal_senders=True, + deliver_message_after_scan=True, + disable_url_rewrite=False, + is_built_in_protection=True, + is_default=False, + ), + "Custom Policy": SafeLinksPolicy( + name="Custom Policy", + identity="Custom-Policy-ID", + enable_safe_links_for_email=True, + enable_safe_links_for_teams=True, + enable_safe_links_for_office=True, + track_clicks=True, + allow_click_through=False, + scan_urls=True, + enable_for_internal_senders=True, + deliver_message_after_scan=True, + disable_url_rewrite=False, + is_built_in_protection=False, + is_default=False, + ), + } + defender_client.safe_links_rules = { + "Custom Policy": SafeLinksRule( + state="Enabled", + priority=0, + users=["user@example.com"], + groups=["Engineering"], + domains=["example.com"], + ) + } + + check = defender_safelinks_policy_enabled() + result = check.execute() + + assert len(result) == 2 + + # Built-in policy PASS + builtin_result = next( + r for r in result if r.resource_name == "Built-In Protection Policy" + ) + assert builtin_result.status == "PASS" + assert ( + "provides baseline Safe Links protection" + in builtin_result.status_extended + ) + + # Custom policy PASS + custom_result = next( + r for r in result if r.resource_name == "Custom Policy" + ) + assert custom_result.status == "PASS" + assert "is properly configured" in custom_result.status_extended + assert "users: user@example.com" in custom_result.status_extended + assert "groups: Engineering" in custom_result.status_extended + assert "domains: example.com" in custom_result.status_extended + assert "priority 0" in custom_result.status_extended + + def test_case3_builtin_pass_custom_misconfigured(self): + """Case 3: Built-in PASS + custom policy misconfigured - Built-in PASS, custom FAIL.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import ( + defender_safelinks_policy_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + SafeLinksPolicy, + SafeLinksRule, + ) + + defender_client.safe_links_policies = { + "Built-In Protection Policy": SafeLinksPolicy( + name="Built-In Protection Policy", + identity="Built-In-Protection-Policy-ID", + enable_safe_links_for_email=True, + enable_safe_links_for_teams=True, + enable_safe_links_for_office=True, + track_clicks=True, + allow_click_through=False, + scan_urls=True, + enable_for_internal_senders=True, + deliver_message_after_scan=True, + disable_url_rewrite=False, + is_built_in_protection=True, + is_default=False, + ), + "Custom Policy": SafeLinksPolicy( + name="Custom Policy", + identity="Custom-Policy-ID", + enable_safe_links_for_email=False, # Misconfigured + enable_safe_links_for_teams=False, # Misconfigured + enable_safe_links_for_office=True, + track_clicks=True, + allow_click_through=True, # Misconfigured + scan_urls=True, + enable_for_internal_senders=True, + deliver_message_after_scan=True, + disable_url_rewrite=False, + is_built_in_protection=False, + is_default=False, + ), + } + defender_client.safe_links_rules = { + "Custom Policy": SafeLinksRule( + state="Enabled", + priority=0, + users=["user@example.com"], + groups=None, + domains=None, + ) + } + + check = defender_safelinks_policy_enabled() + result = check.execute() + + assert len(result) == 2 + + # Built-in policy still PASS + builtin_result = next( + r for r in result if r.resource_name == "Built-In Protection Policy" + ) + assert builtin_result.status == "PASS" + + # Custom policy FAIL + custom_result = next( + r for r in result if r.resource_name == "Custom Policy" + ) + assert custom_result.status == "FAIL" + assert "is not properly configured" in custom_result.status_extended + assert "priority 0" in custom_result.status_extended + + def test_custom_policy_without_rule_skipped(self): + """Test that custom policies without associated rules are skipped.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import ( + defender_safelinks_policy_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + SafeLinksPolicy, + SafeLinksRule, + ) + + defender_client.safe_links_policies = { + "Built-In Protection Policy": SafeLinksPolicy( + name="Built-In Protection Policy", + identity="Built-In-Protection-Policy-ID", + enable_safe_links_for_email=True, + enable_safe_links_for_teams=True, + enable_safe_links_for_office=True, + track_clicks=True, + allow_click_through=False, + scan_urls=True, + enable_for_internal_senders=True, + deliver_message_after_scan=True, + disable_url_rewrite=False, + is_built_in_protection=True, + is_default=False, + ), + "Custom Policy Without Rule": SafeLinksPolicy( + name="Custom Policy Without Rule", + identity="Custom-Policy-ID", + enable_safe_links_for_email=True, + enable_safe_links_for_teams=True, + enable_safe_links_for_office=True, + track_clicks=True, + allow_click_through=False, + scan_urls=True, + enable_for_internal_senders=True, + deliver_message_after_scan=True, + disable_url_rewrite=False, + is_built_in_protection=False, + is_default=False, + ), + } + # Rule for a different policy + defender_client.safe_links_rules = { + "Other Policy": SafeLinksRule( + state="Enabled", + priority=0, + users=["user@example.com"], + groups=None, + domains=None, + ) + } + + check = defender_safelinks_policy_enabled() + result = check.execute() + + # Only Built-in policy should be in results + assert len(result) == 1 + assert result[0].resource_name == "Built-In Protection Policy" + assert result[0].status == "PASS" + + def test_custom_policy_with_disabled_rule(self): + """Test when custom policy has proper settings but disabled rule (FAIL).""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import ( + defender_safelinks_policy_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + SafeLinksPolicy, + SafeLinksRule, + ) + + defender_client.safe_links_policies = { + "Built-In Protection Policy": SafeLinksPolicy( + name="Built-In Protection Policy", + identity="Built-In-Protection-Policy-ID", + enable_safe_links_for_email=True, + enable_safe_links_for_teams=True, + enable_safe_links_for_office=True, + track_clicks=True, + allow_click_through=False, + scan_urls=True, + enable_for_internal_senders=True, + deliver_message_after_scan=True, + disable_url_rewrite=False, + is_built_in_protection=True, + is_default=False, + ), + "Custom Policy": SafeLinksPolicy( + name="Custom Policy", + identity="Custom-Policy-ID", + enable_safe_links_for_email=True, + enable_safe_links_for_teams=True, + enable_safe_links_for_office=True, + track_clicks=True, + allow_click_through=False, + scan_urls=True, + enable_for_internal_senders=True, + deliver_message_after_scan=True, + disable_url_rewrite=False, + is_built_in_protection=False, + is_default=False, + ), + } + defender_client.safe_links_rules = { + "Custom Policy": SafeLinksRule( + state="Disabled", # Disabled rule + priority=0, + users=["user@example.com"], + groups=None, + domains=None, + ) + } + + check = defender_safelinks_policy_enabled() + result = check.execute() + + assert len(result) == 2 + + # Built-in policy PASS + builtin_result = next( + r for r in result if r.resource_name == "Built-In Protection Policy" + ) + assert builtin_result.status == "PASS" + + # Custom policy FAIL because rule is disabled + custom_result = next( + r for r in result if r.resource_name == "Custom Policy" + ) + assert custom_result.status == "FAIL" + assert "is not properly configured" in custom_result.status_extended + + def test_custom_policy_applies_to_all_users_when_no_scope(self): + """Test that custom policy with no users/groups/domains shows 'all users'.""" + defender_client = mock.MagicMock() + defender_client.audited_tenant = "audited_tenant" + defender_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.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.m365.services.defender.defender_safelinks_policy_enabled.defender_safelinks_policy_enabled import ( + defender_safelinks_policy_enabled, + ) + from prowler.providers.m365.services.defender.defender_service import ( + SafeLinksPolicy, + SafeLinksRule, + ) + + defender_client.safe_links_policies = { + "Built-In Protection Policy": SafeLinksPolicy( + name="Built-In Protection Policy", + identity="Built-In-Protection-Policy-ID", + enable_safe_links_for_email=True, + enable_safe_links_for_teams=True, + enable_safe_links_for_office=True, + track_clicks=True, + allow_click_through=False, + scan_urls=True, + enable_for_internal_senders=True, + deliver_message_after_scan=True, + disable_url_rewrite=False, + is_built_in_protection=True, + is_default=False, + ), + "Houston Safe Links Policy test": SafeLinksPolicy( + name="Houston Safe Links Policy test", + identity="Houston-Policy-ID", + enable_safe_links_for_email=False, # Misconfigured + enable_safe_links_for_teams=False, + enable_safe_links_for_office=False, + track_clicks=False, + allow_click_through=True, + scan_urls=False, + enable_for_internal_senders=False, + deliver_message_after_scan=False, + disable_url_rewrite=True, + is_built_in_protection=False, + is_default=False, + ), + } + defender_client.safe_links_rules = { + "Houston Safe Links Policy test": SafeLinksRule( + state="Enabled", + priority=0, + users=None, # No users specified + groups=None, # No groups specified + domains=None, # No domains specified - applies to ALL users + ) + } + + check = defender_safelinks_policy_enabled() + result = check.execute() + + assert len(result) == 2 + + # Custom policy should show "all users" in status_extended + custom_result = next( + r for r in result if r.resource_name == "Houston Safe Links Policy test" + ) + assert custom_result.status == "FAIL" + assert "is not properly configured" in custom_result.status_extended + assert "all users" in custom_result.status_extended + assert "priority 0" in custom_result.status_extended diff --git a/tests/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled_test.py b/tests/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled_test.py new file mode 100644 index 0000000000..3f94d40c18 --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled_test.py @@ -0,0 +1,229 @@ +"""Tests for compute_instance_config_drive_enabled check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_config_drive_enabled: + """Test suite for compute_instance_config_drive_enabled check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled import ( + compute_instance_config_drive_enabled, + ) + + check = compute_instance_config_drive_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_instance_with_config_drive(self): + """Test instance with config drive enabled (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="ConfigDrive Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=True, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled import ( + compute_instance_config_drive_enabled, + ) + + check = compute_instance_config_drive_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance ConfigDrive Instance (instance-1) has config drive enabled for secure metadata injection." + ) + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "ConfigDrive Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_without_config_drive(self): + """Test instance without config drive (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="No ConfigDrive", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled import ( + compute_instance_config_drive_enabled, + ) + + check = compute_instance_config_drive_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance No ConfigDrive (instance-2) does not have config drive enabled (relies on metadata service)." + ) + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "No ConfigDrive" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_instances_mixed(self): + """Test multiple instances with mixed config drive status.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-pass", + name="Pass", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=True, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ComputeInstance( + id="instance-fail", + name="Fail", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled import ( + compute_instance_config_drive_enabled, + ) + + check = compute_instance_config_drive_enabled() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network_test.py b/tests/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network_test.py new file mode 100644 index 0000000000..89831080ed --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network_test.py @@ -0,0 +1,601 @@ +"""Tests for compute_instance_isolated_private_network check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_isolated_private_network: + """Test suite for compute_instance_isolated_private_network check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 0 + + def test_instance_private_only(self): + """Test instance with private IP only (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="Isolated Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.5", + private_v6="", + networks={"private": ["10.0.0.5"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Isolated Instance (instance-1) is properly isolated in private network with private IPs (10.0.0.5) and no public exposure." + ) + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "Isolated Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_mixed_public_private(self): + """Test instance with both public and private IPs (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="Mixed Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="8.8.4.4", + public_v6="", + private_v4="10.0.0.10", + private_v6="", + networks={"public": ["8.8.4.4"], "private": ["10.0.0.10"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance Mixed Instance (instance-2) has mixed public and private network exposure (not properly isolated)." + ) + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "Mixed Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_public_only(self): + """Test instance with only public IP (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-3", + name="Public Only", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="1.1.1.1", + public_v6="", + private_v4="", + private_v6="", + networks={"public": ["1.1.1.1"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance Public Only (instance-3) has only public IP addresses (no private network isolation)." + ) + assert result[0].resource_id == "instance-3" + assert result[0].resource_name == "Public Only" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_no_ips(self): + """Test instance with no IPs (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-4", + name="No IPs", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance No IPs (instance-4) has no network configuration (no IPs assigned)." + ) + assert result[0].resource_id == "instance-4" + assert result[0].resource_name == "No IPs" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_private_ipv6_only(self): + """Test instance with private IPv6 only (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-5", + name="IPv6 Private", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="fd00::1", + networks={"private": ["fd00::1"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance IPv6 Private (instance-5) is properly isolated in private network with private IPs (fd00::1) and no public exposure." + ) + assert result[0].resource_id == "instance-5" + assert result[0].resource_name == "IPv6 Private" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_fallback_private_only_networks_dict(self): + """Test fallback logic: instance with private IP populated by service from networks dict.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-fallback-1", + name="Private Fallback", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", # Empty + access_ipv6="", # Empty + public_v4="", # Empty + public_v6="", # Empty + private_v4="10.99.1.207", # Populated by service fallback + private_v6="", # Empty + networks={"test-private-net": ["10.99.1.207"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "10.99.1.207" in result[0].status_extended + assert "properly isolated in private network" in result[0].status_extended + assert result[0].resource_id == "instance-fallback-1" + + def test_instance_fallback_public_only_networks_dict(self): + """Test fallback logic: instance with public IP populated by service from networks dict.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-fallback-2", + name="Public Fallback", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="8.8.8.8", # Populated by service fallback + public_v6="", # Empty + private_v4="", + private_v6="", + networks={"ext-net": ["8.8.8.8"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + "only public IP addresses" in result[0].status_extended + or "no private network isolation" in result[0].status_extended + ) + assert result[0].resource_id == "instance-fallback-2" + + def test_instance_fallback_mixed_networks_dict(self): + """Test fallback logic: instance with mixed IPs populated by service from networks dict.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-fallback-3", + name="Mixed Fallback", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="8.8.8.8", # Populated by service fallback + public_v6="", # Empty + private_v4="10.0.0.100", # Populated by service fallback + private_v6="", # Empty + networks={ + "private-net": ["10.0.0.100"], + "ext-net": ["8.8.8.8"], + }, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + "mixed public and private network exposure" in result[0].status_extended + ) + assert result[0].resource_id == "instance-fallback-3" + + def test_instance_access_ipv4_private_treated_as_private(self): + """Test that access_ipv4 set to a private IP is not treated as public exposure.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-access-priv", + name="Access Private", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="10.0.0.50", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.50", + private_v6="", + networks={"private-net": ["10.0.0.50"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "properly isolated in private network" in result[0].status_extended + assert result[0].resource_id == "instance-access-priv" + + def test_instance_network_ips_validated_as_public(self): + """Test that IPs from networks dict are validated as truly public.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-net-pub", + name="Network Public", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={ + "my-net": ["10.0.0.5", "8.8.8.8"], + }, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + "mixed public and private network exposure" in result[0].status_extended + ) + assert result[0].resource_id == "instance-net-pub" diff --git a/tests/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication_test.py b/tests/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication_test.py new file mode 100644 index 0000000000..b2e2c83edd --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication_test.py @@ -0,0 +1,229 @@ +"""Tests for compute_instance_key_based_authentication check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_key_based_authentication: + """Test suite for compute_instance_key_based_authentication check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication import ( + compute_instance_key_based_authentication, + ) + + check = compute_instance_key_based_authentication() + result = check.execute() + + assert len(result) == 0 + + def test_instance_with_keypair(self): + """Test instance with SSH keypair configured (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="Secure Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="my-production-keypair", + user_id="user-123", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.5", + private_v6="", + networks={"private": ["10.0.0.5"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication import ( + compute_instance_key_based_authentication, + ) + + check = compute_instance_key_based_authentication() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Secure Instance (instance-1) is configured with SSH key-based authentication (keypair: my-production-keypair)." + ) + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "Secure Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_without_keypair(self): + """Test instance without SSH keypair (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="Insecure Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="user-456", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.10", + private_v6="", + networks={"private": ["10.0.0.10"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication import ( + compute_instance_key_based_authentication, + ) + + check = compute_instance_key_based_authentication() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance Insecure Instance (instance-2) does not have SSH key-based authentication configured (no keypair assigned)." + ) + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "Insecure Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_instances_mixed(self): + """Test multiple instances with mixed keypair configuration.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-secure", + name="With Key", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="prod-keypair", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ComputeInstance( + id="instance-insecure", + name="Without Key", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication import ( + compute_instance_key_based_authentication, + ) + + check = compute_instance_key_based_authentication() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled_test.py b/tests/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled_test.py new file mode 100644 index 0000000000..8f52eb761e --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled_test.py @@ -0,0 +1,287 @@ +"""Tests for compute_instance_locked_status_enabled check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_locked_status_enabled: + """Test suite for compute_instance_locked_status_enabled check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled import ( + compute_instance_locked_status_enabled, + ) + + check = compute_instance_locked_status_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_instance_locked_with_reason(self): + """Test instance with locked status enabled and reason (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="Locked Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=True, + locked_reason="Production instance - do not modify", + key_name="my-keypair", + user_id="user-123", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.5", + private_v6="", + networks={"private": ["10.0.0.5"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled import ( + compute_instance_locked_status_enabled, + ) + + check = compute_instance_locked_status_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Locked Instance (instance-1) has locked status enabled (reason: Production instance - do not modify)." + ) + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "Locked Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_locked_without_reason(self): + """Test instance with locked status enabled but no reason (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="Locked No Reason", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=True, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled import ( + compute_instance_locked_status_enabled, + ) + + check = compute_instance_locked_status_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Locked No Reason (instance-2) has locked status enabled." + ) + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "Locked No Reason" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_not_locked(self): + """Test instance without locked status (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-3", + name="Unlocked Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled import ( + compute_instance_locked_status_enabled, + ) + + check = compute_instance_locked_status_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance Unlocked Instance (instance-3) does not have locked status enabled." + ) + assert result[0].resource_id == "instance-3" + assert result[0].resource_name == "Unlocked Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_instances_mixed(self): + """Test multiple instances with mixed locked status.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-locked", + name="Locked", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=True, + locked_reason="Protected", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ComputeInstance( + id="instance-unlocked", + name="Unlocked", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled import ( + compute_instance_locked_status_enabled, + ) + + check = compute_instance_locked_status_enabled() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data_test.py b/tests/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data_test.py new file mode 100644 index 0000000000..174a8ab83f --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data_test.py @@ -0,0 +1,576 @@ +"""Tests for compute_instance_metadata_sensitive_data check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_metadata_sensitive_data: + """Test suite for compute_instance_metadata_sensitive_data check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + compute_client.audit_config = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 0 + + def test_instance_no_metadata(self): + """Test instance with no metadata (PASS).""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="No Metadata", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance No Metadata (instance-1) has no metadata (no sensitive data exposure risk)." + ) + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "No Metadata" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_safe_metadata(self): + """Test instance with safe metadata (PASS).""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="Safe Metadata", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"environment": "production", "application": "web-app"}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Safe Metadata (instance-2) metadata does not contain sensitive data." + ) + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "Safe Metadata" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_password_in_metadata(self): + """Test instance with password in metadata (FAIL).""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-3", + name="Password Metadata", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"db_password": "supersecret123"}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "contains potential secrets" in result[0].status_extended + + def test_instance_api_key_in_metadata(self): + """Test instance with API key in metadata (FAIL).""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-4", + name="API Key Metadata", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"api_key": "sk-1234567890"}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended.startswith( + "Instance API Key Metadata (instance-4) metadata contains potential secrets ->" + ) + assert result[0].resource_id == "instance-4" + assert result[0].resource_name == "API Key Metadata" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_connection_string_in_metadata(self): + """Test instance with database connection string in metadata (FAIL).""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-5", + name="Connection String", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"db_url": "mysql://admin:s3cret@dbhost:3306/appdb"}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended.startswith( + "Instance Connection String (instance-5) metadata contains potential secrets ->" + ) + assert result[0].resource_id == "instance-5" + assert result[0].resource_name == "Connection String" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_private_key_in_metadata(self): + """Test instance with private key in metadata (FAIL).""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-6", + name="Private Key", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"ssh_key": "-----BEGIN RSA PRIVATE KEY-----"}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended.startswith( + "Instance Private Key (instance-6) metadata contains potential secrets ->" + ) + assert result[0].resource_id == "instance-6" + assert result[0].resource_name == "Private Key" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_instances_mixed(self): + """Test multiple instances with mixed metadata.""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-pass", + name="Safe", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"tier": "web"}, + user_data="", + trusted_image_certificates=[], + ), + ComputeInstance( + id="instance-fail", + name="Unsafe", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"admin_password": "secret123"}, + user_data="", + trusted_image_certificates=[], + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 + + def test_instance_multiple_metadata_keys_correct_identification(self): + """Test that secrets are correctly attributed to the right metadata keys.""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-7", + name="Multiple Keys", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={ + "environment": "production", + "application": "web-app", + "db_password": "supersecret123", + "region": "us-east", + }, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + # Verify the secret is correctly attributed to 'db_password' key + assert "in metadata key 'db_password'" in result[0].status_extended + assert result[0].resource_id == "instance-7" + + def test_instance_metadata_key_ordering(self): + """Test that secret detection works with different key orderings.""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-8", + name="Ordered Keys", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={ + "first_key": "safe_value", + "api_key": "sk-1234567890abcdef", + "third_key": "also_safe", + }, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + # Verify the secret is correctly attributed to 'api_key' key (second in order) + assert "in metadata key 'api_key'" in result[0].status_extended + assert result[0].resource_id == "instance-8" diff --git a/tests/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed_test.py b/tests/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed_test.py new file mode 100644 index 0000000000..583a665cfc --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed_test.py @@ -0,0 +1,708 @@ +"""Tests for compute_instance_public_ip_exposed check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_public_ip_exposed: + """Test suite for compute_instance_public_ip_exposed check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 0 + + def test_instance_without_public_ip(self): + """Test instance without public IP (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="Private Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.5", + private_v6="", + networks={"private": ["10.0.0.5"]}, # Processed from addresses + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Private Instance (instance-1) is not exposed to the internet (no public IP addresses or external network attachments detected)." + ) + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "Private Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_with_public_ipv4(self): + """Test instance with public IPv4 (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="Public Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="203.0.113.10", + public_v6="", + private_v4="10.0.0.10", + private_v6="", + networks={"public": ["203.0.113.10"], "private": ["10.0.0.10"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended.startswith( + "Instance Public Instance (instance-2) is exposed to the internet with public IP addresses:" + ) + assert "203.0.113.10" in result[0].status_extended + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "Public Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_with_access_ipv4(self): + """Test instance with access IPv4 (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-3", + name="Access IP Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="198.51.100.5", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.15", + private_v6="", + networks={"private": ["10.0.0.15"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended.startswith( + "Instance Access IP Instance (instance-3) is exposed to the internet with public IP addresses:" + ) + assert "198.51.100.5" in result[0].status_extended + assert result[0].resource_id == "instance-3" + assert result[0].resource_name == "Access IP Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_with_ipv6(self): + """Test instance with public IPv6 (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-4", + name="IPv6 Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="2001:db8::1", + public_v4="", + public_v6="", + private_v4="", + private_v6="fd00::1", + networks={"private": ["fd00::1"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended.startswith( + "Instance IPv6 Instance (instance-4) is exposed to the internet with public IP addresses:" + ) + assert "2001:db8::1" in result[0].status_extended + assert result[0].resource_id == "instance-4" + assert result[0].resource_name == "IPv6 Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_instances_mixed(self): + """Test multiple instances with mixed public IP configuration.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-pass", + name="Private", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.20", + private_v6="", + networks={"private": ["10.0.0.20"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ComputeInstance( + id="instance-fail", + name="Public", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="203.0.113.20", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 + + def test_instance_on_external_network(self): + """Test instance directly attached to external network (OVH-style).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-extnet", + name="ExtNet Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", # SDK might not populate this + public_v6="", + private_v4="", + private_v6="", + networks={ + "Ext-Net": ["57.128.163.151", "2001:41d0:801:1000::164b"] + }, # OVH external network + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "57.128.163.151" in result[0].status_extended + assert "Ext-Net" in result[0].status_extended + assert result[0].resource_id + assert result[0].resource_name + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_mixed_networks_private_and_external(self): + """Test instance with both private and external network attachments.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-mixed", + name="Mixed Networks", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.5", + private_v6="", + networks={ + "private-net": ["10.0.0.5"], + "public-network": ["8.8.8.8"], # Real public IP (Google DNS) + }, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "8.8.8.8" in result[0].status_extended + assert "public-network" in result[0].status_extended + assert result[0].resource_id + assert result[0].resource_name + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_false_positive_network_names(self): + """Test that network names containing 'ext' as substring don't cause false positives.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-context", + name="Context Network Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.100", + private_v6="", + networks={ + "context-internal": [ + "10.0.0.100" + ], # Contains "ext" but should not match + "next-hop": ["10.0.0.101"], # Contains "ext" but should not match + "text-processing": [ + "10.0.0.102" + ], # Contains "ext" but should not match + }, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Context Network Instance (instance-context) is not exposed to the internet (no public IP addresses or external network attachments detected)." + ) + assert result[0].resource_id == "instance-context" + assert result[0].resource_name == "Context Network Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_word_boundary_ext_network(self): + """Test that 'ext' as a complete word is properly detected.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-ext-word", + name="Ext Word Boundary Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={ + "ext": ["8.8.8.8"], # Word boundary: "ext" alone + "ext-network": ["1.1.1.1"], # Word boundary: "ext" at start + "network-ext": ["9.9.9.9"], # Word boundary: "ext" at end + }, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + # Should detect at least one external network with public IP + assert "ext" in result[0].status_extended.lower() + assert result[0].resource_id == "instance-ext-word" + assert result[0].resource_name == "Ext Word Boundary Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_public_ip_generic_network_name(self): + """Test that public IPs are detected regardless of network name (e.g., 'hello').""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-hello", + name="Generic Network Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="8.8.8.8", # Service populates this via fallback + public_v6="", + private_v4="", + private_v6="", + networks={"hello": ["8.8.8.8"]}, # Generic name, but public IP + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "8.8.8.8" in result[0].status_extended + assert result[0].resource_id == "instance-hello" + assert result[0].resource_name == "Generic Network Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_multiple_public_ips_on_different_networks(self): + """Test that multiple public IPs on different networks are all detected.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-multi-ip", + name="Multiple Public IPs", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="8.8.8.8", # First public IP captured by service + public_v6="", + private_v4="", + private_v6="", + networks={ + "network1": ["8.8.8.8"], # First public IP + "network2": ["1.1.1.1"], # Second public IP + "network3": ["9.9.9.9"], # Third public IP + }, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + # Should detect all three public IPs + assert "8.8.8.8" in result[0].status_extended + assert "1.1.1.1" in result[0].status_extended + assert "9.9.9.9" in result[0].status_extended + # Should show network names for additional IPs + assert "network2" in result[0].status_extended + assert "network3" in result[0].status_extended + assert result[0].resource_id == "instance-multi-ip" + assert result[0].resource_name == "Multiple Public IPs" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID diff --git a/tests/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached_test.py b/tests/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached_test.py index 35636f550e..2ef1f08e6d 100644 --- a/tests/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached_test.py +++ b/tests/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached_test.py @@ -49,6 +49,21 @@ class Test_compute_instance_security_groups_attached: security_groups=["default", "web"], region=OPENSTACK_REGION, project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], ) ] @@ -71,11 +86,14 @@ class Test_compute_instance_security_groups_attached: assert len(result) == 1 assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Instance One (instance-1) has security groups attached: default, web." + ) assert result[0].resource_id == "instance-1" assert result[0].resource_name == "Instance One" assert result[0].region == OPENSTACK_REGION assert result[0].project_id == OPENSTACK_PROJECT_ID - assert "has security groups attached" in result[0].status_extended def test_instance_without_security_groups(self): """Test instance without security groups attached (FAIL).""" @@ -89,6 +107,21 @@ class Test_compute_instance_security_groups_attached: security_groups=[], region=OPENSTACK_REGION, project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], ) ] @@ -111,14 +144,14 @@ class Test_compute_instance_security_groups_attached: assert len(result) == 1 assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance Instance Two (instance-2) does not have any security groups attached." + ) assert result[0].resource_id == "instance-2" assert result[0].resource_name == "Instance Two" assert result[0].region == OPENSTACK_REGION assert result[0].project_id == OPENSTACK_PROJECT_ID - assert ( - "does not have any security groups attached" - in result[0].status_extended - ) def test_multiple_instances_mixed(self): """Test multiple instances with mixed results.""" @@ -132,6 +165,21 @@ class Test_compute_instance_security_groups_attached: security_groups=["default"], region=OPENSTACK_REGION, project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], ), ComputeInstance( id="instance-fail", @@ -141,6 +189,21 @@ class Test_compute_instance_security_groups_attached: security_groups=[], region=OPENSTACK_REGION, project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], ), ] @@ -177,6 +240,21 @@ class Test_compute_instance_security_groups_attached: security_groups=["default"], region=OPENSTACK_REGION, project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], ) ] @@ -198,6 +276,12 @@ class Test_compute_instance_security_groups_attached: result = check.execute() assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance (instance-3) has security groups attached: default." + ) assert result[0].resource_id == "instance-3" assert result[0].resource_name == "" - assert "instance-3" in result[0].status_extended + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID diff --git a/tests/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates_test.py b/tests/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates_test.py new file mode 100644 index 0000000000..5d299027f8 --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates_test.py @@ -0,0 +1,229 @@ +"""Tests for compute_instance_trusted_image_certificates check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_trusted_image_certificates: + """Test suite for compute_instance_trusted_image_certificates check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates import ( + compute_instance_trusted_image_certificates, + ) + + check = compute_instance_trusted_image_certificates() + result = check.execute() + + assert len(result) == 0 + + def test_instance_with_trusted_certificates(self): + """Test instance with trusted image certificates (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="Trusted Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=["cert-123", "cert-456"], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates import ( + compute_instance_trusted_image_certificates, + ) + + check = compute_instance_trusted_image_certificates() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended.startswith( + "Instance Trusted Instance (instance-1) uses trusted image certificates:" + ) + assert "cert-123" in result[0].status_extended + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "Trusted Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_without_trusted_certificates(self): + """Test instance without trusted image certificates (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="Untrusted Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates import ( + compute_instance_trusted_image_certificates, + ) + + check = compute_instance_trusted_image_certificates() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance Untrusted Instance (instance-2) does not use trusted image certificates (image signature validation not enforced)." + ) + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "Untrusted Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_instances_mixed(self): + """Test multiple instances with mixed certificate configuration.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-pass", + name="Pass", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=["cert-789"], + ), + ComputeInstance( + id="instance-fail", + name="Fail", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates import ( + compute_instance_trusted_image_certificates, + ) + + check = compute_instance_trusted_image_certificates() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/compute/lib/ip_test.py b/tests/providers/openstack/services/compute/lib/ip_test.py new file mode 100644 index 0000000000..6905bc45db --- /dev/null +++ b/tests/providers/openstack/services/compute/lib/ip_test.py @@ -0,0 +1,53 @@ +"""Tests for the shared is_public_ip utility.""" + +from prowler.providers.openstack.services.compute.lib.ip import is_public_ip + + +class Test_is_public_ip: + def test_public_ipv4(self): + assert is_public_ip("8.8.8.8") + + def test_public_ipv4_other(self): + assert is_public_ip("1.1.1.1") + + def test_private_ipv4_10(self): + assert not is_public_ip("10.0.0.5") + + def test_private_ipv4_172(self): + assert not is_public_ip("172.16.0.1") + + def test_private_ipv4_192(self): + assert not is_public_ip("192.168.1.1") + + def test_loopback_ipv4(self): + assert not is_public_ip("127.0.0.1") + + def test_link_local_ipv4(self): + assert not is_public_ip("169.254.0.1") + + def test_multicast_ipv4(self): + assert not is_public_ip("224.0.0.1") + + def test_documentation_ipv4_not_global(self): + assert not is_public_ip("203.0.113.10") + + def test_public_ipv6(self): + assert is_public_ip("2001:41d0:801:1000::164b") + + def test_private_ipv6(self): + assert not is_public_ip("fd00::1") + + def test_loopback_ipv6(self): + assert not is_public_ip("::1") + + def test_link_local_ipv6(self): + assert not is_public_ip("fe80::1") + + def test_documentation_ipv6_not_global(self): + assert not is_public_ip("2001:db8::1") + + def test_invalid_ip(self): + assert not is_public_ip("not-an-ip") + + def test_empty_string(self): + assert not is_public_ip("") diff --git a/tests/providers/openstack/services/compute/openstack_compute_service_test.py b/tests/providers/openstack/services/compute/openstack_compute_service_test.py index d21ffbbb30..782572d6a1 100644 --- a/tests/providers/openstack/services/compute/openstack_compute_service_test.py +++ b/tests/providers/openstack/services/compute/openstack_compute_service_test.py @@ -44,6 +44,24 @@ class TestComputeService: mock_server1.status = "ACTIVE" mock_server1.flavor = {"id": "flavor-1"} mock_server1.security_groups = [{"name": "default"}] + mock_server1.is_locked = True + mock_server1.locked_reason = "maintenance" + mock_server1.key_name = "my-keypair" + mock_server1.user_id = "user-123" + mock_server1.access_ipv4 = "203.0.113.10" + mock_server1.access_ipv6 = "2001:db8::1" + mock_server1.public_v4 = "203.0.113.10" + mock_server1.public_v6 = "" + mock_server1.private_v4 = "10.0.0.5" + mock_server1.private_v6 = "" + mock_server1.addresses = { + "private": [{"version": 4, "addr": "10.0.0.5"}], + "public": [{"version": 4, "addr": "203.0.113.10"}], + } + mock_server1.has_config_drive = True + mock_server1.metadata = {"environment": "production"} + mock_server1.user_data = "#!/bin/bash\necho hello" + mock_server1.trusted_image_certificates = ["cert-123"] mock_server2 = MagicMock() mock_server2.id = "instance-2" @@ -51,6 +69,21 @@ class TestComputeService: mock_server2.status = "SHUTOFF" mock_server2.flavor = {"id": "flavor-2"} mock_server2.security_groups = [{"name": "web"}, {"name": "db"}] + mock_server2.is_locked = False + mock_server2.locked_reason = "" + mock_server2.key_name = "" + mock_server2.user_id = "user-456" + mock_server2.access_ipv4 = "" + mock_server2.access_ipv6 = "" + mock_server2.public_v4 = "" + mock_server2.public_v6 = "" + mock_server2.private_v4 = "10.0.0.10" + mock_server2.private_v6 = "" + mock_server2.addresses = {"private": [{"version": 4, "addr": "10.0.0.10"}]} + mock_server2.has_config_drive = False + mock_server2.metadata = {} + mock_server2.user_data = "" + mock_server2.trusted_image_certificates = [] provider.connection.compute.servers.return_value = [ mock_server1, @@ -68,8 +101,27 @@ class TestComputeService: assert compute.instances[0].security_groups == ["default"] assert compute.instances[0].region == OPENSTACK_REGION assert compute.instances[0].project_id == OPENSTACK_PROJECT_ID + assert compute.instances[0].is_locked is True + assert compute.instances[0].locked_reason == "maintenance" + assert compute.instances[0].key_name == "my-keypair" + assert compute.instances[0].user_id == "user-123" + assert compute.instances[0].access_ipv4 == "203.0.113.10" + assert compute.instances[0].access_ipv6 == "2001:db8::1" + assert compute.instances[0].public_v4 == "203.0.113.10" + assert compute.instances[0].private_v4 == "10.0.0.5" + assert compute.instances[0].networks == { + "private": ["10.0.0.5"], + "public": ["203.0.113.10"], + } + assert compute.instances[0].has_config_drive is True + assert compute.instances[0].metadata == {"environment": "production"} + assert compute.instances[0].user_data == "#!/bin/bash\necho hello" + assert compute.instances[0].trusted_image_certificates == ["cert-123"] assert compute.instances[1].security_groups == ["web", "db"] + assert compute.instances[1].is_locked is False + assert compute.instances[1].key_name == "" + assert compute.instances[1].trusted_image_certificates == [] def test_compute_list_instances_empty(self): """Test listing instances when none exist.""" @@ -90,6 +142,21 @@ class TestComputeService: del mock_server.status del mock_server.flavor del mock_server.security_groups + del mock_server.is_locked + del mock_server.locked_reason + del mock_server.key_name + del mock_server.user_id + del mock_server.access_ipv4 + del mock_server.access_ipv6 + del mock_server.public_v4 + del mock_server.public_v6 + del mock_server.private_v4 + del mock_server.private_v6 + del mock_server.addresses + del mock_server.has_config_drive + del mock_server.metadata + del mock_server.user_data + del mock_server.trusted_image_certificates provider.connection.compute.servers.return_value = [mock_server] @@ -101,6 +168,21 @@ class TestComputeService: assert compute.instances[0].status == "" assert compute.instances[0].flavor_id == "" assert compute.instances[0].security_groups == [] + assert compute.instances[0].is_locked is False + assert compute.instances[0].locked_reason == "" + assert compute.instances[0].key_name == "" + assert compute.instances[0].user_id == "" + assert compute.instances[0].access_ipv4 == "" + assert compute.instances[0].access_ipv6 == "" + assert compute.instances[0].public_v4 == "" + assert compute.instances[0].public_v6 == "" + assert compute.instances[0].private_v4 == "" + assert compute.instances[0].private_v6 == "" + assert compute.instances[0].networks == {} + assert compute.instances[0].has_config_drive is False + assert compute.instances[0].metadata == {} + assert compute.instances[0].user_data == "" + assert compute.instances[0].trusted_image_certificates == [] def test_compute_list_instances_sdk_exception(self): """Test handling SDKException when listing instances.""" @@ -133,6 +215,21 @@ class TestComputeService: mock_server.status = "ACTIVE" mock_server.flavor = {"id": "flavor-1"} mock_server.security_groups = [{"name": "default"}] + mock_server.is_locked = False + mock_server.locked_reason = "" + mock_server.key_name = "" + mock_server.user_id = "" + mock_server.access_ipv4 = "" + mock_server.access_ipv6 = "" + mock_server.public_v4 = "" + mock_server.public_v6 = "" + mock_server.private_v4 = "" + mock_server.private_v6 = "" + mock_server.addresses = {} + mock_server.has_config_drive = False + mock_server.metadata = {} + mock_server.user_data = "" + mock_server.trusted_image_certificates = [] yield mock_server raise Exception("Iterator failed") @@ -154,6 +251,23 @@ class TestComputeService: security_groups=["default"], region="RegionOne", project_id="project-1", + is_locked=True, + locked_reason="maintenance", + key_name="my-keypair", + user_id="user-123", + access_ipv4="203.0.113.10", + access_ipv6="2001:db8::1", + public_v4="203.0.113.10", + public_v6="", + private_v4="10.0.0.5", + private_v6="", + networks={ + "private": ["10.0.0.5"] + }, # Note: This is the processed dict, not addresses + has_config_drive=True, + metadata={"environment": "production"}, + user_data="#!/bin/bash\necho hello", + trusted_image_certificates=["cert-123"], ) assert instance.id == "instance-1" @@ -163,6 +277,21 @@ class TestComputeService: assert instance.security_groups == ["default"] assert instance.region == "RegionOne" assert instance.project_id == "project-1" + assert instance.is_locked is True + assert instance.locked_reason == "maintenance" + assert instance.key_name == "my-keypair" + assert instance.user_id == "user-123" + assert instance.access_ipv4 == "203.0.113.10" + assert instance.access_ipv6 == "2001:db8::1" + assert instance.public_v4 == "203.0.113.10" + assert instance.public_v6 == "" + assert instance.private_v4 == "10.0.0.5" + assert instance.private_v6 == "" + assert instance.networks == {"private": ["10.0.0.5"]} + assert instance.has_config_drive is True + assert instance.metadata == {"environment": "production"} + assert instance.user_data == "#!/bin/bash\necho hello" + assert instance.trusted_image_certificates == ["cert-123"] def test_compute_service_inherits_from_base(self): """Test Compute service inherits from OpenStackService.""" @@ -180,3 +309,37 @@ class TestComputeService: assert hasattr(compute, "identity") assert hasattr(compute, "audit_config") assert hasattr(compute, "fixer_config") + + def test_compute_list_instances_with_none_addresses(self): + """Test listing instances when addresses attribute is None.""" + provider = set_mocked_openstack_provider() + + mock_server = MagicMock() + mock_server.id = "instance-1" + mock_server.name = "Instance With None Addresses" + mock_server.status = "ACTIVE" + mock_server.flavor = {"id": "flavor-1"} + mock_server.security_groups = [{"name": "default"}] + mock_server.is_locked = False + mock_server.locked_reason = "" + mock_server.key_name = "test-key" + mock_server.user_id = "user-123" + mock_server.access_ipv4 = "" + mock_server.access_ipv6 = "" + mock_server.public_v4 = "" + mock_server.public_v6 = "" + mock_server.private_v4 = "" + mock_server.private_v6 = "" + mock_server.addresses = None # This is the key test case + mock_server.has_config_drive = False + mock_server.metadata = {} + mock_server.user_data = "" + mock_server.trusted_image_certificates = [] + + provider.connection.compute.servers.return_value = [mock_server] + + compute = Compute(provider) + + assert len(compute.instances) == 1 + assert compute.instances[0].id == "instance-1" + assert compute.instances[0].networks == {} # Should default to empty dict diff --git a/ui/.husky/pre-commit b/ui/.husky/pre-commit index db9b378299..d136d9ec54 100755 --- a/ui/.husky/pre-commit +++ b/ui/.husky/pre-commit @@ -159,6 +159,61 @@ else exit 1 fi +# Run unit tests (targeted based on staged files) +echo -e "${BLUE}🧪 Running unit tests...${NC}" +echo "" + +# Get staged source files (exclude test files) +# Note: we already cd'd into ui/, so pathspecs are relative (no ui/ prefix) +STAGED_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=ACM -- '*.ts' '*.tsx' | grep -v '\.test\.\|\.spec\.\|vitest\.config\|vitest\.setup' || true) + +# Check if critical paths changed (lib/, types/, config/) +CRITICAL_PATHS_CHANGED=$(git diff --cached --name-only -- 'lib/**' 'types/**' 'config/**' 'middleware.ts' 'vitest.config.ts' 'vitest.setup.ts' || true) + +if [ -n "$CRITICAL_PATHS_CHANGED" ]; then + echo -e "${YELLOW}Critical paths changed - running ALL unit tests${NC}" + if pnpm run test:run; then + echo "" + echo -e "${GREEN}✅ Unit tests passed${NC}" + echo "" + else + echo "" + echo -e "${RED}❌ Unit tests failed${NC}" + echo -e "${RED}Fix failing tests before committing${NC}" + echo "" + exit 1 + fi +elif [ -n "$STAGED_SOURCE_FILES" ]; then + echo -e "${YELLOW}Running tests related to changed files:${NC}" + echo "$STAGED_SOURCE_FILES" | while IFS= read -r file; do [ -n "$file" ] && echo " - $file"; done + echo "" + # shellcheck disable=SC2086 # Word splitting is intentional - vitest needs each file as separate arg + if pnpm exec vitest related $STAGED_SOURCE_FILES --run; then + echo "" + echo -e "${GREEN}✅ Unit tests passed${NC}" + echo "" + else + echo "" + echo -e "${RED}❌ Unit tests failed${NC}" + echo -e "${RED}Fix failing tests before committing${NC}" + echo "" + exit 1 + fi +else + echo -e "${YELLOW}No source files changed - running ALL unit tests${NC}" + if pnpm run test:run; then + echo "" + echo -e "${GREEN}✅ Unit tests passed${NC}" + echo "" + else + echo "" + echo -e "${RED}❌ Unit tests failed${NC}" + echo -e "${RED}Fix failing tests before committing${NC}" + echo "" + exit 1 + fi +fi + # Run build echo -e "${BLUE}🔨 Running build...${NC}" echo "" diff --git a/ui/AGENTS.md b/ui/AGENTS.md index edb3f53447..40cf77dc1a 100644 --- a/ui/AGENTS.md +++ b/ui/AGENTS.md @@ -11,6 +11,8 @@ > - [`zustand-5`](../skills/zustand-5/SKILL.md) - Selectors, persist middleware > - [`ai-sdk-5`](../skills/ai-sdk-5/SKILL.md) - UIMessage, sendMessage > - [`playwright`](../skills/playwright/SKILL.md) - Page Object Model, selectors +> - [`vitest`](../skills/vitest/SKILL.md) - Unit testing with React Testing Library +> - [`tdd`](../skills/tdd/SKILL.md) - TDD workflow (MANDATORY for UI tasks) ### Auto-invoke Skills @@ -26,16 +28,25 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Creating Zod schemas | `zod-4` | | Creating a git commit | `prowler-commit` | | Creating/modifying Prowler UI components | `prowler-ui` | +| Fixing bug | `tdd` | +| Implementing feature | `tdd` | +| Modifying component | `tdd` | +| Refactoring code | `tdd` | | Review changelog format and conventions | `prowler-changelog` | +| Testing hooks or utilities | `vitest` | | Update CHANGELOG.md in any component | `prowler-changelog` | | Using Zustand stores | `zustand-5` | | Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` | +| Working on task | `tdd` | | Working with Prowler UI test helpers/pages | `prowler-test-ui` | | Working with Tailwind classes | `tailwind-4` | | Writing Playwright E2E tests | `playwright` | | Writing Prowler UI E2E tests | `prowler-test-ui` | +| Writing React component tests | `vitest` | | Writing React components | `react-19` | | Writing TypeScript types/interfaces | `typescript` | +| Writing Vitest tests | `vitest` | +| Writing unit tests for UI | `vitest` | --- diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 7ac2c70086..ae3d1bfbba 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -2,7 +2,49 @@ All notable changes to the **Prowler UI** are documented in this file. -## [1.18.1] (Prowler UNRELEASED) +## [1.19.0] (Prowler UNRELEASED) + +### 🚀 Added + +- PDF report available for the CSA CCM compliance framework [(#10088)](https://github.com/prowler-cloud/prowler/pull/10088) +- CSV and PDF download buttons in compliance views [(#10093)](https://github.com/prowler-cloud/prowler/pull/10093) + +### 🔄 Changed + +- Attack Paths: Query list now shows their name and short description, when one is selected it also shows a longer description and an attribution if it has it [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983) +- Updated GitHub provider form placeholder to clarify both username and organization names are valid inputs [(#9830)](https://github.com/prowler-cloud/prowler/pull/9830) +- CSA CCM detailed view and small fix related with `Top Failed Sections` width [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018) +- Attack Paths: Show scan data availability status with badges and tooltips, allow selecting scans for querying while a new scan is in progress [(#10089)](https://github.com/prowler-cloud/prowler/pull/10089) + +### 🔐 Security + +- npm dependencies updated to resolve 11 Dependabot alerts (4 HIGH, 7 MEDIUM): fast-xml-parser, @modelcontextprotocol/sdk, tar, @isaacs/brace-expansion, hono, lodash, lodash-es [(#10052)](https://github.com/prowler-cloud/prowler/pull/10052) + +--- + +## [1.18.3] (Prowler UNRELEASED) + +### 🐞 Fixed + +- Dropdown selects in the "Send to Jira" modal and other dialogs not responding to clicks [(#10097)](https://github.com/prowler-cloud/prowler/pull/10097) +- Update credentials for the Alibaba Cloud provider [(#10098)](https://github.com/prowler-cloud/prowler/pull/10098) + +--- + +## [1.18.2] (Prowler v5.18.2) + +### 🐞 Fixed + +- ProviderTypeSelector crashing when an unknown provider type is missing from PROVIDER_DATA [(#9991)](https://github.com/prowler-cloud/prowler/pull/9991) +- Infinite memory loop when opening modals from table row action dropdowns due to HeroUI and Radix Dialog overlay conflict [(#9996)](https://github.com/prowler-cloud/prowler/pull/9996) +- Filter changes not coordinating with Suspense boundaries in ProviderTypeSelector, AccountsSelector, and muted findings checkbox [(#10013)](https://github.com/prowler-cloud/prowler/pull/10013) +- Scans page pagination not refreshing table data after page change [(#10013)](https://github.com/prowler-cloud/prowler/pull/10013) +- Duplicate `filter[search]` parameter in findings and scans API calls [(#10013)](https://github.com/prowler-cloud/prowler/pull/10013) +- Filters on `/findings` silently reverting on first click in production [(#10034)](https://github.com/prowler-cloud/prowler/pull/10034) + +--- + +## [1.18.1] (Prowler v5.18.1) ### 🐞 Fixed @@ -12,6 +54,10 @@ All notable changes to the **Prowler UI** are documented in this file. ## [1.18.0] (Prowler v5.18.0) +### 🚀 Added + +- Setup Vitest with React Testing Library for unit testing with targeted test execution [(#9925)](https://github.com/prowler-cloud/prowler/pull/9925) + ### 🔄 Changed - Restyle resources view with improved resource detail drawer [(#9864)](https://github.com/prowler-cloud/prowler/pull/9864) diff --git a/ui/actions/findings/findings.ts b/ui/actions/findings/findings.ts index 1fade543aa..7ce7f931ad 100644 --- a/ui/actions/findings/findings.ts +++ b/ui/actions/findings/findings.ts @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; export const getFindings = async ({ page = 1, @@ -24,9 +25,7 @@ export const getFindings = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); + appendSanitizedProviderTypeFilters(url, filters); try { const findings = await fetch(url.toString(), { @@ -62,9 +61,7 @@ export const getLatestFindings = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); + appendSanitizedProviderTypeFilters(url, filters); try { const findings = await fetch(url.toString(), { @@ -90,20 +87,13 @@ export const getMetadataInfo = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - // Define filters to exclude - const excludedFilters = [ + appendSanitizedProviderTypeFilters(url, filters, { + excludedKeyIncludes: [ "region__in", "service__in", "resource_type__in", "resource_groups__in", - ]; - if ( - key !== "filter[search]" && - !excludedFilters.some((filter) => key.includes(filter)) - ) { - url.searchParams.append(key, String(value)); - } + ], }); try { @@ -130,20 +120,13 @@ export const getLatestMetadataInfo = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - // Define filters to exclude - const excludedFilters = [ + appendSanitizedProviderTypeFilters(url, filters, { + excludedKeyIncludes: [ "region__in", "service__in", "resource_type__in", "resource_groups__in", - ]; - if ( - key !== "filter[search]" && - !excludedFilters.some((filter) => key.includes(filter)) - ) { - url.searchParams.append(key, String(value)); - } + ], }); try { diff --git a/ui/actions/overview/attack-surface/attack-surface.ts b/ui/actions/overview/attack-surface/attack-surface.ts index 342e958bc3..4a97a406d4 100644 --- a/ui/actions/overview/attack-surface/attack-surface.ts +++ b/ui/actions/overview/attack-surface/attack-surface.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { AttackSurfaceOverviewResponse } from "./types"; @@ -14,12 +15,7 @@ export const getAttackSurfaceOverview = async ({ const url = new URL(`${apiBaseUrl}/overviews/attack-surfaces`); - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/compliance-watchlist/compliance-watchlist.ts b/ui/actions/overview/compliance-watchlist/compliance-watchlist.ts index 647ee99e45..026a1b426b 100644 --- a/ui/actions/overview/compliance-watchlist/compliance-watchlist.ts +++ b/ui/actions/overview/compliance-watchlist/compliance-watchlist.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { ComplianceWatchlistResponse } from "./compliance-watchlist.types"; @@ -15,11 +16,7 @@ export const getComplianceWatchlist = async ({ // Append filter parameters (provider_id, provider_type, etc.) // Exclude filter[search] as this endpoint doesn't support text search - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { headers }); diff --git a/ui/actions/overview/findings/findings.ts b/ui/actions/overview/findings/findings.ts index 28dbbfbe8d..2aa771fb26 100644 --- a/ui/actions/overview/findings/findings.ts +++ b/ui/actions/overview/findings/findings.ts @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { FindingsSeverityOverviewResponse } from "./types"; @@ -28,17 +29,8 @@ export const getFindingsByStatus = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - // Handle multiple filters, but exclude muted filter as overviews endpoint doesn't support it - Object.entries(filters).forEach(([key, value]) => { - // The overviews/findings endpoint does not support status or muted filters - // (allowed filters include date, region, provider fields). Exclude unsupported ones. - if ( - key !== "filter[search]" && - key !== "filter[muted]" && - key !== "filter[status]" - ) { - url.searchParams.append(key, String(value)); - } + appendSanitizedProviderTypeFilters(url, filters, { + excludedKeys: ["filter[search]", "filter[muted]", "filter[status]"], }); try { @@ -62,15 +54,8 @@ export const getFindingsBySeverity = async ({ const url = new URL(`${apiBaseUrl}/overviews/findings_severity`); - // Handle multiple filters, but exclude unsupported filters - Object.entries(filters).forEach(([key, value]) => { - if ( - key !== "filter[search]" && - key !== "filter[muted]" && - value !== undefined - ) { - url.searchParams.append(key, String(value)); - } + appendSanitizedProviderTypeFilters(url, filters, { + excludedKeys: ["filter[search]", "filter[muted]"], }); try { diff --git a/ui/actions/overview/providers/providers.ts b/ui/actions/overview/providers/providers.ts index 5222856ff5..9aaec5df8c 100644 --- a/ui/actions/overview/providers/providers.ts +++ b/ui/actions/overview/providers/providers.ts @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { ProvidersOverviewResponse } from "./types"; @@ -28,11 +29,7 @@ export const getProvidersOverview = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/regions/regions.ts b/ui/actions/overview/regions/regions.ts index f32632208c..31610892ef 100644 --- a/ui/actions/overview/regions/regions.ts +++ b/ui/actions/overview/regions/regions.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { RegionsOverviewResponse } from "./types"; @@ -14,12 +15,7 @@ export const getRegionsOverview = async ({ const url = new URL(`${apiBaseUrl}/overviews/regions`); - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/resources-inventory/resources-inventory.ts b/ui/actions/overview/resources-inventory/resources-inventory.ts index 7cd353df39..264da6e375 100644 --- a/ui/actions/overview/resources-inventory/resources-inventory.ts +++ b/ui/actions/overview/resources-inventory/resources-inventory.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { ResourceGroupOverviewResponse } from "./types"; @@ -14,12 +15,7 @@ export const getResourceGroupOverview = async ({ const url = new URL(`${apiBaseUrl}/overviews/resource-groups`); - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/risk-radar/risk-radar.ts b/ui/actions/overview/risk-radar/risk-radar.ts index 44c04ad87e..419330dc18 100644 --- a/ui/actions/overview/risk-radar/risk-radar.ts +++ b/ui/actions/overview/risk-radar/risk-radar.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { CategoryOverviewResponse } from "./types"; @@ -14,12 +15,7 @@ export const getCategoryOverview = async ({ const url = new URL(`${apiBaseUrl}/overviews/categories`); - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/services/services.ts b/ui/actions/overview/services/services.ts index 3e73776dd2..1e55c04531 100644 --- a/ui/actions/overview/services/services.ts +++ b/ui/actions/overview/services/services.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { ServicesOverviewResponse } from "./types"; @@ -14,11 +15,7 @@ export const getServicesOverview = async ({ const url = new URL(`${apiBaseUrl}/overviews/services`); - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]" && value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/severity-trends/severity-trends.ts b/ui/actions/overview/severity-trends/severity-trends.ts index 70701a41a8..e90efa15b7 100644 --- a/ui/actions/overview/severity-trends/severity-trends.ts +++ b/ui/actions/overview/severity-trends/severity-trends.ts @@ -5,6 +5,7 @@ import { type TimeRange, } from "@/app/(prowler)/_overview/severity-over-time/_constants/time-range.constants"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { adaptSeverityTrendsResponse } from "./severity-trends.adapter"; @@ -27,11 +28,7 @@ const getFindingsSeverityTrends = async ({ const url = new URL(`${apiBaseUrl}/overviews/findings_severity/timeseries`); - Object.entries(filters).forEach(([key, value]) => { - if (value !== undefined) { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/overview/threat-score/threat-score.ts b/ui/actions/overview/threat-score/threat-score.ts index 3a78d1ecda..0a1d7505d1 100644 --- a/ui/actions/overview/threat-score/threat-score.ts +++ b/ui/actions/overview/threat-score/threat-score.ts @@ -1,6 +1,7 @@ "use server"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; export const getThreatScore = async ({ @@ -12,12 +13,7 @@ export const getThreatScore = async ({ const url = new URL(`${apiBaseUrl}/overviews/threatscore`); - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]") { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/providers/providers.ts b/ui/actions/providers/providers.ts index 4665e9db39..63c01332b1 100644 --- a/ui/actions/providers/providers.ts +++ b/ui/actions/providers/providers.ts @@ -6,6 +6,7 @@ import { redirect } from "next/navigation"; import { apiBaseUrl, getAuthHeaders, getFormValue, wait } from "@/lib"; import { buildSecretConfig } from "@/lib/provider-credentials/build-crendentials"; import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; +import { appendSanitizedProviderInFilters } from "@/lib/provider-filters"; import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper"; import { ProvidersApiResponse, ProviderType } from "@/types/providers"; @@ -15,6 +16,12 @@ export const getProviders = async ({ sort = "", filters = {}, pageSize = 10, +}: { + page?: number; + query?: string; + sort?: string; + filters?: Record; + pageSize?: number; }): Promise => { const headers = await getAuthHeaders({ contentType: false }); @@ -27,12 +34,7 @@ export const getProviders = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]") { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderInFilters(url, filters); try { const response = await fetch(url.toString(), { @@ -60,7 +62,7 @@ export const getAllProviders = async ({ }: { query?: string; sort?: string; - filters?: Record; + filters?: Record; } = {}): Promise => { const headers = await getAuthHeaders({ contentType: false }); const pageSize = 100; // Use larger page size to minimize API calls @@ -79,11 +81,7 @@ export const getAllProviders = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]") { - url.searchParams.append(key, String(value)); - } - }); + appendSanitizedProviderInFilters(url, filters); const response = await fetch(url.toString(), { headers }); const data = (await handleApiResponse(response)) as diff --git a/ui/actions/resources/resources.ts b/ui/actions/resources/resources.ts index f7b0a1b002..d65505704f 100644 --- a/ui/actions/resources/resources.ts +++ b/ui/actions/resources/resources.ts @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; export const getResources = async ({ @@ -17,7 +18,7 @@ export const getResources = async ({ page?: number; query?: string; sort?: string; - filters?: Record; + filters?: Record; pageSize?: number; include?: string; fields?: string[]; @@ -38,9 +39,7 @@ export const getResources = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { @@ -66,7 +65,7 @@ export const getLatestResources = async ({ page?: number; query?: string; sort?: string; - filters?: Record; + filters?: Record; pageSize?: number; include?: string; fields?: string[]; @@ -87,9 +86,7 @@ export const getLatestResources = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { @@ -107,6 +104,10 @@ export const getMetadataInfo = async ({ query = "", sort = "", filters = {}, +}: { + query?: string; + sort?: string; + filters?: Record; }) => { const headers = await getAuthHeaders({ contentType: false }); @@ -115,9 +116,7 @@ export const getMetadataInfo = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { @@ -135,6 +134,10 @@ export const getLatestMetadataInfo = async ({ query = "", sort = "", filters = {}, +}: { + query?: string; + sort?: string; + filters?: Record; }) => { const headers = await getAuthHeaders({ contentType: false }); @@ -143,9 +146,7 @@ export const getLatestMetadataInfo = async ({ if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); - Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { diff --git a/ui/actions/scans/scans.ts b/ui/actions/scans/scans.ts index ccbd2448c0..52da71bb3b 100644 --- a/ui/actions/scans/scans.ts +++ b/ui/actions/scans/scans.ts @@ -7,6 +7,10 @@ import { COMPLIANCE_REPORT_DISPLAY_NAMES, type ComplianceReportType, } from "@/lib/compliance/compliance-report-types"; +import { + appendSanitizedProviderTypeFilters, + sanitizeProviderTypesCsv, +} from "@/lib/provider-filters"; import { addScanOperation } from "@/lib/sentry-breadcrumbs"; import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper"; export const getScans = async ({ @@ -35,10 +39,7 @@ export const getScans = async ({ url.searchParams.append(`fields[${key}]`, String(value)); }); - // Add dynamic filters (e.g., "filter[state]", "fields[scans]") - Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); + appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { headers }); @@ -55,6 +56,10 @@ export const getScansByState = async () => { const url = new URL(`${apiBaseUrl}/scans`); // Request only the necessary fields to optimize the response url.searchParams.append("fields[scans]", "state"); + url.searchParams.append( + "filter[provider_type__in]", + sanitizeProviderTypesCsv(), + ); try { const response = await fetch(url.toString(), { diff --git a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx index dce5f049dc..98bb9df9f5 100644 --- a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import { ReactNode } from "react"; import { @@ -22,6 +22,7 @@ import { MultiSelectTrigger, MultiSelectValue, } from "@/components/shadcn/select/multiselect"; +import { useUrlFilters } from "@/hooks/use-url-filters"; import type { ProviderProps, ProviderType } from "@/types/providers"; const PROVIDER_ICON: Record = { @@ -42,8 +43,8 @@ interface AccountsSelectorProps { } export function AccountsSelector({ providers }: AccountsSelectorProps) { - const router = useRouter(); const searchParams = useSearchParams(); + const { navigateWithParams } = useUrlFilters(); const filterKey = "filter[provider_id__in]"; const current = searchParams.get(filterKey) || ""; @@ -61,38 +62,37 @@ export function AccountsSelector({ providers }: AccountsSelectorProps) { ); const handleMultiValueChange = (ids: string[]) => { - const params = new URLSearchParams(searchParams.toString()); - params.delete(filterKey); + navigateWithParams((params) => { + params.delete(filterKey); - if (ids.length > 0) { - params.set(filterKey, ids.join(",")); - } - - // Auto-deselect provider types that no longer have any selected accounts - if (selectedTypesList.length > 0) { - // Get provider types of currently selected accounts - const selectedProviders = providers.filter((p) => ids.includes(p.id)); - const selectedProviderTypes = new Set( - selectedProviders.map((p) => p.attributes.provider), - ); - - // Keep only provider types that still have selected accounts - const remainingProviderTypes = selectedTypesList.filter((type) => - selectedProviderTypes.has(type as ProviderType), - ); - - // Update provider_type__in filter - if (remainingProviderTypes.length > 0) { - params.set( - "filter[provider_type__in]", - remainingProviderTypes.join(","), - ); - } else { - params.delete("filter[provider_type__in]"); + if (ids.length > 0) { + params.set(filterKey, ids.join(",")); } - } - router.push(`?${params.toString()}`, { scroll: false }); + // Auto-deselect provider types that no longer have any selected accounts + if (selectedTypesList.length > 0) { + // Get provider types of currently selected accounts + const selectedProviders = providers.filter((p) => ids.includes(p.id)); + const selectedProviderTypes = new Set( + selectedProviders.map((p) => p.attributes.provider), + ); + + // Keep only provider types that still have selected accounts + const remainingProviderTypes = selectedTypesList.filter((type) => + selectedProviderTypes.has(type as ProviderType), + ); + + // Update provider_type__in filter + if (remainingProviderTypes.length > 0) { + params.set( + "filter[provider_type__in]", + remainingProviderTypes.join(","), + ); + } else { + params.delete("filter[provider_type__in]"); + } + } + }); }; const selectedLabel = () => { diff --git a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx index e5eb7928f3..d9457a499b 100644 --- a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import { lazy, Suspense } from "react"; import { @@ -10,6 +10,7 @@ import { MultiSelectTrigger, MultiSelectValue, } from "@/components/shadcn/select/multiselect"; +import { useUrlFilters } from "@/hooks/use-url-filters"; import { type ProviderProps, ProviderType } from "@/types/providers"; const AWSProviderBadge = lazy(() => @@ -122,8 +123,8 @@ type ProviderTypeSelectorProps = { export const ProviderTypeSelector = ({ providers, }: ProviderTypeSelectorProps) => { - const router = useRouter(); const searchParams = useSearchParams(); + const { navigateWithParams } = useUrlFilters(); const currentProviders = searchParams.get("filter[provider_type__in]") || ""; const selectedTypes = currentProviders @@ -131,20 +132,18 @@ export const ProviderTypeSelector = ({ : []; const handleMultiValueChange = (values: string[]) => { - const params = new URLSearchParams(searchParams.toString()); + navigateWithParams((params) => { + // Update provider_type__in + if (values.length > 0) { + params.set("filter[provider_type__in]", values.join(",")); + } else { + params.delete("filter[provider_type__in]"); + } - // Update provider_type__in - if (values.length > 0) { - params.set("filter[provider_type__in]", values.join(",")); - } else { - params.delete("filter[provider_type__in]"); - } - - // Clear account selection when changing provider types - // User should manually select accounts if they want to filter by specific accounts - params.delete("filter[provider_id__in]"); - - router.push(`?${params.toString()}`, { scroll: false }); + // Clear account selection when changing provider types + // User should manually select accounts if they want to filter by specific accounts + params.delete("filter[provider_id__in]"); + }); }; const availableTypes = Array.from( @@ -153,7 +152,7 @@ export const ProviderTypeSelector = ({ // .filter((p) => p.attributes.connection?.connected) .map((p) => p.attributes.provider), ), - ) as ProviderType[]; + ).filter((type): type is ProviderType => type in PROVIDER_DATA); const renderIcon = (providerType: ProviderType) => { const IconComponent = PROVIDER_DATA[providerType].icon; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-selector.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-selector.tsx index 65acb8a84a..f7d36c9a68 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-selector.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-selector.tsx @@ -35,7 +35,7 @@ export const QuerySelector = ({
{query.attributes.name} - {query.attributes.description} + {query.attributes.short_description}
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.tsx index 8e10305617..5cf3749265 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-list-table.tsx @@ -30,7 +30,7 @@ import { } from "@/components/ui/table"; import { cn } from "@/lib/utils"; import type { ProviderType } from "@/types"; -import type { AttackPathScan } from "@/types/attack-paths"; +import type { AttackPathScan, ScanState } from "@/types/attack-paths"; import { SCAN_STATES } from "@/types/attack-paths"; import { ScanStatusBadge } from "./scan-status-badge"; @@ -42,6 +42,11 @@ interface ScanListTableProps { const TABLE_COLUMN_COUNT = 6; const DEFAULT_PAGE_SIZE = 5; const PAGE_SIZE_OPTIONS = [2, 5, 10, 15]; +const WAITING_STATES: readonly ScanState[] = [ + SCAN_STATES.SCHEDULED, + SCAN_STATES.AVAILABLE, + SCAN_STATES.EXECUTING, +]; const baseLinkClass = "relative block rounded border-0 bg-transparent px-3 py-1.5 text-button-primary outline-none transition-all duration-300 hover:bg-bg-neutral-tertiary hover:text-text-neutral-primary focus:shadow-none dark:hover:bg-bg-neutral-secondary dark:hover:text-text-neutral-primary"; @@ -77,20 +82,17 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => { }; const isSelectDisabled = (scan: AttackPathScan) => { - return ( - scan.attributes.state !== SCAN_STATES.COMPLETED || - selectedScanId === scan.id - ); + return !scan.attributes.graph_data_ready || selectedScanId === scan.id; }; const getSelectButtonLabel = (scan: AttackPathScan) => { if (selectedScanId === scan.id) { return "Selected"; } - if (scan.attributes.state === SCAN_STATES.SCHEDULED) { - return "Scheduled"; + if (scan.attributes.graph_data_ready) { + return "Select"; } - if (scan.attributes.state === SCAN_STATES.EXECUTING) { + if (WAITING_STATES.includes(scan.attributes.state)) { return "Waiting..."; } if (scan.attributes.state === SCAN_STATES.FAILED) { @@ -184,6 +186,7 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => { @@ -342,8 +345,8 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => { )}

- Only Attack Paths scans with "Completed" status can be - selected. Scans in progress will update automatically. + Scans can be selected when data is available. A new scan does not + interrupt access to existing data.

); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-status-badge.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-status-badge.tsx index 74c7302126..de8833749e 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-status-badge.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/scan-status-badge.tsx @@ -3,57 +3,94 @@ import { Loader2 } from "lucide-react"; import { Badge } from "@/components/shadcn/badge/badge"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/shadcn/tooltip"; import type { ScanState } from "@/types/attack-paths"; +import { SCAN_STATES } from "@/types/attack-paths"; + +const BADGE_CONFIG: Record< + ScanState, + { className: string; label: string; showGraphDot: boolean } +> = { + [SCAN_STATES.SCHEDULED]: { + className: "bg-bg-neutral-tertiary text-text-neutral-primary", + label: "Scheduled", + showGraphDot: true, + }, + [SCAN_STATES.AVAILABLE]: { + className: "bg-bg-neutral-tertiary text-text-neutral-primary", + label: "Queued", + showGraphDot: true, + }, + [SCAN_STATES.EXECUTING]: { + className: "bg-bg-warning-secondary text-text-neutral-primary", + label: "In Progress", + showGraphDot: false, + }, + [SCAN_STATES.COMPLETED]: { + className: "bg-bg-pass-secondary text-text-success-primary", + label: "Completed", + showGraphDot: false, + }, + [SCAN_STATES.FAILED]: { + className: "bg-bg-fail-secondary text-text-error-primary", + label: "Failed", + showGraphDot: true, + }, +}; interface ScanStatusBadgeProps { status: ScanState; progress?: number; + graphDataReady?: boolean; } -/** - * Status badge for attack path scan status - * Shows visual indicator and text for scan progress - */ export const ScanStatusBadge = ({ status, progress = 0, + graphDataReady = false, }: ScanStatusBadgeProps) => { - if (status === "scheduled") { - return ( - - Scheduled - - ); - } + const config = BADGE_CONFIG[status]; - if (status === "available") { - return ( - - Queued - - ); - } + const graphDot = graphDataReady && config.showGraphDot && ( + + ); - if (status === "executing") { - return ( - - - In Progress ({progress}%) - - ); - } + const tooltipText = graphDataReady + ? "Graph available" + : status === SCAN_STATES.FAILED || status === SCAN_STATES.COMPLETED + ? "Graph not available" + : "Graph not available yet"; - if (status === "completed") { - return ( - - Completed - + const icon = + status === SCAN_STATES.EXECUTING ? ( + + ) : ( + graphDot ); - } + + const label = + status === SCAN_STATES.EXECUTING + ? `${config.label} (${progress}%)` + : config.label; return ( - - Failed - + + + + {icon} + {label} + + + {tooltipText} + ); }; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/page.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/page.tsx index a817b9a715..ff66a7e68f 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/page.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/page.tsx @@ -340,6 +340,33 @@ export default function AttackPathAnalysisPage() { onQueryChange={handleQueryChange} /> + {queryBuilder.selectedQueryData && ( +
+

+ {queryBuilder.selectedQueryData.attributes.description} +

+ {queryBuilder.selectedQueryData.attributes.attribution && ( +

+ Source:{" "} + + { + queryBuilder.selectedQueryData.attributes + .attribution.text + } + +

+ )} +
+ )} + {queryBuilder.selectedQuery && ( - {(() => { - const framework = attributesData?.data?.[0]?.attributes?.framework; - const reportType = getReportTypeForFramework(framework); - - return selectedScanId && reportType ? ( -
- -
- ) : null; - })()} + {selectedScanId && ( +
+ +
+ )} {isThreatScore && ( @@ -209,7 +207,7 @@ const SSRComplianceContent = async ({ if (!scanId || type === "tasks") { return (
-
+
{/* */} @@ -244,8 +242,9 @@ const SSRComplianceContent = async ({ {/* Mobile: each card on own row | Tablet: ThreatScore full row, others share row | Desktop: all 3 in one row */}
{threatScoreData && ( diff --git a/ui/app/(prowler)/demo-expandable-table/page.tsx b/ui/app/(prowler)/demo-expandable-table/page.tsx new file mode 100644 index 0000000000..b97770f1a6 --- /dev/null +++ b/ui/app/(prowler)/demo-expandable-table/page.tsx @@ -0,0 +1,214 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { CloudIcon, FolderIcon, ServerIcon } from "lucide-react"; +import { notFound } from "next/navigation"; + +import { DataTable } from "@/components/ui/table/data-table"; +import { DataTableExpandAllToggle } from "@/components/ui/table/data-table-expand-all-toggle"; +import { DataTableExpandableCell } from "@/components/ui/table/data-table-expandable-cell"; + +/** + * Demo page for the Expandable DataTable component. + * Only accessible in development mode. + * + * Showcases: + * 1. Hierarchical rows with expand/collapse + * 2. Expand all / collapse all toggle + * 3. Row selection with child auto-selection + */ + +const IS_DEV = process.env.NODE_ENV === "development"; + +// Type constants following project conventions +const PROVIDER_TYPES = { + ORGANIZATION: "organization", + OU: "ou", + ACCOUNT: "account", +} as const; +type ProviderType = (typeof PROVIDER_TYPES)[keyof typeof PROVIDER_TYPES]; + +const PROVIDER_STATUSES = { + CONNECTED: "connected", + DISCONNECTED: "disconnected", + PENDING: "pending", +} as const; +type ProviderStatus = + (typeof PROVIDER_STATUSES)[keyof typeof PROVIDER_STATUSES]; + +interface HierarchicalProvider { + id: string; + name: string; + type: ProviderType; + status: ProviderStatus; + resourceCount: number; + children?: HierarchicalProvider[]; +} + +const tableData: HierarchicalProvider[] = [ + { + id: "org-1", + name: "AWS Organization", + type: PROVIDER_TYPES.ORGANIZATION, + status: PROVIDER_STATUSES.CONNECTED, + resourceCount: 1250, + children: [ + { + id: "ou-prod", + name: "Production OU", + type: PROVIDER_TYPES.OU, + status: PROVIDER_STATUSES.CONNECTED, + resourceCount: 800, + children: [ + { + id: "acc-prod-1", + name: "prod-web-services", + type: PROVIDER_TYPES.ACCOUNT, + status: PROVIDER_STATUSES.CONNECTED, + resourceCount: 450, + }, + { + id: "acc-prod-2", + name: "prod-databases", + type: PROVIDER_TYPES.ACCOUNT, + status: PROVIDER_STATUSES.CONNECTED, + resourceCount: 350, + }, + ], + }, + { + id: "ou-dev", + name: "Development OU", + type: PROVIDER_TYPES.OU, + status: PROVIDER_STATUSES.CONNECTED, + resourceCount: 450, + children: [ + { + id: "acc-dev-1", + name: "dev-sandbox", + type: PROVIDER_TYPES.ACCOUNT, + status: PROVIDER_STATUSES.PENDING, + resourceCount: 200, + }, + { + id: "acc-dev-2", + name: "dev-testing", + type: PROVIDER_TYPES.ACCOUNT, + status: PROVIDER_STATUSES.DISCONNECTED, + resourceCount: 250, + }, + ], + }, + ], + }, +]; + +const STATUS_COLORS = { + connected: "text-green-500", + disconnected: "text-red-500", + pending: "text-yellow-500", +} as const; + +const TYPE_ICONS = { + organization: ServerIcon, + ou: FolderIcon, + account: CloudIcon, +} as const; + +const columns: ColumnDef[] = [ + { + accessorKey: "name", + size: 300, + header: ({ table }) => ( +
+ + Name +
+ ), + cell: ({ row }) => { + const Icon = TYPE_ICONS[row.original.type]; + return ( + +
+ + {row.original.name} +
+
+ ); + }, + }, + { + accessorKey: "type", + size: 120, + header: "Type", + cell: ({ row }) => {row.original.type}, + }, + { + accessorKey: "status", + size: 120, + header: "Status", + cell: ({ row }) => ( + + {row.original.status} + + ), + }, + { + accessorKey: "resourceCount", + size: 100, + header: "Resources", + cell: ({ row }) => row.original.resourceCount.toLocaleString(), + }, +]; + +export default function DemoExpandableTablePage() { + if (!IS_DEV) { + notFound(); + } + + return ( +
+

Expandable DataTable Demo

+ + {/* Expandable DataTable */} +
+
+

Expandable DataTable

+

+ Table with hierarchical rows. Click chevron to expand/collapse, or + use the header icon to expand/collapse all. +

+
+ + row.children} + defaultExpanded={true} + /> +
+ + {/* Expandable DataTable with Row Selection */} +
+
+

+ Expandable DataTable with Row Selection +

+

+ Selecting a parent auto-selects all children + (enableSubRowSelection). +

+
+ + row.children} + enableRowSelection + enableSubRowSelection + defaultExpanded={{ "org-1": true, "ou-prod": true }} + /> +
+
+ ); +} diff --git a/ui/app/(prowler)/demo-tree-view/page.tsx b/ui/app/(prowler)/demo-tree-view/page.tsx new file mode 100644 index 0000000000..353aeccc57 --- /dev/null +++ b/ui/app/(prowler)/demo-tree-view/page.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { CloudIcon, FolderIcon, ServerIcon } from "lucide-react"; +import { notFound } from "next/navigation"; +import { useState } from "react"; + +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/shadcn/tooltip"; +import { TreeView } from "@/components/shadcn/tree-view"; +import { TreeDataItem } from "@/types/tree"; + +/** + * Demo page for the TreeView component. + * + * ⚠️ DEVELOPMENT ONLY - This page is for component demonstration and testing. + * It returns 404 in production environments. + * + * Showcases: + * 1. TreeView with checkboxes and hierarchical selection + * 2. TreeView without checkboxes (navigation mode) + * 3. Custom rendering with renderItem prop + */ + +// Hide in production - evaluated at build time +const IS_DEV = process.env.NODE_ENV === "development"; + +const accountsTreeData: TreeDataItem[] = [ + { + id: "org-1", + name: "Organization Root", + icon: ServerIcon, + children: [ + { + id: "ou-1", + name: "ou-996789098 (Production)", + icon: FolderIcon, + status: "success", + children: [ + { + id: "acc-1", + name: "123456789098", + icon: CloudIcon, + status: "success", + }, + { + id: "acc-2", + name: "123456789099", + icon: CloudIcon, + status: "success", + }, + { + id: "acc-3", + name: "123456789100", + icon: CloudIcon, + status: "success", + }, + ], + }, + { + id: "ou-2", + name: "ou-996789099 (Development)", + icon: FolderIcon, + status: "error", + children: [ + { + id: "acc-4", + name: "223456789098", + icon: CloudIcon, + status: "success", + }, + { + id: "acc-5", + name: "223456789099", + icon: CloudIcon, + status: "error", + }, + ], + }, + { + id: "ou-3", + name: "ou-996789100 (Staging)", + icon: FolderIcon, + isLoading: true, + children: [{ id: "acc-6", name: "323456789098", icon: CloudIcon }], + }, + ], + }, +]; + +export default function DemoTreeViewPage() { + // Return 404 in production - this page is for development only + if (!IS_DEV) { + notFound(); + } + + const [selectedIds, setSelectedIds] = useState([]); + const [expandedIds, setExpandedIds] = useState(["org-1"]); + + return ( +
+

TreeView Component Demo

+ + {/* TreeView with Checkboxes */} +
+
+

+ TreeView with Checkboxes (Account Selector) +

+

+ Select accounts hierarchically. Selecting a parent selects all + children. +

+
+ +
+
+ ( +
+ {item.icon && } + + + {item.name} + + {item.name} + + {hasChildren && !isLeaf && ( + + {item.children?.length} + + )} +
+ )} + /> +
+ +
+

Selected IDs:

+
+              {JSON.stringify(selectedIds, null, 2)}
+            
+
+
+
+ + {/* TreeView without Checkboxes */} +
+
+

+ TreeView without Checkboxes (Navigation) +

+

+ Click to expand/collapse. Use arrow keys to navigate. +

+
+ +
+ +
+
+
+ ); +} diff --git a/ui/app/(prowler)/mutelist/_components/simple/mute-rule-row-actions.tsx b/ui/app/(prowler)/mutelist/_components/simple/mute-rule-row-actions.tsx index f57b9ffbe4..70f437d967 100644 --- a/ui/app/(prowler)/mutelist/_components/simple/mute-rule-row-actions.tsx +++ b/ui/app/(prowler)/mutelist/_components/simple/mute-rule-row-actions.tsx @@ -1,17 +1,15 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; import { Pencil, Trash2 } from "lucide-react"; import { MuteRuleData } from "@/actions/mute-rules/types"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; interface MuteRuleRowActionsProps { muteRule: MuteRuleData; @@ -26,11 +24,8 @@ export function MuteRuleRowActions({ }: MuteRuleRowActionsProps) { return (
- - + - - - - - } - onPress={() => onEdit(muteRule)} - > - Edit - - - } - onPress={() => onDelete(muteRule)} - > - Delete - - - - + } + > + } + label="Edit Mute Rule" + onSelect={() => onEdit(muteRule)} + /> + + } + label="Delete Mute Rule" + destructive + onSelect={() => onDelete(muteRule)} + /> + +
); } diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx index a9baafa75b..d700821ed6 100644 --- a/ui/components/compliance/compliance-card.tsx +++ b/ui/components/compliance/compliance-card.tsx @@ -3,14 +3,13 @@ import { Progress } from "@heroui/progress"; import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; -import { useState } from "react"; import { Card, CardContent } from "@/components/shadcn/card/card"; -import { DownloadIconButton, toast } from "@/components/ui"; -import { downloadComplianceCsv } from "@/lib/helper"; +import { getReportTypeForFramework } from "@/lib/compliance/compliance-report-types"; import { ScanEntity } from "@/types/scans"; import { getComplianceIcon } from "../icons"; +import { ComplianceDownloadContainer } from "./compliance-download-container"; interface ComplianceCardProps { title: string; @@ -38,7 +37,6 @@ export const ComplianceCard: React.FC = ({ const searchParams = useSearchParams(); const router = useRouter(); const hasRegionFilter = searchParams.has("filter[region__in]"); - const [isDownloading, setIsDownloading] = useState(false); const formatTitle = (title: string) => { return title.split("-").join(" "); @@ -85,14 +83,6 @@ export const ComplianceCard: React.FC = ({ router.push(`${path}?${params.toString()}`); }; - const handleDownload = async () => { - setIsDownloading(true); - try { - await downloadComplianceCsv(scanId, complianceId, toast); - } finally { - setIsDownloading(false); - } - }; return ( = ({ e.stopPropagation(); } }} - role="button" + role="group" tabIndex={0} > -
diff --git a/ui/components/compliance/compliance-custom-details/csa-details.tsx b/ui/components/compliance/compliance-custom-details/csa-details.tsx new file mode 100644 index 0000000000..738e4a8aab --- /dev/null +++ b/ui/components/compliance/compliance-custom-details/csa-details.tsx @@ -0,0 +1,93 @@ +import { cn } from "@/lib"; +import { CSA_MAPPING_SECTIONS } from "@/lib/compliance/csa"; +import { Requirement } from "@/types/compliance"; + +import { + ComplianceBadge, + ComplianceBadgeContainer, + ComplianceDetailContainer, + ComplianceDetailSection, + ComplianceDetailText, +} from "./shared-components"; + +interface CSADetailsProps { + requirement: Requirement; +} + +export const CSACustomDetails = ({ requirement }: CSADetailsProps) => { + const mappingSections = CSA_MAPPING_SECTIONS.map((section) => ({ + ...section, + data: requirement[section.key] as Array<{ + ReferenceId: string; + Identifiers: string[]; + }>, + })).filter((section) => section.data && section.data.length > 0); + + return ( + + {requirement.description && ( + + {requirement.description} + + )} + + + {requirement.ccm_lite && ( + + )} + {requirement.iaas && ( + + )} + {requirement.paas && ( + + )} + {requirement.saas && ( + + )} + + + {mappingSections.map((section) => ( + +
+ {section.data.map((mapping, index) => ( +
+ + {mapping.ReferenceId} + +
+ {mapping.Identifiers.map((identifier, idx) => ( + + {identifier} + + ))} +
+
+ ))} +
+
+ ))} +
+ ); +}; diff --git a/ui/components/compliance/compliance-download-button.tsx b/ui/components/compliance/compliance-download-button.tsx deleted file mode 100644 index 50e7be0ec4..0000000000 --- a/ui/components/compliance/compliance-download-button.tsx +++ /dev/null @@ -1,101 +0,0 @@ -"use client"; - -import { DownloadIcon } from "lucide-react"; -import { useState } from "react"; - -import { Button } from "@/components/shadcn/button/button"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/shadcn/tooltip"; -import { toast } from "@/components/ui"; -import { - COMPLIANCE_REPORT_BUTTON_LABELS, - type ComplianceReportType, -} from "@/lib/compliance/compliance-report-types"; -import { downloadComplianceReportPdf } from "@/lib/helper"; - -interface ComplianceDownloadButtonProps { - scanId: string; - reportType: ComplianceReportType; - label?: string; - /** Show only icon with tooltip on mobile (sm and below) */ - iconOnlyOnMobile?: boolean; -} - -export const ComplianceDownloadButton = ({ - scanId, - reportType, - label, - iconOnlyOnMobile = false, -}: ComplianceDownloadButtonProps) => { - const [isDownloading, setIsDownloading] = useState(false); - - const handleDownload = async () => { - setIsDownloading(true); - try { - await downloadComplianceReportPdf(scanId, reportType, toast); - } finally { - setIsDownloading(false); - } - }; - - const defaultLabel = COMPLIANCE_REPORT_BUTTON_LABELS[reportType]; - const buttonLabel = label || defaultLabel; - - if (iconOnlyOnMobile) { - return ( - <> - {/* Mobile and Tablet: Icon only with tooltip */} - - - - - {buttonLabel} - - {/* Desktop: Full button with label */} - - - ); - } - - return ( - - ); -}; diff --git a/ui/components/compliance/compliance-download-container.tsx b/ui/components/compliance/compliance-download-container.tsx new file mode 100644 index 0000000000..526057ea5c --- /dev/null +++ b/ui/components/compliance/compliance-download-container.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { DownloadIcon, FileTextIcon } from "lucide-react"; +import { useState } from "react"; + +import { Button } from "@/components/shadcn/button/button"; +import { toast } from "@/components/ui"; +import type { ComplianceReportType } from "@/lib/compliance/compliance-report-types"; +import { + downloadComplianceCsv, + downloadComplianceReportPdf, +} from "@/lib/helper"; +import { cn } from "@/lib/utils"; + +interface ComplianceDownloadContainerProps { + scanId: string; + complianceId: string; + reportType?: ComplianceReportType; + compact?: boolean; + disabled?: boolean; +} + +export const ComplianceDownloadContainer = ({ + scanId, + complianceId, + reportType, + compact = false, + disabled = false, +}: ComplianceDownloadContainerProps) => { + const [isDownloadingCsv, setIsDownloadingCsv] = useState(false); + const [isDownloadingPdf, setIsDownloadingPdf] = useState(false); + + const handleDownloadCsv = async () => { + if (isDownloadingCsv) return; + setIsDownloadingCsv(true); + try { + await downloadComplianceCsv(scanId, complianceId, toast); + } finally { + setIsDownloadingCsv(false); + } + }; + + const handleDownloadPdf = async () => { + if (!reportType || isDownloadingPdf) return; + setIsDownloadingPdf(true); + try { + await downloadComplianceReportPdf(scanId, reportType, toast); + } finally { + setIsDownloadingPdf(false); + } + }; + + const buttonClassName = cn( + "border-button-primary text-button-primary hover:bg-button-primary/10", + compact && "h-7 px-2 text-xs", + ); + + return ( +
+ + {reportType && ( + + )} +
+ ); +}; diff --git a/ui/components/compliance/index.ts b/ui/components/compliance/index.ts index 54befb9dd6..3cff13df45 100644 --- a/ui/components/compliance/index.ts +++ b/ui/components/compliance/index.ts @@ -12,7 +12,7 @@ export * from "./compliance-charts/top-failed-sections-card"; export * from "./compliance-custom-details/cis-details"; export * from "./compliance-custom-details/ens-details"; export * from "./compliance-custom-details/iso-details"; -export * from "./compliance-download-button"; +export * from "./compliance-download-container"; export * from "./compliance-header/compliance-header"; export * from "./compliance-header/compliance-scan-info"; export * from "./compliance-header/data-compliance"; diff --git a/ui/components/filters/custom-account-selection.tsx b/ui/components/filters/custom-account-selection.tsx deleted file mode 100644 index 04c95c03b7..0000000000 --- a/ui/components/filters/custom-account-selection.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; -import { Select, SelectItem } from "@heroui/select"; - -const accounts = [ - { key: "audit-test-1", label: "740350143844" }, - { key: "audit-test-2", label: "890837126756" }, - { key: "audit-test-3", label: "563829104923" }, - { key: "audit-test-4", label: "678943217543" }, - { key: "audit-test-5", label: "932187465320" }, - { key: "audit-test-6", label: "492837106587" }, - { key: "audit-test-7", label: "812736459201" }, - { key: "audit-test-8", label: "374829106524" }, - { key: "audit-test-9", label: "926481053298" }, - { key: "audit-test-10", label: "748192364579" }, - { key: "audit-test-11", label: "501374829106" }, -]; -export const CustomAccountSelection = () => { - return ( - - ); -}; diff --git a/ui/components/filters/custom-checkbox-muted-findings.tsx b/ui/components/filters/custom-checkbox-muted-findings.tsx index 9af7722957..546ebed5a8 100644 --- a/ui/components/filters/custom-checkbox-muted-findings.tsx +++ b/ui/components/filters/custom-checkbox-muted-findings.tsx @@ -1,8 +1,9 @@ "use client"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import { Checkbox } from "@/components/shadcn"; +import { useUrlFilters } from "@/hooks/use-url-filters"; // Constants for muted filter URL values const MUTED_FILTER_VALUES = { @@ -11,12 +12,10 @@ const MUTED_FILTER_VALUES = { } as const; export const CustomCheckboxMutedFindings = () => { - const router = useRouter(); - const pathname = usePathname(); const searchParams = useSearchParams(); + const { navigateWithParams } = useUrlFilters(); // Get the current muted filter value from URL - // Middleware ensures filter[muted] is always present when navigating to /findings const mutedFilterValue = searchParams.get("filter[muted]"); // URL states: @@ -26,22 +25,16 @@ export const CustomCheckboxMutedFindings = () => { const handleMutedChange = (checked: boolean | "indeterminate") => { const isChecked = checked === true; - const params = new URLSearchParams(searchParams.toString()); - if (isChecked) { - // Include muted: set special value (API will ignore invalid value and show all) - params.set("filter[muted]", MUTED_FILTER_VALUES.INCLUDE); - } else { - // Exclude muted: apply filter to show only non-muted - params.set("filter[muted]", MUTED_FILTER_VALUES.EXCLUDE); - } - - // Reset to page 1 when changing filter - if (params.has("page")) { - params.set("page", "1"); - } - - router.push(`${pathname}?${params.toString()}`, { scroll: false }); + navigateWithParams((params) => { + if (isChecked) { + // Include muted: set special value (API will ignore invalid value and show all) + params.set("filter[muted]", MUTED_FILTER_VALUES.INCLUDE); + } else { + // Exclude muted: apply filter to show only non-muted + params.set("filter[muted]", MUTED_FILTER_VALUES.EXCLUDE); + } + }); }; return ( diff --git a/ui/components/filters/custom-region-selection.tsx b/ui/components/filters/custom-region-selection.tsx deleted file mode 100644 index a242e8f664..0000000000 --- a/ui/components/filters/custom-region-selection.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; - -import { Select, SelectItem } from "@heroui/select"; -import { useRouter, useSearchParams } from "next/navigation"; -import React, { useCallback, useMemo } from "react"; - -export const CustomRegionSelection: React.FC = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - const region = "none"; - // Memoize selected keys based on the URL - const selectedKeys = useMemo(() => { - const params = searchParams.get("filter[regions]"); - return params ? params.split(",") : []; - }, [searchParams]); - - const applyRegionFilter = useCallback( - (values: string[]) => { - const params = new URLSearchParams(searchParams.toString()); - if (values.length > 0) { - params.set("filter[regions]", values.join(",")); - } else { - params.delete("filter[regions]"); - } - router.push(`?${params.toString()}`, { scroll: false }); - }, - [router, searchParams], - ); - - return ( - - ); -}; diff --git a/ui/components/filters/custom-select-provider.tsx b/ui/components/filters/custom-select-provider.tsx deleted file mode 100644 index bab26272cb..0000000000 --- a/ui/components/filters/custom-select-provider.tsx +++ /dev/null @@ -1,128 +0,0 @@ -"use client"; - -import { Select, SelectItem } from "@heroui/select"; -import { useRouter, useSearchParams } from "next/navigation"; -import { ReactElement } from "react"; - -import { PROVIDER_TYPES, ProviderType } from "@/types/providers"; - -import { - CustomProviderInputAlibabaCloud, - CustomProviderInputAWS, - CustomProviderInputAzure, - CustomProviderInputGCP, - CustomProviderInputGitHub, - CustomProviderInputIac, - CustomProviderInputKubernetes, - CustomProviderInputM365, - CustomProviderInputMongoDBAtlas, - CustomProviderInputOracleCloud, -} from "./custom-provider-inputs"; - -const providerDisplayData: Record< - ProviderType, - { label: string; component: ReactElement } -> = { - aws: { - label: "Amazon Web Services", - component: , - }, - azure: { - label: "Microsoft Azure", - component: , - }, - gcp: { - label: "Google Cloud Platform", - component: , - }, - github: { - label: "GitHub", - component: , - }, - iac: { - label: "Infrastructure as Code", - component: , - }, - kubernetes: { - label: "Kubernetes", - component: , - }, - m365: { - label: "Microsoft 365", - component: , - }, - mongodbatlas: { - label: "MongoDB Atlas", - component: , - }, - oraclecloud: { - label: "Oracle Cloud Infrastructure", - component: , - }, - alibabacloud: { - label: "Alibaba Cloud", - component: , - }, -}; - -const dataInputsProvider = PROVIDER_TYPES.map((providerType) => ({ - key: providerType, - label: providerDisplayData[providerType].label, - value: providerDisplayData[providerType].component, -})); - -export const CustomSelectProvider = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const applyProviderFilter = (value: string) => { - const params = new URLSearchParams(searchParams.toString()); - if (value) { - params.set("filter[provider_type]", value); - } else { - params.delete("filter[provider_type]"); - } - router.push(`?${params.toString()}`, { scroll: false }); - }; - - const currentProvider = searchParams.get("filter[provider_type]") || ""; - - const selectedKeys = dataInputsProvider.some( - (provider) => provider.key === currentProvider, - ) - ? [currentProvider] - : []; - - return ( - - ); -}; diff --git a/ui/components/filters/filter-controls.tsx b/ui/components/filters/filter-controls.tsx index e8df5c30be..2ee5d24bfa 100644 --- a/ui/components/filters/filter-controls.tsx +++ b/ui/components/filters/filter-controls.tsx @@ -3,30 +3,15 @@ import { FilterOption } from "@/types"; import { DataTableFilterCustom } from "../ui/table"; -import { CustomAccountSelection } from "./custom-account-selection"; -import { CustomCheckboxMutedFindings } from "./custom-checkbox-muted-findings"; -import { CustomDatePicker } from "./custom-date-picker"; -import { CustomRegionSelection } from "./custom-region-selection"; import { CustomSearchInput } from "./custom-search-input"; -import { CustomSelectProvider } from "./custom-select-provider"; export interface FilterControlsProps { search?: boolean; - providers?: boolean; - date?: boolean; - regions?: boolean; - accounts?: boolean; - mutedFindings?: boolean; customFilters?: FilterOption[]; } export const FilterControls = ({ search = false, - providers = false, - date = false, - regions = false, - accounts = false, - mutedFindings = false, customFilters, }: FilterControlsProps) => { return ( @@ -34,11 +19,6 @@ export const FilterControls = ({
{search && } - {providers && } - {date && } - {regions && } - {accounts && } - {mutedFindings && }
{customFilters && customFilters.length > 0 && ( diff --git a/ui/components/filters/index.ts b/ui/components/filters/index.ts index 5d9f577590..7e2de31cd7 100644 --- a/ui/components/filters/index.ts +++ b/ui/components/filters/index.ts @@ -1,9 +1,6 @@ export * from "./clear-filters-button"; -export * from "./custom-account-selection"; export * from "./custom-checkbox-muted-findings"; export * from "./custom-date-picker"; export * from "./custom-provider-inputs"; -export * from "./custom-region-selection"; -export * from "./custom-select-provider"; export * from "./data-filters"; export * from "./filter-controls"; diff --git a/ui/components/findings/send-to-jira-modal.tsx b/ui/components/findings/send-to-jira-modal.tsx index a692fe20b7..d5ee2d36fc 100644 --- a/ui/components/findings/send-to-jira-modal.tsx +++ b/ui/components/findings/send-to-jira-modal.tsx @@ -1,10 +1,7 @@ "use client"; -import { Input } from "@heroui/input"; -import { Select, SelectItem } from "@heroui/select"; import { zodResolver } from "@hookform/resolvers/zod"; -import type { Selection } from "@react-types/shared"; -import { Search, Send } from "lucide-react"; +import { Send } from "lucide-react"; import { type Dispatch, type SetStateAction, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -14,16 +11,11 @@ import { pollJiraDispatchTask, sendFindingToJira, } from "@/actions/integrations/jira-dispatch"; -import { JiraIcon } from "@/components/icons/services/IconServices"; import { Modal } from "@/components/shadcn/modal"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; import { CustomBanner } from "@/components/ui/custom/custom-banner"; -import { - Form, - FormControl, - FormField, - FormMessage, -} from "@/components/ui/form"; +import { Form, FormField, FormMessage } from "@/components/ui/form"; import { FormButtons } from "@/components/ui/form/form-buttons"; import { IntegrationProps } from "@/types/integrations"; @@ -42,15 +34,6 @@ const sendToJiraSchema = z.object({ type SendToJiraFormData = z.infer; -const selectorClassNames = { - trigger: "min-h-12", - popoverContent: "bg-bg-neutral-secondary", - listboxWrapper: "max-h-[300px] bg-bg-neutral-secondary", - listbox: "gap-0", - label: "tracking-tight font-light !text-text-neutral-secondary text-xs z-0!", - value: "text-text-neutral-secondary text-small", -}; - // The commented code is related to issue types, which are not required for the first implementation, but will be used in the future export const SendToJiraModal = ({ isOpen, @@ -61,8 +44,6 @@ export const SendToJiraModal = ({ const { toast } = useToast(); const [integrations, setIntegrations] = useState([]); const [isFetchingIntegrations, setIsFetchingIntegrations] = useState(false); - const [searchProjectValue, setSearchProjectValue] = useState(""); - // const [searchIssueTypeValue, setSearchIssueTypeValue] = useState(""); const form = useForm({ resolver: zodResolver(sendToJiraSchema), @@ -75,18 +56,11 @@ export const SendToJiraModal = ({ }); const selectedIntegration = form.watch("integration"); - // const selectedProject = form.watch("project"); const hasConnectedIntegration = integrations.some( (i) => i.attributes.connected === true, ); - const getSelectedValue = (keys: Selection): string => { - if (keys === "all") return ""; - const first = Array.from(keys)[0]; - return first !== null ? String(first) : ""; - }; - const setOpenForFormButtons: Dispatch> = (value) => { const next = typeof value === "function" ? value(isOpen) : value; onOpenChange(next); @@ -129,8 +103,6 @@ export const SendToJiraModal = ({ } else { // Reset form when modal closes form.reset(); - setSearchProjectValue(""); - // setSearchIssueTypeValue(""); } }, [isOpen, form, toast]); @@ -187,32 +159,16 @@ export const SendToJiraModal = ({ ({} as Record); const projectEntries = Object.entries(projects); - const shouldShowProjectSearch = projectEntries.length > 5; - // const issueTypes: string[] = - // selectedIntegrationData?.attributes.configuration.issue_types || - // ([] as string[]); - // Filter projects based on search - const filteredProjects = (() => { - if (!searchProjectValue) return projectEntries; + const integrationOptions = integrations.map((integration) => ({ + value: integration.id, + label: integration.attributes.configuration.domain || integration.id, + })); - const lowerSearch = searchProjectValue.toLowerCase(); - return projectEntries.filter( - ([key, name]) => - key.toLowerCase().includes(lowerSearch) || - name.toLowerCase().includes(lowerSearch), - ); - })(); - - // Filter issue types based on search - // const filteredIssueTypes = useMemo(() => { - // if (!searchIssueTypeValue) return issueTypes; - - // const lowerSearch = searchIssueTypeValue.toLowerCase(); - // return issueTypes.filter((type) => - // type.toLowerCase().includes(lowerSearch), - // ); - // }, [issueTypes, searchIssueTypeValue]); + const projectOptions = projectEntries.map(([key, name]) => ({ + value: key, + label: `${key} - ${name}`, + })); return ( ( - <> - - - +
+ + { + const selectedValue = values.at(-1) ?? ""; + field.onChange(selectedValue); + // Reset dependent fields + form.setValue("project", ""); + form.setValue("issueType", "Task"); + }} + defaultValue={field.value ? [field.value] : []} + placeholder="Select a Jira integration" + searchable={true} + emptyIndicator="No integrations found." + disabled={isFetchingIntegrations} + hideSelectAll={true} + maxCount={1} + closeOnSelect={true} + resetOnDefaultValueChange={true} + /> - +
)} /> )} - {/* Project Selection - Enhanced Style */} - {selectedIntegration && Object.keys(projects).length > 0 && ( + {/* Project Selection */} + {selectedIntegration && projectEntries.length > 0 && ( ( - <> - - } - value={searchProjectValue} - onValueChange={setSearchProjectValue} - onClear={() => setSearchProjectValue("")} - classNames={{ - inputWrapper: - "border-border-input-primary bg-bg-input-primary hover:bg-bg-neutral-secondary", - input: "text-small", - clearButton: "text-default-400", - }} - /> -
- ) : null, - }} - > - {filteredProjects.map(([key, name]) => ( - -
-
-
-
- - {key} - - - - - - - {name} - -
-
-
-
-
- ))} - - +
+ + { + const selectedValue = values.at(-1) ?? ""; + field.onChange(selectedValue); + // Keep issue type defaulting to Task when project changes + form.setValue("issueType", "Task"); + }} + defaultValue={field.value ? [field.value] : []} + placeholder="Select a Jira project" + searchable={true} + emptyIndicator="No projects found." + hideSelectAll={true} + maxCount={1} + closeOnSelect={true} + resetOnDefaultValueChange={true} + /> - +
)} /> )} diff --git a/ui/components/findings/table/data-table-row-actions.tsx b/ui/components/findings/table/data-table-row-actions.tsx index 9055d7eb9a..752f9f5144 100644 --- a/ui/components/findings/table/data-table-row-actions.tsx +++ b/ui/components/findings/table/data-table-row-actions.tsx @@ -66,30 +66,13 @@ export function DataTableRowActions({ return [finding.id]; }; - const getMuteDescription = (): string => { - if (isMuted) { - return "This finding is already muted"; - } - const ids = getMuteIds(); - if (ids.length > 1) { - return `Mute ${ids.length} selected findings`; - } - return "Mute this finding"; - }; - const getMuteLabel = () => { if (isMuted) return "Muted"; - if (!isMuted && isCurrentSelected && hasMultipleSelected) { - return ( - <> - Mute - - ({selectedFindingIds.length}) - - - ); + const ids = getMuteIds(); + if (ids.length > 1) { + return `Mute ${ids.length} Findings`; } - return "Mute"; + return "Mute Finding"; }; const handleMuteComplete = () => { @@ -146,7 +129,6 @@ export function DataTableRowActions({ ) } label={getMuteLabel()} - description={getMuteDescription()} disabled={isMuted} onSelect={() => { setIsMuteModalOpen(true); @@ -155,7 +137,6 @@ export function DataTableRowActions({ } label="Send to Jira" - description="Create a Jira issue for this finding" onSelect={() => setIsJiraModalOpen(true)} /> diff --git a/ui/components/findings/table/provider-icon-cell.tsx b/ui/components/findings/table/provider-icon-cell.tsx index c99d833cbc..a04341b4d2 100644 --- a/ui/components/findings/table/provider-icon-cell.tsx +++ b/ui/components/findings/table/provider-icon-cell.tsx @@ -12,7 +12,7 @@ import { } from "@/components/icons/providers-badge"; import { ProviderType } from "@/types"; -const PROVIDER_ICONS = { +export const PROVIDER_ICONS = { aws: AWSProviderBadge, azure: AzureProviderBadge, gcp: GCPProviderBadge, diff --git a/ui/components/icons/compliance/IconCompliance.tsx b/ui/components/icons/compliance/IconCompliance.tsx index aac9801eb3..032e0bdecc 100644 --- a/ui/components/icons/compliance/IconCompliance.tsx +++ b/ui/components/icons/compliance/IconCompliance.tsx @@ -3,6 +3,7 @@ import C5Logo from "./c5.svg"; import CCCLogo from "./ccc.svg"; import CISLogo from "./cis.svg"; import CISALogo from "./cisa.svg"; +import CSALogo from "./csa.svg"; import ENSLogo from "./ens.png"; import FedRAMPLogo from "./fedramp.svg"; import FFIECLogo from "./ffiec.svg"; @@ -40,6 +41,7 @@ const COMPLIANCE_LOGOS = { nis2: NIS2Logo, c5: C5Logo, ccc: CCCLogo, + csa: CSALogo, } as const; export const getComplianceIcon = (complianceTitle: string) => { diff --git a/ui/components/icons/compliance/csa.svg b/ui/components/icons/compliance/csa.svg new file mode 100644 index 0000000000..99204d146e --- /dev/null +++ b/ui/components/icons/compliance/csa.svg @@ -0,0 +1 @@ + diff --git a/ui/components/integrations/index.ts b/ui/components/integrations/index.ts index d46942d57c..223887ad53 100644 --- a/ui/components/integrations/index.ts +++ b/ui/components/integrations/index.ts @@ -1,4 +1,3 @@ -export * from "../providers/enhanced-provider-selector"; export * from "./api-key/api-key-link-card"; export * from "./jira/jira-integration-card"; export * from "./jira/jira-integration-form"; diff --git a/ui/components/integrations/s3/s3-integration-form.tsx b/ui/components/integrations/s3/s3-integration-form.tsx index 2570441b29..7b3af4b018 100644 --- a/ui/components/integrations/s3/s3-integration-form.tsx +++ b/ui/components/integrations/s3/s3-integration-form.tsx @@ -8,12 +8,18 @@ import { useState } from "react"; import { Control, useForm } from "react-hook-form"; import { createIntegration, updateIntegration } from "@/actions/integrations"; -import { EnhancedProviderSelector } from "@/components/providers/enhanced-provider-selector"; +import { PROVIDER_ICONS } from "@/components/findings/table/provider-icon-cell"; import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; import { CustomInput } from "@/components/ui/custom"; import { CustomLink } from "@/components/ui/custom/custom-link"; -import { Form } from "@/components/ui/form"; +import { + Form, + FormControl, + FormField, + FormMessage, +} from "@/components/ui/form"; import { FormButtons } from "@/components/ui/form/form-buttons"; import { getAWSCredentialsTemplateLinks } from "@/lib"; import { AWSCredentialsRole } from "@/types"; @@ -272,18 +278,40 @@ export const S3IntegrationForm = ({ // Show configuration step (step 0 or editing configuration) if (isEditingConfig || currentStep === 0) { + const providerOptions = providers.map((provider) => { + const Icon = PROVIDER_ICONS[provider.attributes.provider]; + return { + value: provider.id, + label: provider.attributes.alias || provider.attributes.uid, + icon: Icon ? : undefined, + description: provider.attributes.connection.connected + ? "Connected" + : "Disconnected", + }; + }); + return ( <> {/* Provider Selection */}
- ( + <> + + + + + + )} />
diff --git a/ui/components/integrations/security-hub/security-hub-integration-form.tsx b/ui/components/integrations/security-hub/security-hub-integration-form.tsx index 1acfa31809..8a62f62457 100644 --- a/ui/components/integrations/security-hub/security-hub-integration-form.tsx +++ b/ui/components/integrations/security-hub/security-hub-integration-form.tsx @@ -6,15 +6,21 @@ import { Radio, RadioGroup } from "@heroui/radio"; import { zodResolver } from "@hookform/resolvers/zod"; import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; import { useSession } from "next-auth/react"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { Control, useForm } from "react-hook-form"; import { createIntegration, updateIntegration } from "@/actions/integrations"; -import { EnhancedProviderSelector } from "@/components/providers/enhanced-provider-selector"; +import { PROVIDER_ICONS } from "@/components/findings/table/provider-icon-cell"; import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; import { CustomLink } from "@/components/ui/custom/custom-link"; -import { Form, FormControl, FormField } from "@/components/ui/form"; +import { + Form, + FormControl, + FormField, + FormMessage, +} from "@/components/ui/form"; import { FormButtons } from "@/components/ui/form/form-buttons"; import { getAWSCredentialsTemplateLinks } from "@/lib"; import { AWSCredentialsRole } from "@/types"; @@ -52,7 +58,7 @@ export const SecurityHubIntegrationForm = ({ const isEditingConfig = editMode === "configuration"; const isEditingCredentials = editMode === "credentials"; - const disabledProviderIds = useMemo(() => { + const disabledProviderIds = (() => { // When editing, no providers should be disabled since we're not changing it if (isEditing) { return []; @@ -69,7 +75,7 @@ export const SecurityHubIntegrationForm = ({ }); return usedProviderIds; - }, [isEditing, existingIntegrations]); + })(); const form = useForm({ resolver: zodResolver( @@ -107,6 +113,26 @@ export const SecurityHubIntegrationForm = ({ const providerIdValue = form.watch("provider_id"); const hasErrors = !!form.formState.errors.provider_id || !providerIdValue; + const providerOptions = providers + .filter((provider) => provider.attributes.provider === "aws") + .map((provider) => { + const isDisabled = disabledProviderIds.includes(provider.id); + const connectionLabel = provider.attributes.connection.connected + ? "Connected" + : "Disconnected"; + + const Icon = PROVIDER_ICONS[provider.attributes.provider]; + return { + value: provider.id, + label: provider.attributes.alias || provider.attributes.uid, + icon: Icon ? : undefined, + description: isDisabled + ? `${connectionLabel} (Already in use)` + : connectionLabel, + disabled: isDisabled, + }; + }); + useEffect(() => { if (!useCustomCredentials && isCreating) { setCurrentStep(0); @@ -325,17 +351,29 @@ export const SecurityHubIntegrationForm = ({ {!isEditingConfig && ( <>
- ( + <> + + { + field.onChange(values.at(-1) ?? ""); + }} + defaultValue={field.value ? [field.value] : []} + placeholder="Search and select an AWS provider" + searchable={true} + hideSelectAll={true} + maxCount={1} + closeOnSelect={true} + resetOnDefaultValueChange={true} + /> + + + + )} />
diff --git a/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx b/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx index b3b4d965ad..a1bd977b1b 100644 --- a/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx +++ b/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx @@ -1,6 +1,5 @@ "use client"; -import { Chip } from "@heroui/chip"; import { format } from "date-fns"; import { PlusIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; @@ -16,7 +15,7 @@ import { IntegrationCardHeader, IntegrationSkeleton, } from "@/components/integrations/shared"; -import { Button } from "@/components/shadcn"; +import { Badge, Button } from "@/components/shadcn"; import { Modal } from "@/components/shadcn/modal"; import { useToast } from "@/components/ui"; import { DataTablePagination } from "@/components/ui/table/data-table-pagination"; @@ -385,14 +384,13 @@ export const SecurityHubIntegrationsManager = ({ {enabledRegions.length > 0 && (
{enabledRegions.map((region) => ( - {region} - + ))}
)} diff --git a/ui/components/integrations/shared/integration-card-header.tsx b/ui/components/integrations/shared/integration-card-header.tsx index 73f9173e61..ab0a252585 100644 --- a/ui/components/integrations/shared/integration-card-header.tsx +++ b/ui/components/integrations/shared/integration-card-header.tsx @@ -1,23 +1,18 @@ "use client"; -import { Chip } from "@heroui/chip"; import { ExternalLink } from "lucide-react"; import { ReactNode } from "react"; +import { Badge } from "@/components/shadcn"; +import { cn } from "@/lib/utils"; + interface IntegrationCardHeaderProps { icon: ReactNode; title: string; subtitle?: string; chips?: Array<{ label: string; - color?: - | "default" - | "primary" - | "secondary" - | "success" - | "warning" - | "danger"; - variant?: "solid" | "bordered" | "light" | "flat" | "faded" | "shadow"; + className?: string; }>; connectionStatus?: { connected: boolean; @@ -63,25 +58,30 @@ export const IntegrationCardHeader = ({ {(chips.length > 0 || connectionStatus) && (
{chips.map((chip, index) => ( - {chip.label} - + ))} {connectionStatus && ( - {connectionStatus.label || (connectionStatus.connected ? "Connected" : "Disconnected")} - + )}
)} diff --git a/ui/components/invitations/forms/edit-form.tsx b/ui/components/invitations/forms/edit-form.tsx index d087f34729..57f0717313 100644 --- a/ui/components/invitations/forms/edit-form.tsx +++ b/ui/components/invitations/forms/edit-form.tsx @@ -1,4 +1,3 @@ -import { Select, SelectItem } from "@heroui/select"; import { zodResolver } from "@hookform/resolvers/zod"; import { MailIcon, ShieldIcon } from "lucide-react"; import { Dispatch, SetStateAction } from "react"; @@ -6,6 +5,13 @@ import { Controller, useForm } from "react-hook-form"; import * as z from "zod"; import { updateInvite } from "@/actions/invitations/invitation"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/shadcn/select/select"; import { useToast } from "@/components/ui"; import { CustomInput } from "@/components/ui/custom"; import { Form, FormButtons } from "@/components/ui/form"; @@ -126,27 +132,25 @@ export const EditForm = ({ isRequired={false} />
-
+
+ ( - + + + + + {roles.map((role) => ( + + {role.name} + + ))} + )} /> diff --git a/ui/components/invitations/table/data-table-row-actions.tsx b/ui/components/invitations/table/data-table-row-actions.tsx index 4ec5f50e79..b643202f80 100644 --- a/ui/components/invitations/table/data-table-row-actions.tsx +++ b/ui/components/invitations/table/data-table-row-actions.tsx @@ -1,24 +1,17 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - AddNoteBulkIcon, - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; +import { Eye, Pencil, Trash2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; import { DeleteForm, EditForm } from "../forms"; @@ -27,7 +20,6 @@ interface DataTableRowActionsProps { row: Row; roles?: { id: string; name: string }[]; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; export function DataTableRowActions({ row, @@ -67,65 +59,36 @@ export function DataTableRowActions({
- - + - - - - } - onPress={() => - router.push(`/invitations/check-details?id=${invitationId}`) - } - > - Check Details - - - } - onPress={() => setIsEditOpen(true)} - isDisabled={invitationAccepted === "accepted"} - > - Edit Invitation - - - - - } - onPress={() => setIsDeleteOpen(true)} - isDisabled={invitationAccepted === "accepted"} - > - Revoke Invitation - - - - + } + > + } + label="Check Details" + onSelect={() => + router.push(`/invitations/check-details?id=${invitationId}`) + } + /> + } + label="Edit Invitation" + onSelect={() => setIsEditOpen(true)} + disabled={invitationAccepted === "accepted"} + /> + + } + label="Revoke Invitation" + destructive + onSelect={() => setIsDeleteOpen(true)} + disabled={invitationAccepted === "accepted"} + /> + +
); diff --git a/ui/components/invitations/workflow/forms/send-invitation-form.tsx b/ui/components/invitations/workflow/forms/send-invitation-form.tsx index ea1cb19935..843885d772 100644 --- a/ui/components/invitations/workflow/forms/send-invitation-form.tsx +++ b/ui/components/invitations/workflow/forms/send-invitation-form.tsx @@ -1,6 +1,5 @@ "use client"; -import { Select, SelectItem } from "@heroui/select"; import { zodResolver } from "@hookform/resolvers/zod"; import { SaveIcon } from "lucide-react"; import { useRouter } from "next/navigation"; @@ -9,6 +8,13 @@ import * as z from "zod"; import { sendInvite } from "@/actions/invitations/invitation"; import { Button } from "@/components/shadcn"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/shadcn/select/select"; import { useToast } from "@/components/ui"; import { CustomInput } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; @@ -111,35 +117,33 @@ export const SendInvitationForm = ({ name="roleId" control={form.control} render={({ field }) => ( - <> +
{form.formState.errors.roleId && (

{form.formState.errors.roleId.message}

)} - +
)} /> diff --git a/ui/components/manage-groups/forms/add-group-form.tsx b/ui/components/manage-groups/forms/add-group-form.tsx index fa03f1f4eb..394d3ae55d 100644 --- a/ui/components/manage-groups/forms/add-group-form.tsx +++ b/ui/components/manage-groups/forms/add-group-form.tsx @@ -7,8 +7,9 @@ import * as z from "zod"; import { createProviderGroup } from "@/actions/manage-groups"; import { Button } from "@/components/shadcn"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; -import { CustomDropdownSelection, CustomInput } from "@/components/ui/custom"; +import { CustomInput } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; import { ApiError } from "@/types"; @@ -39,6 +40,14 @@ export const AddGroupForm = ({ }); const isLoading = form.formState.isSubmitting; + const providerOptions = providers.map((provider) => ({ + label: provider.name, + value: provider.id, + })); + const roleOptions = roles.map((role) => ({ + label: role.name, + value: role.id, + })); const onSubmitClient = async (values: FormValues) => { try { @@ -128,15 +137,19 @@ export const AddGroupForm = ({ name="providers" control={form.control} render={({ field }) => ( - - field.onChange(selectedValues) - } - /> +
+ +
)} /> {form.formState.errors.providers && ( @@ -155,15 +168,19 @@ export const AddGroupForm = ({ name="roles" control={form.control} render={({ field }) => ( - - field.onChange(selectedValues) - } - /> +
+ +
)} /> {form.formState.errors.roles && ( diff --git a/ui/components/manage-groups/forms/edit-group-form.tsx b/ui/components/manage-groups/forms/edit-group-form.tsx index 781a502e5b..a29a054e8b 100644 --- a/ui/components/manage-groups/forms/edit-group-form.tsx +++ b/ui/components/manage-groups/forms/edit-group-form.tsx @@ -9,8 +9,9 @@ import * as z from "zod"; import { updateProviderGroup } from "@/actions/manage-groups/manage-groups"; import { Button } from "@/components/shadcn"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; -import { CustomDropdownSelection, CustomInput } from "@/components/ui/custom"; +import { CustomInput } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; import { ApiError } from "@/types"; @@ -176,18 +177,29 @@ export const EditGroupForm = ({ ]; return ( - p.id) || []} - onChange={(name, selectedValues) => { - const selectedProviders = combinedProviders.filter( - (provider) => selectedValues.includes(provider.id), - ); - field.onChange(selectedProviders); - }} - /> +
+ ({ + label: provider.name, + value: provider.id, + }))} + onValueChange={(selectedValues) => { + const selectedProviders = combinedProviders.filter( + (provider) => selectedValues.includes(provider.id), + ); + field.onChange(selectedProviders); + }} + defaultValue={ + field.value?.map((provider) => provider.id) || [] + } + placeholder="Select providers" + aria-label="Select providers" + searchable={true} + hideSelectAll={true} + emptyIndicator="No results found" + resetOnDefaultValueChange={true} + /> +
); }} /> @@ -216,18 +228,27 @@ export const EditGroupForm = ({ ]; return ( - r.id) || []} - onChange={(name, selectedValues) => { - const selectedRoles = combinedRoles.filter((role) => - selectedValues.includes(role.id), - ); - field.onChange(selectedRoles); - }} - /> +
+ ({ + label: role.name, + value: role.id, + }))} + onValueChange={(selectedValues) => { + const selectedRoles = combinedRoles.filter((role) => + selectedValues.includes(role.id), + ); + field.onChange(selectedRoles); + }} + defaultValue={field.value?.map((role) => role.id) || []} + placeholder="Select roles" + aria-label="Select roles" + searchable={true} + hideSelectAll={true} + emptyIndicator="No results found" + resetOnDefaultValueChange={true} + /> +
); }} /> diff --git a/ui/components/manage-groups/table/data-table-row-actions.tsx b/ui/components/manage-groups/table/data-table-row-actions.tsx index 3ea2f9c9a1..519b0b05e2 100644 --- a/ui/components/manage-groups/table/data-table-row-actions.tsx +++ b/ui/components/manage-groups/table/data-table-row-actions.tsx @@ -1,23 +1,17 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; +import { Pencil, Trash2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; import { DeleteGroupForm } from "../forms"; @@ -25,7 +19,6 @@ import { DeleteGroupForm } from "../forms"; interface DataTableRowActionsProps { row: Row; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; export function DataTableRowActions({ row, @@ -47,51 +40,27 @@ export function DataTableRowActions({
- - + - - - - } - onPress={() => router.push(`/manage-groups?groupId=${groupId}`)} - > - Edit Provider Group - - - - - } - onPress={() => setIsDeleteOpen(true)} - > - Delete Provider Group - - - - + } + > + } + label="Edit Provider Group" + onSelect={() => router.push(`/manage-groups?groupId=${groupId}`)} + /> + + } + label="Delete Provider Group" + destructive + onSelect={() => setIsDeleteOpen(true)} + /> + +
); diff --git a/ui/components/providers/credentials-update-info.tsx b/ui/components/providers/credentials-update-info.tsx index 3fb13f97a7..bd99f594d9 100644 --- a/ui/components/providers/credentials-update-info.tsx +++ b/ui/components/providers/credentials-update-info.tsx @@ -1,5 +1,6 @@ "use client"; +import { SelectViaAlibabaCloud } from "@/components/providers/workflow/forms/select-credentials-type/alibabacloud"; import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws"; import { SelectViaGCP } from "@/components/providers/workflow/forms/select-credentials-type/gcp"; import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github"; @@ -28,6 +29,9 @@ export const CredentialsUpdateInfo = ({ if (providerType === "m365") { return ; } + if (providerType === "alibabacloud") { + return ; + } return null; }; diff --git a/ui/components/providers/enhanced-provider-selector.tsx b/ui/components/providers/enhanced-provider-selector.tsx deleted file mode 100644 index 94f543b2d0..0000000000 --- a/ui/components/providers/enhanced-provider-selector.tsx +++ /dev/null @@ -1,307 +0,0 @@ -"use client"; - -import { Input } from "@heroui/input"; -import { Select, SelectItem } from "@heroui/select"; -import { SharedSelection } from "@heroui/system"; -import { CheckSquare, Search, Square } from "lucide-react"; -import { useState } from "react"; -import { Control, FieldValues, Path } from "react-hook-form"; - -import { Button } from "@/components/shadcn"; -import { FormControl, FormField, FormMessage } from "@/components/ui/form"; -import { ProviderProps, ProviderType } from "@/types/providers"; - -const providerTypeLabels: Record = { - aws: "Amazon Web Services", - gcp: "Google Cloud Platform", - azure: "Microsoft Azure", - m365: "Microsoft 365", - kubernetes: "Kubernetes", - github: "GitHub", - iac: "Infrastructure as Code", - oraclecloud: "Oracle Cloud Infrastructure", - mongodbatlas: "MongoDB Atlas", - alibabacloud: "Alibaba Cloud", -}; - -interface EnhancedProviderSelectorProps { - control: Control; - name: Path; - providers: ProviderProps[]; - label?: string; - placeholder?: string; - isInvalid?: boolean; - showFormMessage?: boolean; - selectionMode?: "single" | "multiple"; - providerType?: ProviderType; - enableSearch?: boolean; - disabledProviderIds?: string[]; -} - -export const EnhancedProviderSelector = ({ - control, - name, - providers, - label = "Provider", - placeholder = "Select provider", - isInvalid = false, - showFormMessage = true, - selectionMode = "single", - providerType, - enableSearch = false, - disabledProviderIds = [], -}: EnhancedProviderSelectorProps) => { - const [searchValue, setSearchValue] = useState(""); - - const filteredProviders = (() => { - let filtered = providers; - - // Filter by provider type if specified - if (providerType) { - filtered = filtered.filter((p) => p.attributes.provider === providerType); - } - - // Filter by search value - if (searchValue && enableSearch) { - const lowerSearch = searchValue.toLowerCase(); - filtered = filtered.filter((p) => { - const displayName = p.attributes.alias || p.attributes.uid; - const typeLabel = providerTypeLabels[p.attributes.provider]; - return ( - displayName.toLowerCase().includes(lowerSearch) || - typeLabel.toLowerCase().includes(lowerSearch) - ); - }); - } - - // Sort providers - return filtered.sort((a, b) => { - const typeComparison = a.attributes.provider.localeCompare( - b.attributes.provider, - ); - if (typeComparison !== 0) return typeComparison; - - const nameA = a.attributes.alias || a.attributes.uid; - const nameB = b.attributes.alias || b.attributes.uid; - return nameA.localeCompare(nameB); - }); - })(); - - return ( - { - const isMultiple = selectionMode === "multiple"; - const selectedIds: string[] = isMultiple - ? (value as string[] | undefined) || [] - : value - ? [value as string] - : []; - const allProviderIds = filteredProviders - .filter((p) => !disabledProviderIds.includes(p.id)) - .map((p) => p.id); - const isAllSelected = - isMultiple && - allProviderIds.length > 0 && - allProviderIds.every((id) => selectedIds.includes(id)); - - const handleSelectAll = () => { - if (isAllSelected) { - onChange([]); - } else { - onChange(allProviderIds); - } - }; - - const handleSelectionChange = (keys: SharedSelection) => { - if (keys === "all") { - onChange(allProviderIds); - return; - } - if (isMultiple) { - const selectedArray = Array.from(keys).map(String); - onChange(selectedArray); - } else { - const selectedValue = Array.from(keys)[0]; - onChange(selectedValue ? String(selectedValue) : ""); - } - }; - - return ( - <> - -
- {isMultiple && filteredProviders.length > 1 && ( -
- - {label} - - -
- )} - } - value={searchValue} - onValueChange={setSearchValue} - onClear={() => setSearchValue("")} - classNames={{ - inputWrapper: - "border-border-input-primary bg-bg-input-primary hover:bg-bg-neutral-secondary", - input: "text-small", - clearButton: "text-default-400", - }} - /> -
- ) : null, - }} - > - {filteredProviders.map((provider) => { - const providerType = provider.attributes.provider; - const displayName = - provider.attributes.alias || provider.attributes.uid; - const typeLabel = providerTypeLabels[providerType]; - const isDisabled = disabledProviderIds.includes( - provider.id, - ); - - return ( - -
-
-
-
- {displayName} -
-
- {typeLabel} - {isDisabled && ( - - (Already used) - - )} -
-
-
-
-
-
-
- - ); - })} - -
-
- {showFormMessage && ( - - )} - - ); - }} - /> - ); -}; diff --git a/ui/components/providers/table/data-table-row-actions.tsx b/ui/components/providers/table/data-table-row-actions.tsx index 1866abe8ab..023f563006 100644 --- a/ui/components/providers/table/data-table-row-actions.tsx +++ b/ui/components/providers/table/data-table-row-actions.tsx @@ -1,25 +1,18 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - AddNoteBulkIcon, - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; +import { Pencil, PlugZap, Trash2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { checkConnectionProvider } from "@/actions/providers/providers"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; import { EditForm } from "../forms"; @@ -28,7 +21,6 @@ import { DeleteForm } from "../forms/delete-form"; interface DataTableRowActionsProps { row: Row; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; export function DataTableRowActions({ row, @@ -53,12 +45,6 @@ export function DataTableRowActions({ const hasSecret = (row.original as any).relationships?.secret?.data; - // Calculate disabled keys based on conditions - const disabledKeys = []; - if (!hasSecret || loading) { - disabledKeys.push("new"); - } - return ( <> ({
- - + - - - - } - onPress={() => - router.push( - `/providers/${hasSecret ? "update" : "add"}-credentials?type=${providerType}&id=${providerId}${providerSecretId ? `&secretId=${providerSecretId}` : ""}`, - ) - } - closeOnSelect={true} - > - {hasSecret ? "Update Credentials" : "Add Credentials"} - - } - onPress={handleTestConnection} - closeOnSelect={false} - > - {loading ? "Testing..." : "Test Connection"} - - } - onPress={() => setIsEditOpen(true)} - closeOnSelect={true} - > - Edit Provider Alias - - - - - } - onPress={() => setIsDeleteOpen(true)} - closeOnSelect={true} - > - Delete Provider - - - - + } + > + } + label={hasSecret ? "Update Credentials" : "Add Credentials"} + onSelect={() => + router.push( + `/providers/${hasSecret ? "update" : "add"}-credentials?type=${providerType}&id=${providerId}${providerSecretId ? `&secretId=${providerSecretId}` : ""}`, + ) + } + /> + } + label={loading ? "Testing..." : "Test Connection"} + description={ + hasSecret && !loading + ? "Check the provider connection" + : loading + ? "Checking provider connection" + : "Add credentials to test the connection" + } + onSelect={(e) => { + e.preventDefault(); + handleTestConnection(); + }} + disabled={!hasSecret || loading} + /> + } + label="Edit Provider Alias" + onSelect={() => setIsEditOpen(true)} + /> + + } + label="Delete Provider" + destructive + onSelect={() => setIsDeleteOpen(true)} + /> + +
); diff --git a/ui/components/providers/workflow/forms/connect-account-form.tsx b/ui/components/providers/workflow/forms/connect-account-form.tsx index 90e3cba642..b6b50ead52 100644 --- a/ui/components/providers/workflow/forms/connect-account-form.tsx +++ b/ui/components/providers/workflow/forms/connect-account-form.tsx @@ -49,8 +49,8 @@ const getProviderFieldDetails = (providerType?: ProviderType) => { }; case "github": return { - label: "Username", - placeholder: "e.g. your-github-username", + label: "Username/Organization", + placeholder: "e.g. username or organization-name", }; case "iac": return { diff --git a/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx index ad38a761c0..1347b1d47d 100644 --- a/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx @@ -1,11 +1,17 @@ import { Chip } from "@heroui/chip"; import { Divider } from "@heroui/divider"; -import { Select, SelectItem } from "@heroui/select"; import { Switch } from "@heroui/switch"; import { useEffect, useState } from "react"; import { Control, UseFormSetValue, useWatch } from "react-hook-form"; import { CredentialsRoleHelper } from "@/components/providers/workflow"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/shadcn/select/select"; import { CustomInput } from "@/components/ui/custom"; import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; import { AWSCredentialsRole } from "@/types"; @@ -77,47 +83,47 @@ export const AWSRoleCredentialsForm = ({ Specify which AWS credentials to use - { + setValue( + ProviderCredentialFields.CREDENTIALS_TYPE, + value as "aws-sdk-default" | "access-secret-key", + ); + }} > -
- - {isCloudEnv - ? "Prowler Cloud will assume your IAM role" - : "AWS SDK Default"} - - {isCloudEnv && ( - - Recommended - - )} -
- - -
- Access & Secret Key -
-
- + + + + + +
+ + {isCloudEnv + ? "Prowler Cloud will assume your IAM role" + : "AWS SDK Default"} + + {isCloudEnv && ( + + Recommended + + )} +
+
+ +
+ Access & Secret Key +
+
+
+ +
{credentialsType === "access-secret-key" && ( <> diff --git a/ui/components/resources/table/column-resources.tsx b/ui/components/resources/table/column-resources.tsx index e94021d2d0..cdbc000ff3 100644 --- a/ui/components/resources/table/column-resources.tsx +++ b/ui/components/resources/table/column-resources.tsx @@ -83,7 +83,6 @@ const FailedFindingsBadge = ({ count }: { count: number }) => { // Row actions dropdown const ResourceRowActions = ({ row }: { row: { original: ResourceProps } }) => { const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const resourceName = row.original.attributes?.name || "Resource"; return ( <> @@ -102,8 +101,7 @@ const ResourceRowActions = ({ row }: { row: { original: ResourceProps } }) => { > } - label="View details" - description={`View details for ${resourceName}`} + label="View Details" onSelect={() => setIsDrawerOpen(true)} /> diff --git a/ui/components/roles/table/data-table-row-actions.tsx b/ui/components/roles/table/data-table-row-actions.tsx index 0eb7864a51..70895817e3 100644 --- a/ui/components/roles/table/data-table-row-actions.tsx +++ b/ui/components/roles/table/data-table-row-actions.tsx @@ -1,30 +1,23 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; +import { Pencil, Trash2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; import { DeleteRoleForm } from "../workflow/forms"; interface DataTableRowActionsProps { row: Row; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; export function DataTableRowActions({ row, @@ -43,51 +36,27 @@ export function DataTableRowActions({
- - + - - - - } - onPress={() => router.push(`/roles/edit?roleId=${roleId}`)} - > - Edit Role - - - - - } - onPress={() => setIsDeleteOpen(true)} - > - Delete Role - - - - + } + > + } + label="Edit Role" + onSelect={() => router.push(`/roles/edit?roleId=${roleId}`)} + /> + + } + label="Delete Role" + destructive + onSelect={() => setIsDeleteOpen(true)} + /> + +
); diff --git a/ui/components/roles/workflow/forms/add-role-form.tsx b/ui/components/roles/workflow/forms/add-role-form.tsx index 56c61308fd..9ff4772540 100644 --- a/ui/components/roles/workflow/forms/add-role-form.tsx +++ b/ui/components/roles/workflow/forms/add-role-form.tsx @@ -12,8 +12,9 @@ import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { addRole } from "@/actions/roles/roles"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; -import { CustomDropdownSelection, CustomInput } from "@/components/ui/custom"; +import { CustomInput } from "@/components/ui/custom"; import { Form, FormButtons } from "@/components/ui/form"; import { getErrorMessage, permissionFormFields } from "@/lib"; import { addRoleFormSchema, ApiError } from "@/types"; @@ -232,15 +233,21 @@ export const AddRoleForm = ({ name="groups" control={form.control} render={({ field }) => ( - - field.onChange(selectedValues) - } - /> +
+ ({ + label: group.name, + value: group.id, + }))} + onValueChange={field.onChange} + defaultValue={field.value || []} + placeholder="Select groups" + searchable={true} + hideSelectAll={true} + emptyIndicator="No results found" + resetOnDefaultValueChange={true} + /> +
)} /> {form.formState.errors.groups && ( diff --git a/ui/components/roles/workflow/forms/edit-role-form.tsx b/ui/components/roles/workflow/forms/edit-role-form.tsx index ab8fc5b567..76afe290a0 100644 --- a/ui/components/roles/workflow/forms/edit-role-form.tsx +++ b/ui/components/roles/workflow/forms/edit-role-form.tsx @@ -12,8 +12,9 @@ import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { updateRole } from "@/actions/roles/roles"; +import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select"; import { useToast } from "@/components/ui"; -import { CustomDropdownSelection, CustomInput } from "@/components/ui/custom"; +import { CustomInput } from "@/components/ui/custom"; import { Form, FormButtons } from "@/components/ui/form"; import { getErrorMessage, permissionFormFields } from "@/lib"; import { ApiError, editRoleFormSchema } from "@/types"; @@ -250,15 +251,21 @@ export const EditRoleForm = ({ name="groups" control={form.control} render={({ field }) => ( - { - field.onChange(selectedValues); - }} - /> +
+ ({ + label: group.name, + value: group.id, + }))} + onValueChange={field.onChange} + defaultValue={field.value || []} + placeholder="Select groups" + searchable={true} + hideSelectAll={true} + emptyIndicator="No results found" + resetOnDefaultValueChange={true} + /> +
)} /> diff --git a/ui/components/scans/table/scans/data-table-row-actions.tsx b/ui/components/scans/table/scans/data-table-row-actions.tsx index b9d16db9ab..e185d0d43d 100644 --- a/ui/components/scans/table/scans/data-table-row-actions.tsx +++ b/ui/components/scans/table/scans/data-table-row-actions.tsx @@ -1,22 +1,15 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - // DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import { DownloadIcon } from "lucide-react"; +import { Download, Pencil } from "lucide-react"; import { useState } from "react"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; import { useToast } from "@/components/ui"; import { downloadScanZip } from "@/lib/helper"; @@ -26,7 +19,6 @@ import { EditScanForm } from "../../forms"; interface DataTableRowActionsProps { row: Row; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; export function DataTableRowActions({ row, @@ -52,46 +44,26 @@ export function DataTableRowActions({
- - + - - - - } - onPress={() => downloadScanZip(scanId, toast)} - isDisabled={scanState !== "completed"} - > - Download .zip - - - - } - onPress={() => setIsEditOpen(true)} - > - Edit scan name - - - - + } + > + } + label="Download .zip" + description="Available only for completed scans" + onSelect={() => downloadScanZip(scanId, toast)} + disabled={scanState !== "completed"} + /> + } + label="Edit Scan Name" + onSelect={() => setIsEditOpen(true)} + /> +
); diff --git a/ui/components/scans/table/scans/scans-table-with-polling.tsx b/ui/components/scans/table/scans/scans-table-with-polling.tsx index 5c59a5f267..3854871941 100644 --- a/ui/components/scans/table/scans/scans-table-with-polling.tsx +++ b/ui/components/scans/table/scans/scans-table-with-polling.tsx @@ -59,6 +59,14 @@ export function ScansTableWithPolling({ const [scansData, setScansData] = useState(initialData); const [meta, setMeta] = useState(initialMeta); + // Sync state with server data when props change (e.g., pagination or filter changes). + // useState only uses its argument on first mount, so without this effect, + // navigating to page 2 would change the URL but keep showing page 1 data. + useEffect(() => { + setScansData(initialData); + setMeta(initialMeta); + }, [initialData, initialMeta]); + const hasExecutingScan = scansData.some((scan) => EXECUTING_STATES.includes( scan.attributes.state as (typeof EXECUTING_STATES)[number], diff --git a/ui/components/shadcn/badge/badge.tsx b/ui/components/shadcn/badge/badge.tsx index 59b863a541..f0b8306f4a 100644 --- a/ui/components/shadcn/badge/badge.tsx +++ b/ui/components/shadcn/badge/badge.tsx @@ -17,6 +17,7 @@ const badgeVariants = cva( "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + tag: "bg-bg-tag border-border-tag text-text-neutral-primary", }, }, defaultVariants: { diff --git a/ui/components/shadcn/checkbox/checkbox.tsx b/ui/components/shadcn/checkbox/checkbox.tsx index 43da49eb3e..930b70a236 100644 --- a/ui/components/shadcn/checkbox/checkbox.tsx +++ b/ui/components/shadcn/checkbox/checkbox.tsx @@ -1,24 +1,54 @@ "use client"; import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; -import { CheckIcon } from "lucide-react"; +import { CheckIcon, MinusIcon } from "lucide-react"; import { cn } from "@/lib/utils"; +const SIZE_STYLES = { + default: { + root: "size-6", + icon: "size-4", + }, + sm: { + root: "size-5", + icon: "size-3.5", + }, +} as const; + +type CheckboxSize = keyof typeof SIZE_STYLES; + +interface CheckboxProps + extends React.ComponentProps { + /** Size variant: "default" (24px) or "sm" (20px) */ + size?: CheckboxSize; + /** Show indeterminate state (minus icon) - used for partial selection in trees */ + indeterminate?: boolean; +} + function Checkbox({ className, + size = "default", + indeterminate, + checked, ...props -}: React.ComponentProps) { +}: CheckboxProps) { + const sizeStyles = SIZE_STYLES[size]; + return ( - + {indeterminate ? ( + + ) : ( + + )} ); } export { Checkbox }; +export type { CheckboxProps, CheckboxSize }; diff --git a/ui/components/shadcn/checkbox/index.ts b/ui/components/shadcn/checkbox/index.ts new file mode 100644 index 0000000000..bbf3fc318c --- /dev/null +++ b/ui/components/shadcn/checkbox/index.ts @@ -0,0 +1,2 @@ +export type { CheckboxProps, CheckboxSize } from "./checkbox"; +export { Checkbox } from "./checkbox"; diff --git a/ui/components/shadcn/command.tsx b/ui/components/shadcn/command.tsx index d848db5c27..4a3f672dc4 100644 --- a/ui/components/shadcn/command.tsx +++ b/ui/components/shadcn/command.tsx @@ -66,7 +66,7 @@ function CommandInput({ return (
) { - return ; + return ; } function DrawerTrigger({ diff --git a/ui/components/shadcn/dropdown/action-dropdown.tsx b/ui/components/shadcn/dropdown/action-dropdown.tsx index d272872129..3eeb7dc385 100644 --- a/ui/components/shadcn/dropdown/action-dropdown.tsx +++ b/ui/components/shadcn/dropdown/action-dropdown.tsx @@ -9,7 +9,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "./dropdown"; @@ -17,8 +16,6 @@ import { interface ActionDropdownProps { /** The dropdown trigger element. Defaults to a vertical dots icon button */ trigger?: ReactNode; - /** Label shown at the top of the dropdown */ - label?: string; /** Alignment of the dropdown content */ align?: "start" | "center" | "end"; /** Additional className for the content */ @@ -30,7 +27,6 @@ interface ActionDropdownProps { export function ActionDropdown({ trigger, - label = "Actions", align = "end", className, ariaLabel = "Open actions menu", @@ -52,16 +48,10 @@ export function ActionDropdown({ - {label && ( - <> - {label} - - - )} {children} @@ -91,8 +81,9 @@ export function ActionDropdownItem({ return ( svg]:size-5", - destructive && "text-destructive", + "text-muted-foreground mt-0.5 shrink-0 [&>svg]:size-4", + destructive && "text-text-error-primary", )} > {icon} @@ -113,7 +104,7 @@ export function ActionDropdownItem({ {description} @@ -124,8 +115,18 @@ export function ActionDropdownItem({ ); } -// Re-export commonly used components for convenience -export { - DropdownMenuLabel as ActionDropdownLabel, - DropdownMenuSeparator as ActionDropdownSeparator, -} from "./dropdown"; +export function ActionDropdownDangerZone({ + children, +}: { + children: ReactNode; +}) { + return ( + <> + + + Danger zone + + {children} + + ); +} diff --git a/ui/components/shadcn/dropdown/index.ts b/ui/components/shadcn/dropdown/index.ts index d21a2ff982..06169c0668 100644 --- a/ui/components/shadcn/dropdown/index.ts +++ b/ui/components/shadcn/dropdown/index.ts @@ -1,8 +1,7 @@ export { ActionDropdown, + ActionDropdownDangerZone, ActionDropdownItem, - ActionDropdownLabel, - ActionDropdownSeparator, } from "./action-dropdown"; export { DropdownMenu, diff --git a/ui/components/shadcn/popover.tsx b/ui/components/shadcn/popover.tsx index 711877134f..e64bae46ad 100644 --- a/ui/components/shadcn/popover.tsx +++ b/ui/components/shadcn/popover.tsx @@ -20,16 +20,19 @@ function PopoverContent({ className, align = "center", sideOffset = 4, + container, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { + container?: HTMLElement | null; +}) { return ( - + void; + defaultValue?: string[]; + placeholder?: string; + searchable?: boolean; + hideSelectAll?: boolean; + maxCount?: number; + closeOnSelect?: boolean; + resetOnDefaultValueChange?: boolean; + emptyIndicator?: ReactNode; + disabled?: boolean; + className?: string; + id?: string; + "aria-label"?: string; +} + +function arraysEqual(a: string[], b: string[]): boolean { + if (a.length !== b.length) return false; + const sortedA = [...a].sort(); + const sortedB = [...b].sort(); + return sortedA.every((val, index) => val === sortedB[index]); +} + +export function EnhancedMultiSelect({ + options, + onValueChange, + defaultValue = [], + placeholder = "Select options", + searchable = true, + hideSelectAll = false, + maxCount = 3, + closeOnSelect = false, + resetOnDefaultValueChange = true, + emptyIndicator, + disabled = false, + className, + id, + "aria-label": ariaLabel, +}: EnhancedMultiSelectProps) { + const [selectedValues, setSelectedValues] = useState(defaultValue); + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(""); + const [portalContainer, setPortalContainer] = useState( + null, + ); + + const buttonRef = useRef(null); + const prevDefaultValueRef = useRef(defaultValue); + const selectedAtOpenRef = useRef(selectedValues); + const multiSelectId = useId(); + const listboxId = `${multiSelectId}-listbox`; + + // Detect dialog container for portal stacking (critical for Jira modal) + useEffect(() => { + if (!buttonRef.current) return; + const closestDialogContainer = buttonRef.current.closest( + "[data-slot='dialog-content'], [data-slot='modal-content'], [role='dialog']", + ); + setPortalContainer( + closestDialogContainer instanceof HTMLElement + ? closestDialogContainer + : null, + ); + }, []); + + // Reset when defaultValue changes externally (e.g. React Hook Form reset) + useEffect(() => { + if (!resetOnDefaultValueChange) return; + const prev = prevDefaultValueRef.current; + if (!arraysEqual(prev, defaultValue)) { + if (!arraysEqual(selectedValues, defaultValue)) { + setSelectedValues(defaultValue); + } + prevDefaultValueRef.current = [...defaultValue]; + } + }, [defaultValue, selectedValues, resetOnDefaultValueChange]); + + function handleOpenChange(nextOpen: boolean) { + if (nextOpen) { + selectedAtOpenRef.current = [...selectedValues]; + } else { + setSearch(""); + } + setOpen(nextOpen); + } + + const enabledOptions = options.filter((o) => !o.disabled); + + const filteredOptions = ( + searchable && search + ? options.filter( + (o) => + o.label.toLowerCase().includes(search.toLowerCase()) || + o.value.toLowerCase().includes(search.toLowerCase()), + ) + : options + ).toSorted((a, b) => { + const snapshot = selectedAtOpenRef.current; + const aSelected = snapshot.includes(a.value) ? 0 : 1; + const bSelected = snapshot.includes(b.value) ? 0 : 1; + return aSelected - bSelected; + }); + + function getOptionByValue(value: string) { + return options.find((o) => o.value === value); + } + + function toggleOption(value: string) { + if (disabled) return; + const option = getOptionByValue(value); + if (option?.disabled) return; + const next = selectedValues.includes(value) + ? selectedValues.filter((v) => v !== value) + : [...selectedValues, value]; + setSelectedValues(next); + onValueChange(next); + if (closeOnSelect) setOpen(false); + } + + function toggleAll() { + if (disabled) return; + if (selectedValues.length === enabledOptions.length) { + handleClear(); + } else { + const all = enabledOptions.map((o) => o.value); + setSelectedValues(all); + onValueChange(all); + } + if (closeOnSelect) setOpen(false); + } + + function handleClear() { + if (disabled) return; + setSelectedValues([]); + onValueChange([]); + } + + return ( + + + + + setOpen(false)} + > + + {searchable && ( + + )} + + {emptyIndicator || "No results found."} + {!hideSelectAll && !search && ( + + + + + )} + + {filteredOptions.map((option) => { + const isSelected = selectedValues.includes(option.value); + return ( + toggleOption(option.value)} + role="option" + aria-selected={isSelected} + aria-disabled={option.disabled} + className={cn( + "cursor-pointer", + option.disabled && "cursor-not-allowed opacity-50", + )} + disabled={option.disabled} + > + + ); + })} + + + +
+ {selectedValues.length > 0 && ( + <> + + + + )} + +
+
+
+
+ ); +} + +EnhancedMultiSelect.displayName = "EnhancedMultiSelect"; +export type { EnhancedMultiSelectProps, MultiSelectOption }; diff --git a/ui/components/shadcn/select/multiselect.tsx b/ui/components/shadcn/select/multiselect.tsx index 8d1b6512ba..58b9b86035 100644 --- a/ui/components/shadcn/select/multiselect.tsx +++ b/ui/components/shadcn/select/multiselect.tsx @@ -224,9 +224,9 @@ export function MultiSelectValue({ .filter((value) => items.has(value)) .map((value) => ( {items.get(value)} {clickToRemove && ( - + )} ))} @@ -247,9 +247,9 @@ export function MultiSelectValue({ style={{ display: overflowAmount > 0 && !shouldWrap ? "block" : "none", }} - variant="outline" + variant="tag" ref={overflowRef} - className="text-bg-button-secondary border-slate-300 bg-slate-100 px-2 py-1 text-xs font-medium dark:border-slate-600 dark:bg-slate-800" + className="px-2 py-1 text-xs font-medium" > +{overflowAmount} diff --git a/ui/components/shadcn/tree-view/index.ts b/ui/components/shadcn/tree-view/index.ts new file mode 100644 index 0000000000..6483d3d611 --- /dev/null +++ b/ui/components/shadcn/tree-view/index.ts @@ -0,0 +1,13 @@ +export { TreeItemLabel } from "./tree-item-label"; +export { TreeLeaf } from "./tree-leaf"; +export { TreeNode } from "./tree-node"; +export { TreeSpinner } from "./tree-spinner"; +export { TreeStatusIcon } from "./tree-status-icon"; +export { TreeView } from "./tree-view"; +export { + getAllDescendantIds, + getTreeLeafPadding, + getTreeNodePadding, + TREE_INDENT_REM, + TREE_LEAF_EXTRA_PADDING_REM, +} from "./utils"; diff --git a/ui/components/shadcn/tree-view/tree-item-label.tsx b/ui/components/shadcn/tree-view/tree-item-label.tsx new file mode 100644 index 0000000000..8a40484c14 --- /dev/null +++ b/ui/components/shadcn/tree-view/tree-item-label.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/shadcn/tooltip"; +import { TreeDataItem } from "@/types/tree"; + +interface TreeItemLabelProps { + /** The tree item to display */ + item: TreeDataItem; + /** Optional text size class (defaults to text-base) */ + textClassName?: string; +} + +/** + * TreeItemLabel component - displays an item's icon and name with truncation. + * + * Features: + * - Optional icon rendering + * - Text truncation with tooltip on hover + * - Consistent layout across tree nodes and leaves + * + * This component extracts the common pattern used in TreeNode and TreeLeaf + * for displaying item content with overflow handling. + */ +export function TreeItemLabel({ item, textClassName }: TreeItemLabelProps) { + return ( +
+ {item.icon && } + + + + {item.name} + + + {item.name} + +
+ ); +} diff --git a/ui/components/shadcn/tree-view/tree-leaf.tsx b/ui/components/shadcn/tree-view/tree-leaf.tsx new file mode 100644 index 0000000000..a9cb6c815b --- /dev/null +++ b/ui/components/shadcn/tree-view/tree-leaf.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { KeyboardEvent } from "react"; + +import { Checkbox } from "@/components/shadcn/checkbox"; +import { cn } from "@/lib/utils"; +import { TreeLeafProps } from "@/types/tree"; + +import { TreeItemLabel } from "./tree-item-label"; +import { TreeSpinner } from "./tree-spinner"; +import { TreeStatusIcon } from "./tree-status-icon"; +import { getTreeLeafPadding } from "./utils"; + +/** + * TreeLeaf component for rendering leaf nodes (nodes without children). + * + * Features: + * - Selection via checkbox or click + * - Loading spinner state + * - Custom rendering via renderItem prop + * - Indentation based on nesting level + * - Keyboard navigation support (Enter/Space to select) + */ +export function TreeLeaf({ + item, + level, + selectedIds, + onSelectionChange, + showCheckboxes, + renderItem, +}: TreeLeafProps) { + const isSelected = selectedIds.includes(item.id); + + const handleSelect = () => { + if (!item.disabled) { + onSelectionChange(item.id, item); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleSelect(); + } + }; + + return ( +
+ {item.isLoading && } + {!item.isLoading && item.status && ( + + )} + + {showCheckboxes && ( + e.stopPropagation()} + /> + )} + + {renderItem ? ( + renderItem({ + item, + level, + isLeaf: true, + isSelected, + hasChildren: false, + }) + ) : ( + + )} +
+ ); +} diff --git a/ui/components/shadcn/tree-view/tree-node.tsx b/ui/components/shadcn/tree-view/tree-node.tsx new file mode 100644 index 0000000000..6c2a11e63d --- /dev/null +++ b/ui/components/shadcn/tree-view/tree-node.tsx @@ -0,0 +1,203 @@ +"use client"; + +import { AnimatePresence, motion } from "framer-motion"; +import { ChevronRightIcon } from "lucide-react"; +import { KeyboardEvent } from "react"; + +import { Checkbox } from "@/components/shadcn/checkbox"; +import { cn } from "@/lib/utils"; +import { TreeNodeProps } from "@/types/tree"; + +import { TreeItemLabel } from "./tree-item-label"; +import { TreeLeaf } from "./tree-leaf"; +import { TreeSpinner } from "./tree-spinner"; +import { TreeStatusIcon } from "./tree-status-icon"; +import { getAllDescendantIds, getTreeNodePadding } from "./utils"; + +/** + * TreeNode component for rendering expandable nodes with children. + * + * Features: + * - Collapsible content using Radix UI + * - Indeterminate checkbox state when partially selected + * - Recursive selection of all descendants + * - Loading spinner state + * - Custom rendering via renderItem prop + * - Keyboard navigation support (Enter/Space to select, Arrow keys to expand) + */ +export function TreeNode({ + item, + level, + selectedIds, + expandedIds, + onSelectionChange, + onExpandedChange, + showCheckboxes, + renderItem, + expandAll, + enableSelectChildren, +}: TreeNodeProps) { + const isExpanded = expandAll || expandedIds.includes(item.id); + const isSelected = selectedIds.includes(item.id); + + // Calculate indeterminate state based on descendant selection + const descendantIds = getAllDescendantIds(item); + const selectedDescendantCount = descendantIds.filter((id) => + selectedIds.includes(id), + ).length; + const isIndeterminate = + selectedDescendantCount > 0 && + selectedDescendantCount < descendantIds.length; + + const handleToggleExpand = () => { + const newExpandedIds = isExpanded + ? expandedIds.filter((id) => id !== item.id) + : [...expandedIds, item.id]; + onExpandedChange(newExpandedIds); + }; + + const handleSelect = () => { + onSelectionChange(item.id, item); + }; + + const handleContentClick = showCheckboxes ? handleSelect : handleToggleExpand; + + const handleKeyDown = (event: KeyboardEvent) => { + switch (event.key) { + case "Enter": + case " ": + event.preventDefault(); + handleContentClick(); + break; + case "ArrowRight": + if (!isExpanded) { + event.preventDefault(); + handleToggleExpand(); + } + break; + case "ArrowLeft": + if (isExpanded) { + event.preventDefault(); + handleToggleExpand(); + } + break; + } + }; + + return ( +
+
+ + + {!item.isLoading && item.status && ( + + )} + + {showCheckboxes && ( + e.stopPropagation()} + /> + )} + +
+ {renderItem ? ( + renderItem({ + item, + level, + isLeaf: false, + isSelected, + isExpanded, + isIndeterminate, + hasChildren: true, + }) + ) : ( + + )} +
+
+ + + {isExpanded && ( + + {item.children?.map((child) => ( +
  • + {child.children ? ( + + ) : ( + + )} +
  • + ))} +
    + )} +
    +
    + ); +} diff --git a/ui/components/shadcn/tree-view/tree-spinner.tsx b/ui/components/shadcn/tree-view/tree-spinner.tsx new file mode 100644 index 0000000000..6527cb2ccb --- /dev/null +++ b/ui/components/shadcn/tree-view/tree-spinner.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { cn } from "@/lib/utils"; + +interface TreeSpinnerProps { + className?: string; +} + +/** + * TreeSpinner component - a circular loading indicator for tree nodes. + * + * Features: + * - 20x20 (size-5) default size to match checkbox sm + * - 2.5px stroke for good visibility + * - Uses button-primary color + * - Smooth rotation animation + */ +export function TreeSpinner({ className }: TreeSpinnerProps) { + return ( + + {/* Background track */} + + {/* Animated arc */} + + + ); +} diff --git a/ui/components/shadcn/tree-view/tree-status-icon.tsx b/ui/components/shadcn/tree-view/tree-status-icon.tsx new file mode 100644 index 0000000000..1ef34024b1 --- /dev/null +++ b/ui/components/shadcn/tree-view/tree-status-icon.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { CircleCheckIcon, CircleXIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { TREE_ITEM_STATUS, TreeItemStatus } from "@/types/tree"; + +interface TreeStatusIconProps { + status: TreeItemStatus; + className?: string; +} + +/** + * TreeStatusIcon component - displays success or error status for tree nodes. + * + * Features: + * - CircleCheck icon for success (green) + * - CircleX icon for error (red) + * - Same size as TreeSpinner for consistent layout + */ +export function TreeStatusIcon({ status, className }: TreeStatusIconProps) { + if (status === TREE_ITEM_STATUS.SUCCESS) { + return ( + + ); + } + + if (status === TREE_ITEM_STATUS.ERROR) { + return ( + + ); + } + + return null; +} diff --git a/ui/components/shadcn/tree-view/tree-view.tsx b/ui/components/shadcn/tree-view/tree-view.tsx new file mode 100644 index 0000000000..940574677d --- /dev/null +++ b/ui/components/shadcn/tree-view/tree-view.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useState } from "react"; + +import { cn } from "@/lib/utils"; +import { TreeDataItem, TreeViewProps } from "@/types/tree"; + +import { TreeLeaf } from "./tree-leaf"; +import { TreeNode } from "./tree-node"; +import { getAllDescendantIds } from "./utils"; + +/** + * TreeView component for rendering hierarchical data structures. + * + * Features: + * - Recursive nested structure support + * - Controlled or uncontrolled selection state + * - Controlled or uncontrolled expansion state + * - Auto-select children when parent is selected (optional) + * - Indeterminate checkbox state for partial selection + * - Loading states with spinners + * - Custom rendering via renderItem prop + * + * @example + * ```tsx + * const data: TreeDataItem[] = [ + * { + * id: "org-1", + * name: "Organization", + * children: [ + * { id: "acc-1", name: "Account 1" }, + * { id: "acc-2", name: "Account 2" }, + * ], + * }, + * ]; + * + * + * ``` + */ +export function TreeView({ + data, + className, + selectedIds: controlledSelectedIds, + onSelectionChange, + expandedIds: controlledExpandedIds, + onExpandedChange, + expandAll = false, + showCheckboxes = false, + enableSelectChildren = true, + renderItem, +}: TreeViewProps) { + const [internalSelectedIds, setInternalSelectedIds] = useState([]); + const [internalExpandedIds, setInternalExpandedIds] = useState([]); + + const selectedIds = controlledSelectedIds ?? internalSelectedIds; + const expandedIds = controlledExpandedIds ?? internalExpandedIds; + + const handleSelectionChange = (itemId: string, item: TreeDataItem) => { + const isSelected = selectedIds.includes(itemId); + let newSelectedIds: string[]; + + if (enableSelectChildren && item.children) { + // When selecting a parent, also select/deselect all descendants + const allItemIds = getAllDescendantIds(item, true); + if (isSelected) { + // Deselect this item and all descendants + newSelectedIds = selectedIds.filter((id) => !allItemIds.includes(id)); + } else { + // Select this item and all descendants + newSelectedIds = Array.from(new Set([...selectedIds, ...allItemIds])); + } + } else { + // Simple toggle without affecting children + newSelectedIds = isSelected + ? selectedIds.filter((id) => id !== itemId) + : [...selectedIds, itemId]; + } + + if (onSelectionChange) { + onSelectionChange(newSelectedIds); + } else { + setInternalSelectedIds(newSelectedIds); + } + }; + + const handleExpandedChange = (newExpandedIds: string[]) => { + if (onExpandedChange) { + onExpandedChange(newExpandedIds); + } else { + setInternalExpandedIds(newExpandedIds); + } + }; + + const items = Array.isArray(data) ? data : [data]; + + return ( +
    +
      + {items.map((item) => ( +
    • + {item.children ? ( + + ) : ( + + )} +
    • + ))} +
    +
    + ); +} diff --git a/ui/components/shadcn/tree-view/utils.ts b/ui/components/shadcn/tree-view/utils.ts new file mode 100644 index 0000000000..1df696518d --- /dev/null +++ b/ui/components/shadcn/tree-view/utils.ts @@ -0,0 +1,50 @@ +import { TreeDataItem } from "@/types/tree"; + +/** + * Tree indentation constants (in rem units). + * Used to calculate consistent padding for nested tree items. + */ +export const TREE_INDENT_REM = 1.25; +export const TREE_LEAF_EXTRA_PADDING_REM = 1.5; + +/** + * Calculates the left padding for a tree node based on its nesting level. + */ +export function getTreeNodePadding(level: number): string { + return `${level * TREE_INDENT_REM}rem`; +} + +/** + * Calculates the left padding for a tree leaf based on its nesting level. + * Leaves have extra padding to align with node content (accounting for expand button). + */ +export function getTreeLeafPadding(level: number): string { + return `${level * TREE_INDENT_REM + TREE_LEAF_EXTRA_PADDING_REM}rem`; +} + +/** + * Recursively collects all descendant IDs from a tree item. + * + * @param item - The tree item to collect IDs from + * @param includeParent - Whether to include the parent item's ID (default: false) + * @returns Array of all descendant IDs (and optionally the parent ID) + * + * @example + * Get only descendant IDs + * const childIds = getAllDescendantIds(parentItem); + * + * Get parent + all descendant IDs + * const allIds = getAllDescendantIds(parentItem, true); + */ +export function getAllDescendantIds( + item: TreeDataItem, + includeParent = false, +): string[] { + const ids = includeParent ? [item.id] : []; + if (item.children) { + for (const child of item.children) { + ids.push(child.id, ...getAllDescendantIds(child, false)); + } + } + return ids; +} diff --git a/ui/components/ui/button/button.test.tsx b/ui/components/ui/button/button.test.tsx new file mode 100644 index 0000000000..98cb6da88b --- /dev/null +++ b/ui/components/ui/button/button.test.tsx @@ -0,0 +1,61 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; + +import { Button } from "./button"; + +describe("Button", () => { + it("renders with children", () => { + render(); + + expect( + screen.getByRole("button", { name: "Click me" }), + ).toBeInTheDocument(); + }); + + it("handles click events", async () => { + const user = userEvent.setup(); + const handleClick = vi.fn(); + + render(); + + await user.click(screen.getByRole("button")); + + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it("can be disabled", () => { + render(); + + expect(screen.getByRole("button")).toBeDisabled(); + }); + + it("applies variant classes correctly", () => { + const { rerender } = render(); + + expect(screen.getByRole("button")).toHaveClass("bg-destructive"); + + rerender(); + + expect(screen.getByRole("button")).toHaveClass("border"); + }); + + it("applies size classes correctly", () => { + render(); + + expect(screen.getByRole("button")).toHaveClass("h-8"); + }); + + it("renders as child component when asChild is true", () => { + render( + , + ); + + const link = screen.getByRole("link", { name: "Link Button" }); + + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute("href", "/test"); + }); +}); diff --git a/ui/components/ui/custom/custom-dropdown-selection.tsx b/ui/components/ui/custom/custom-dropdown-selection.tsx deleted file mode 100644 index 4f7e3d6bf0..0000000000 --- a/ui/components/ui/custom/custom-dropdown-selection.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import React, { useCallback } from "react"; - -import { - MultiSelect, - MultiSelectContent, - MultiSelectItem, - MultiSelectTrigger, - MultiSelectValue, -} from "@/components/shadcn/select/multiselect"; - -interface CustomDropdownSelectionProps { - label: string; - name: string; - values: { id: string; name: string }[]; - onChange: (name: string, selectedValues: string[]) => void; - selectedKeys?: string[]; -} - -export const CustomDropdownSelection: React.FC< - CustomDropdownSelectionProps -> = ({ label, name, values, onChange, selectedKeys = [] }) => { - const handleValuesChange = useCallback( - (newValues: string[]) => { - onChange(name, newValues); - }, - [name, onChange], - ); - - return ( -
    -

    {label}

    - - - - - - {values.map((item) => ( - - {item.name} - - ))} - - -
    - ); -}; diff --git a/ui/components/ui/custom/index.ts b/ui/components/ui/custom/index.ts index 0007e08580..6314862ea7 100644 --- a/ui/components/ui/custom/index.ts +++ b/ui/components/ui/custom/index.ts @@ -1,5 +1,4 @@ export * from "./custom-banner"; -export * from "./custom-dropdown-selection"; export * from "./custom-input"; export * from "./custom-link"; export * from "./custom-modal-buttons"; diff --git a/ui/components/ui/table/data-table-animated-row.tsx b/ui/components/ui/table/data-table-animated-row.tsx new file mode 100644 index 0000000000..8ea153df4d --- /dev/null +++ b/ui/components/ui/table/data-table-animated-row.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { Cell, flexRender, Row } from "@tanstack/react-table"; +import { motion } from "framer-motion"; + +import { cn } from "@/lib/utils"; + +interface DataTableAnimatedRowProps { + row: Row; +} + +/** + * DataTableAnimatedRow renders a table row with smooth expand/collapse animations. + * + * The trick: You cannot animate height directly (tables ignore it). + * Instead, we wrap each cell's content in a motion.div and animate THAT. + * + * How it works: + * 1. The itself is not animated + * 2. Each contains a motion.div wrapper + * 3. The wrapper animates height from 0 to "auto" + * 4. overflow-hidden clips content during animation + * 5. Padding is on the inner content, not the td + */ +export function DataTableAnimatedRow({ + row, +}: DataTableAnimatedRowProps) { + return ( + td:first-child]:rounded-l-full [&>td:last-child]:rounded-r-full", + "hover:bg-bg-neutral-tertiary", + "data-[state=selected]:bg-bg-neutral-tertiary", + )} + > + {row.getVisibleCells().map((cell: Cell, index, cells) => { + const isFirst = index === 0; + const isLast = index === cells.length - 1; + + return ( + + +
    + {flexRender(cell.column.columnDef.cell, cell.getContext())} +
    +
    + + ); + })} +
    + ); +} diff --git a/ui/components/ui/table/data-table-expand-all-toggle.tsx b/ui/components/ui/table/data-table-expand-all-toggle.tsx new file mode 100644 index 0000000000..c53782f451 --- /dev/null +++ b/ui/components/ui/table/data-table-expand-all-toggle.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { Table } from "@tanstack/react-table"; +import { Maximize2Icon, Minimize2Icon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +interface DataTableExpandAllToggleProps { + table: Table; +} + +/** + * DataTableExpandAllToggle provides a button in the table header to expand + * or collapse all rows at once. + * + * Features: + * - Shows Maximize2 icon when rows are collapsed (click to expand all) + * - Shows Minimize2 icon when rows are expanded (click to collapse all) + * - Accessible with proper aria-label + * - Only renders when the table has expandable rows + * + * @example + * ```tsx + * // In column definition header: + * { + * id: "name", + * header: ({ table }) => ( + *
    + * + * Name + *
    + * ), + * cell: ({ row }) => ( + * + * {row.original.name} + * + * ), + * } + * ``` + */ +export function DataTableExpandAllToggle({ + table, +}: DataTableExpandAllToggleProps) { + const isAllExpanded = table.getIsAllRowsExpanded(); + const canExpand = table.getCanSomeRowsExpand(); + + if (!canExpand) { + return null; + } + + return ( + + ); +} diff --git a/ui/components/ui/table/data-table-expand-toggle.tsx b/ui/components/ui/table/data-table-expand-toggle.tsx new file mode 100644 index 0000000000..4258ee8a0b --- /dev/null +++ b/ui/components/ui/table/data-table-expand-toggle.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { Row } from "@tanstack/react-table"; +import { ChevronRightIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +interface DataTableExpandToggleProps { + row: Row; +} + +/** + * DataTableExpandToggle provides a clickable chevron button for expanding/collapsing + * table rows that have sub-rows. + * + * Features: + * - Only shows for rows that can expand (have sub-rows) + * - Provides consistent spacing for rows without sub-rows + * - Animates chevron rotation on expand/collapse + * - Accessible with proper aria-label + * + * @example + * ```tsx + * // In column definition: + * { + * id: "expand", + * cell: ({ row }) => , + * } + * ``` + */ +export function DataTableExpandToggle({ + row, +}: DataTableExpandToggleProps) { + if (!row.getCanExpand()) { + // Return a spacer div for alignment when row has no sub-rows + return
    ; + } + + return ( + + ); +} diff --git a/ui/components/ui/table/data-table-expandable-cell.tsx b/ui/components/ui/table/data-table-expandable-cell.tsx new file mode 100644 index 0000000000..78bfa651cf --- /dev/null +++ b/ui/components/ui/table/data-table-expandable-cell.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { Row } from "@tanstack/react-table"; +import { CornerDownRightIcon } from "lucide-react"; + +import { DataTableExpandToggle } from "./data-table-expand-toggle"; + +/** Indentation per nesting level in rem units */ +const INDENT_PER_LEVEL_REM = 1.5; + +interface DataTableExpandableCellProps { + row: Row; + children: React.ReactNode; + /** Whether to show the expand/collapse toggle (default: true) */ + showToggle?: boolean; +} + +/** + * DataTableExpandableCell is a wrapper component for table cells that need + * to display content with proper indentation for nested rows. + * + * Features: + * - Automatically indents content based on row depth + * - Shows CornerDownRight icon for child rows (depth > 0) + * - Optionally includes the expand/collapse toggle for parent rows + * - Maintains proper alignment for all nesting levels + * + * @example + * ```tsx + * // In column definition: + * { + * accessorKey: "name", + * header: "Name", + * cell: ({ row }) => ( + * + * {row.original.name} + * + * ), + * } + * ``` + */ +export function DataTableExpandableCell({ + row, + children, + showToggle = true, +}: DataTableExpandableCellProps) { + const isChildRow = row.depth > 0; + const canExpand = row.getCanExpand(); + + return ( +
    + {showToggle && ( + <> + {canExpand ? ( + + ) : isChildRow ? ( + + ) : ( +
    + )} + + )} + {children} +
    + ); +} diff --git a/ui/components/ui/table/data-table.tsx b/ui/components/ui/table/data-table.tsx index c34f446e05..3fbdd780cc 100644 --- a/ui/components/ui/table/data-table.tsx +++ b/ui/components/ui/table/data-table.tsx @@ -3,8 +3,10 @@ import { ColumnDef, ColumnFiltersState, + ExpandedState, flexRender, getCoreRowModel, + getExpandedRowModel, getFilteredRowModel, getSortedRowModel, OnChangeFn, @@ -13,7 +15,8 @@ import { SortingState, useReactTable, } from "@tanstack/react-table"; -import { useState } from "react"; +import { AnimatePresence } from "framer-motion"; +import { useEffect, useRef, useState } from "react"; import { Table, @@ -23,12 +26,20 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { DataTableAnimatedRow } from "@/components/ui/table/data-table-animated-row"; import { DataTablePagination } from "@/components/ui/table/data-table-pagination"; import { DataTableSearch } from "@/components/ui/table/data-table-search"; import { useFilterTransitionOptional } from "@/contexts"; import { cn } from "@/lib"; import { FilterOption, MetaDataProps } from "@/types"; +/** + * Default column size used by TanStack Table when no explicit size is set. + * We skip applying inline width styles for columns with this default value + * to allow them to flex naturally within the table layout. + */ +const DEFAULT_COLUMN_SIZE = 150; + interface DataTableProviderProps { columns: ColumnDef[]; data: TData[]; @@ -42,6 +53,16 @@ interface DataTableProviderProps { getRowCanSelect?: (row: Row) => boolean; /** Show search bar in the table toolbar */ showSearch?: boolean; + /** Function to extract sub-rows from a row for hierarchical data */ + getSubRows?: (row: TData) => TData[] | undefined; + /** Controlled expanded state */ + expanded?: ExpandedState; + /** Callback when expanded state changes */ + onExpandedChange?: OnChangeFn; + /** Auto-select children when parent selected (default: true) */ + enableSubRowSelection?: boolean; + /** Expand all rows by default, or provide specific expanded state */ + defaultExpanded?: boolean | ExpandedState; /** Prefix for URL params to avoid conflicts (e.g., "findings" -> "findingsPage") */ paramPrefix?: string; @@ -87,6 +108,11 @@ export function DataTable({ onRowSelectionChange, getRowCanSelect, showSearch = false, + getSubRows, + expanded: controlledExpanded, + onExpandedChange, + enableSubRowSelection = true, + defaultExpanded, paramPrefix = "", controlledSearch, onSearchChange, @@ -98,6 +124,12 @@ export function DataTable({ }: DataTableProviderProps) { const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); + // ExpandedState should be a Record for individual row control + // Note: We don't use `true` (boolean) as it makes rows permanently expanded + const [expanded, setExpanded] = useState(() => { + if (typeof defaultExpanded === "object") return defaultExpanded; + return {}; + }); // Get transition state from context for loading indicator const filterTransition = useFilterTransitionOptional(); @@ -116,18 +148,48 @@ export function DataTable({ getFilteredRowModel: getFilteredRowModel(), onRowSelectionChange, manualPagination: true, + // Expansion support for hierarchical data + getSubRows, + getExpandedRowModel: getSubRows ? getExpandedRowModel() : undefined, + enableSubRowSelection, + onExpandedChange: onExpandedChange ?? setExpanded, state: { sorting, columnFilters, rowSelection: rowSelection ?? {}, + expanded: controlledExpanded ?? expanded, }, }); + // Track whether initial expansion has been applied + const hasInitiallyExpanded = useRef(false); + + // Expand all rows on mount when defaultExpanded={true} + useEffect(() => { + if ( + !hasInitiallyExpanded.current && + defaultExpanded === true && + getSubRows + ) { + table.toggleAllRowsExpanded(true); + hasInitiallyExpanded.current = true; + } + }, [defaultExpanded, getSubRows, table]); + // Calculate selection key to force header re-render when selection changes const selectionKey = rowSelection ? Object.keys(rowSelection).filter((k) => rowSelection[k]).length : 0; + // Calculate expansion key to force header re-render when expansion changes + const currentExpanded = controlledExpanded ?? expanded; + const expansionKey = + currentExpanded === true + ? "all" + : typeof currentExpanded === "object" + ? Object.keys(currentExpanded).filter((k) => currentExpanded[k]).length + : 0; + // Format total entries count const totalEntries = metadata?.pagination?.count ?? 0; const formattedTotal = totalEntries.toLocaleString(); @@ -161,13 +223,21 @@ export function DataTable({ )}
    )} - +
    {table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => { + const size = header.getSize(); return ( - + {header.isPlaceholder ? null : flexRender( @@ -181,26 +251,38 @@ export function DataTable({ ))} - {rows?.length ? ( - rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + + {rows?.length ? ( + rows.map((row) => + getSubRows && row.depth > 0 ? ( + + ) : ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + ), + ) + ) : ( + + + No results. + - )) - ) : ( - - - No results. - - - )} + )} +
    {metadata && ( diff --git a/ui/components/users/forms/edit-form.tsx b/ui/components/users/forms/edit-form.tsx index 19cef75624..26b94f518d 100644 --- a/ui/components/users/forms/edit-form.tsx +++ b/ui/components/users/forms/edit-form.tsx @@ -1,6 +1,5 @@ "use client"; -import { Select, SelectItem } from "@heroui/select"; import { zodResolver } from "@hookform/resolvers/zod"; import { ShieldIcon, UserIcon } from "lucide-react"; import { Dispatch, SetStateAction } from "react"; @@ -9,6 +8,13 @@ import * as z from "zod"; import { updateUser, updateUserRole } from "@/actions/users/users"; import { Card } from "@/components/shadcn"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/shadcn/select/select"; import { useToast } from "@/components/ui"; import { CustomInput } from "@/components/ui/custom"; import { Form, FormButtons } from "@/components/ui/form"; @@ -168,29 +174,22 @@ export const EditForm = ({ />
    -
    +
    ( - + + + + + {roles.map((role: { id: string; name: string }) => ( + + {role.name} + + ))} + )} /> diff --git a/ui/components/users/profile/api-keys/data-table-row-actions.tsx b/ui/components/users/profile/api-keys/data-table-row-actions.tsx index 51af064c8a..b47eaab0cc 100644 --- a/ui/components/users/profile/api-keys/data-table-row-actions.tsx +++ b/ui/components/users/profile/api-keys/data-table-row-actions.tsx @@ -1,21 +1,15 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; +import { Pencil, Trash2 } from "lucide-react"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { EnrichedApiKey } from "./types"; @@ -25,8 +19,6 @@ interface DataTableRowActionsProps { onRevoke: (apiKey: EnrichedApiKey) => void; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; - export function DataTableRowActions({ row, onEdit, @@ -39,53 +31,29 @@ export function DataTableRowActions({ return (
    - - + - - - - } - onPress={() => onEdit(apiKey)} - > - Edit name - - - {canRevoke ? ( - - - } - onPress={() => onRevoke(apiKey)} - > - Revoke - - - ) : null} - - + } + > + } + label="Edit API Key" + onSelect={() => onEdit(apiKey)} + /> + {canRevoke && ( + + } + label="Revoke API Key" + destructive + onSelect={() => onRevoke(apiKey)} + /> + + )} +
    ); } diff --git a/ui/components/users/table/data-table-row-actions.tsx b/ui/components/users/table/data-table-row-actions.tsx index 41f81290da..fafabb5011 100644 --- a/ui/components/users/table/data-table-row-actions.tsx +++ b/ui/components/users/table/data-table-row-actions.tsx @@ -1,22 +1,16 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; +import { Pencil, Trash2 } from "lucide-react"; import { useState } from "react"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; import { DeleteForm, EditForm } from "../forms"; @@ -25,7 +19,6 @@ interface DataTableRowActionsProps { row: Row; roles?: { id: string; name: string }[]; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; export function DataTableRowActions({ row, @@ -66,51 +59,27 @@ export function DataTableRowActions({
    - - + - - - - } - onPress={() => setIsEditOpen(true)} - > - Edit User - - - - - } - onPress={() => setIsDeleteOpen(true)} - > - Delete User - - - - + } + > + } + label="Edit User" + onSelect={() => setIsEditOpen(true)} + /> + + } + label="Delete User" + destructive + onSelect={() => setIsDeleteOpen(true)} + /> + +
    ); diff --git a/ui/contexts/filter-transition-context.tsx b/ui/contexts/filter-transition-context.tsx index cd529e0a3c..b283829e3d 100644 --- a/ui/contexts/filter-transition-context.tsx +++ b/ui/contexts/filter-transition-context.tsx @@ -1,16 +1,17 @@ "use client"; +import { useSearchParams } from "next/navigation"; import { createContext, ReactNode, - TransitionStartFunction, useContext, - useTransition, + useEffect, + useState, } from "react"; interface FilterTransitionContextType { isPending: boolean; - startTransition: TransitionStartFunction; + signalFilterChange: () => void; } const FilterTransitionContext = createContext< @@ -39,13 +40,33 @@ interface FilterTransitionProviderProps { children: ReactNode; } +/** + * Provides a shared pending state for filter changes. + * + * Filter components signal the start of navigation via signalFilterChange(), + * and use their own local useTransition() for the actual router.push(). + * This avoids a known Next.js production bug where a shared useTransition() + * wrapping router.push() causes the navigation to be silently reverted. + * + * The pending state auto-resets when searchParams change (navigation completed). + */ export const FilterTransitionProvider = ({ children, }: FilterTransitionProviderProps) => { - const [isPending, startTransition] = useTransition(); + const searchParams = useSearchParams(); + const [isPending, setIsPending] = useState(false); + + // Auto-reset pending state when searchParams change (navigation completed) + useEffect(() => { + setIsPending(false); + }, [searchParams]); + + const signalFilterChange = () => { + setIsPending(true); + }; return ( - + {children} ); diff --git a/ui/dependency-log.json b/ui/dependency-log.json index 65b799d3f9..79fadb3a75 100644 --- a/ui/dependency-log.json +++ b/ui/dependency-log.json @@ -10,10 +10,10 @@ { "section": "dependencies", "name": "@aws-sdk/client-bedrock-runtime", - "from": "3.943.0", - "to": "3.948.0", + "from": "3.948.0", + "to": "3.988.0", "strategy": "installed", - "generatedAt": "2025-12-15T08:24:46.195Z" + "generatedAt": "2026-02-12T11:04:39.164Z" }, { "section": "dependencies", @@ -67,9 +67,9 @@ "section": "dependencies", "name": "@langchain/mcp-adapters", "from": "1.0.3", - "to": "1.0.3", + "to": "1.1.3", "strategy": "installed", - "generatedAt": "2025-12-12T10:01:54.132Z" + "generatedAt": "2026-02-12T11:04:39.164Z" }, { "section": "dependencies", @@ -283,9 +283,9 @@ "section": "dependencies", "name": "@tailwindcss/postcss", "from": "4.1.13", - "to": "4.1.13", + "to": "4.1.18", "strategy": "installed", - "generatedAt": "2025-10-22T12:36:37.962Z" + "generatedAt": "2026-02-12T11:04:39.164Z" }, { "section": "dependencies", @@ -695,6 +695,30 @@ "strategy": "installed", "generatedAt": "2025-10-22T12:36:37.962Z" }, + { + "section": "devDependencies", + "name": "@testing-library/jest-dom", + "from": "6.9.1", + "to": "6.9.1", + "strategy": "installed", + "generatedAt": "2026-01-29T16:42:27.795Z" + }, + { + "section": "devDependencies", + "name": "@testing-library/react", + "from": "16.3.2", + "to": "16.3.2", + "strategy": "installed", + "generatedAt": "2026-01-29T16:42:27.795Z" + }, + { + "section": "devDependencies", + "name": "@testing-library/user-event", + "from": "14.6.1", + "to": "14.6.1", + "strategy": "installed", + "generatedAt": "2026-01-29T16:42:27.795Z" + }, { "section": "devDependencies", "name": "@types/d3", @@ -775,6 +799,22 @@ "strategy": "installed", "generatedAt": "2026-01-19T13:54:24.770Z" }, + { + "section": "devDependencies", + "name": "@vitejs/plugin-react", + "from": "5.1.2", + "to": "5.1.2", + "strategy": "installed", + "generatedAt": "2026-01-29T16:42:27.795Z" + }, + { + "section": "devDependencies", + "name": "@vitest/coverage-v8", + "from": "4.0.18", + "to": "4.0.18", + "strategy": "installed", + "generatedAt": "2026-01-29T16:42:27.795Z" + }, { "section": "devDependencies", "name": "autoprefixer", @@ -911,6 +951,14 @@ "strategy": "installed", "generatedAt": "2025-10-22T12:36:37.962Z" }, + { + "section": "devDependencies", + "name": "jsdom", + "from": "27.4.0", + "to": "27.4.0", + "strategy": "installed", + "generatedAt": "2026-01-29T16:42:27.795Z" + }, { "section": "devDependencies", "name": "lint-staged", @@ -947,9 +995,9 @@ "section": "devDependencies", "name": "shadcn", "from": "3.4.1", - "to": "3.4.1", + "to": "3.8.4", "strategy": "installed", - "generatedAt": "2025-10-22T15:52:15.849Z" + "generatedAt": "2026-02-12T11:04:39.164Z" }, { "section": "devDependencies", @@ -963,9 +1011,9 @@ "section": "devDependencies", "name": "tailwindcss", "from": "4.1.13", - "to": "4.1.13", + "to": "4.1.18", "strategy": "installed", - "generatedAt": "2025-10-22T12:36:37.962Z" + "generatedAt": "2026-02-12T11:04:39.164Z" }, { "section": "devDependencies", @@ -974,5 +1022,13 @@ "to": "5.5.4", "strategy": "installed", "generatedAt": "2025-10-22T12:36:37.962Z" + }, + { + "section": "devDependencies", + "name": "vitest", + "from": "4.0.18", + "to": "4.0.18", + "strategy": "installed", + "generatedAt": "2026-01-29T16:42:27.795Z" } ] diff --git a/ui/hooks/use-related-filters.ts b/ui/hooks/use-related-filters.ts index 0fc4ce2de5..424b360e51 100644 --- a/ui/hooks/use-related-filters.ts +++ b/ui/hooks/use-related-filters.ts @@ -1,7 +1,5 @@ import { useSearchParams } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; -import { useUrlFilters } from "@/hooks/use-url-filters"; import { isScanEntity } from "@/lib/helper-filters"; import { FilterEntity, @@ -21,6 +19,19 @@ interface UseRelatedFiltersProps { providerFilterType?: FilterType.PROVIDER | FilterType.PROVIDER_UID; } +/** + * Derives available providers and scans based on the current URL filters. + * + * Pure computation — no effects, no state, no navigation. The returned + * lists update automatically when searchParams change because the component + * re-renders with new searchParams from Next.js. + * + * Cascading filter cleanup (e.g. auto-clearing a scan when its provider is + * deselected) is handled atomically by the filter components themselves + * (ProviderTypeSelector clears provider_id__in, AccountsSelector updates + * provider_type__in). This avoids the production bug where router.push() + * calls inside useEffect would silently abort pending navigations. + */ export const useRelatedFilters = ({ providerIds = [], providerUIDs = [], @@ -31,33 +42,18 @@ export const useRelatedFilters = ({ providerFilterType = FilterType.PROVIDER, }: UseRelatedFiltersProps) => { const searchParams = useSearchParams(); - const { updateFilter } = useUrlFilters(); - const [availableScans, setAvailableScans] = - useState(completedScanIds); - // Use providerIds if provided (for findings), otherwise use providerUIDs (for scans) const providers = providerIds.length > 0 ? providerIds : providerUIDs; - const [availableProviders, setAvailableProviders] = - useState(providers); - const previousProviders = useRef([]); - const previousProviderTypes = useRef([]); - const isManualDeselection = useRef(false); - const getScanProvider = (scanId: string) => { - if (!enableScanRelation) return null; - const scanDetail = scanDetails.find( - (detail) => Object.keys(detail)[0] === scanId, - ); - return scanDetail ? scanDetail[scanId]?.providerInfo?.uid : null; - }; + const providerParam = searchParams.get(`filter[${providerFilterType}]`); + const providerTypeParam = searchParams.get( + `filter[${FilterType.PROVIDER_TYPE}]`, + ); - const getScanProviderType = (scanId: string): ProviderType | null => { - if (!enableScanRelation) return null; - const scanDetail = scanDetails.find( - (detail) => Object.keys(detail)[0] === scanId, - ); - return scanDetail ? scanDetail[scanId]?.providerInfo?.provider : null; - }; + const currentProviders = providerParam ? providerParam.split(",") : []; + const currentProviderTypes = providerTypeParam + ? (providerTypeParam.split(",") as ProviderType[]) + : []; const getProviderType = (providerKey: string): ProviderType | null => { const providerDetail = providerDetails.find( @@ -72,128 +68,28 @@ export const useRelatedFilters = ({ return null; }; - useEffect(() => { - const scanParam = enableScanRelation - ? searchParams.get(`filter[${FilterType.SCAN}]`) - : null; - const providerParam = searchParams.get(`filter[${providerFilterType}]`); - const providerTypeParam = searchParams.get( - `filter[${FilterType.PROVIDER_TYPE}]`, - ); + // Derive available providers filtered by selected provider types + const availableProviders = + currentProviderTypes.length > 0 + ? providers.filter((key) => { + const providerType = getProviderType(key); + return providerType && currentProviderTypes.includes(providerType); + }) + : providers; - const currentProviders = providerParam ? providerParam.split(",") : []; - const currentProviderTypes = providerTypeParam - ? (providerTypeParam.split(",") as ProviderType[]) - : []; + // Derive available scans filtered by selected providers and provider types + const availableScans = enableScanRelation + ? currentProviders.length > 0 || currentProviderTypes.length > 0 + ? completedScanIds.filter((scanId) => { + const scanDetail = scanDetails.find( + (detail) => Object.keys(detail)[0] === scanId, + ); + if (!scanDetail) return false; - // Detect deselected items - const deselectedProviders = previousProviders.current.filter( - (provider) => !currentProviders.includes(provider), - ); - const deselectedProviderTypes = previousProviderTypes.current.filter( - (type) => !currentProviderTypes.includes(type), - ); - - // Check if it's a manual deselection - if (deselectedProviderTypes.length > 0) { - isManualDeselection.current = true; - } else if ( - currentProviderTypes.length === 0 && - previousProviderTypes.current.length === 0 - ) { - isManualDeselection.current = false; - } - - // Update references - previousProviders.current = currentProviders; - previousProviderTypes.current = currentProviderTypes; - - // Handle scan selection logic - if (enableScanRelation && scanParam) { - const scanProviderId = getScanProvider(scanParam); - const scanProviderType = getScanProviderType(scanParam); - - const shouldDeselectScan = - (scanProviderId && - (deselectedProviders.includes(scanProviderId) || - (currentProviders.length > 0 && - !currentProviders.includes(scanProviderId)))) || - (scanProviderType && - !isManualDeselection.current && - (deselectedProviderTypes.includes(scanProviderType) || - (currentProviderTypes.length > 0 && - !currentProviderTypes.includes(scanProviderType)))); - - if (shouldDeselectScan) { - updateFilter(FilterType.SCAN, null); - // } else { - // // Add provider if not already selected - // if (scanProviderId && !currentProviders.includes(scanProviderId)) { - // updateFilter(FilterType.PROVIDER_UID, [ - // ...currentProviders, - // scanProviderId, - // ]); - // } - - // // Only add provider type if there are none selected - // if ( - // scanProviderType && - // currentProviderTypes.length === 0 && - // !isManualDeselection.current - // ) { - // updateFilter(FilterType.PROVIDER_TYPE, [scanProviderType]); - // } - } - } - - // // Handle provider selection logic - // if ( - // currentProviders.length > 0 && - // deselectedProviders.length === 0 && - // !isManualDeselection.current - // ) { - // const providerTypes = currentProviders - // .map(getProviderType) - // .filter((type): type is ProviderType => type !== null); - // const selectedProviderTypes = Array.from(new Set(providerTypes)); - - // if ( - // selectedProviderTypes.length > 0 && - // currentProviderTypes.length === 0 - // ) { - // updateFilter(FilterType.PROVIDER_TYPE, selectedProviderTypes); - // } - // } - - // Update available providers - if (currentProviderTypes.length > 0) { - const filteredProviders = providers.filter((key) => { - const providerType = getProviderType(key); - return providerType && currentProviderTypes.includes(providerType); - }); - setAvailableProviders(filteredProviders); - - const validProviders = currentProviders.filter((key) => { - const providerType = getProviderType(key); - return providerType && currentProviderTypes.includes(providerType); - }); - - if (validProviders.length !== currentProviders.length) { - updateFilter( - providerFilterType, - validProviders.length > 0 ? validProviders : null, - ); - } - } else { - setAvailableProviders(providers); - } - - // Update available scans - if (enableScanRelation) { - if (currentProviders.length > 0 || currentProviderTypes.length > 0) { - const filteredScans = completedScanIds.filter((scanId) => { - const scanProviderId = getScanProvider(scanId); - const scanProviderType = getScanProviderType(scanId); + const scanProviderId = scanDetail[scanId]?.providerInfo?.uid ?? null; + const scanProviderType = + (scanDetail[scanId]?.providerInfo?.provider as ProviderType) ?? + null; return ( (currentProviders.length === 0 || @@ -202,14 +98,9 @@ export const useRelatedFilters = ({ (scanProviderType && currentProviderTypes.includes(scanProviderType))) ); - }); - setAvailableScans(filteredScans); - } else { - setAvailableScans(completedScanIds); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchParams]); + }) + : completedScanIds + : completedScanIds; return { availableProviderIds: providerIds.length > 0 ? availableProviders : [], diff --git a/ui/hooks/use-url-filters.ts b/ui/hooks/use-url-filters.ts index 911aa61558..e0afa764f4 100644 --- a/ui/hooks/use-url-filters.ts +++ b/ui/hooks/use-url-filters.ts @@ -1,89 +1,90 @@ "use client"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useTransition } from "react"; import { useFilterTransitionOptional } from "@/contexts"; +const FINDINGS_PATH = "/findings"; +const DEFAULT_MUTED_FILTER = "false"; + /** * Custom hook to handle URL filters and automatically reset * pagination when filters change. * - * Uses useTransition to prevent full page reloads when filters change, - * keeping the current UI visible while the new data loads. - * - * When used within a FilterTransitionProvider, the transition state is shared - * across all components using this hook, enabling coordinated loading indicators. + * Uses client-side router navigation to update query params without + * full page reloads when filters change. */ export const useUrlFilters = () => { const router = useRouter(); const searchParams = useSearchParams(); const pathname = usePathname(); + const filterTransition = useFilterTransitionOptional(); + const isPending = false; - // Use shared context if available, otherwise fall back to local transition - const sharedTransition = useFilterTransitionOptional(); - const [localIsPending, localStartTransition] = useTransition(); + const ensureFindingsDefaultMuted = (params: URLSearchParams) => { + // Findings defaults to excluding muted findings unless user sets it explicitly. + if (pathname === FINDINGS_PATH && !params.has("filter[muted]")) { + params.set("filter[muted]", DEFAULT_MUTED_FILTER); + } + }; - const isPending = sharedTransition?.isPending ?? localIsPending; - const startTransition = - sharedTransition?.startTransition ?? localStartTransition; + const navigate = (params: URLSearchParams) => { + ensureFindingsDefaultMuted(params); - const updateFilter = useCallback( - (key: string, value: string | string[] | null) => { - const params = new URLSearchParams(searchParams.toString()); + const queryString = params.toString(); + if (queryString === searchParams.toString()) return; - const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`; + const targetUrl = queryString ? `${pathname}?${queryString}` : pathname; + filterTransition?.signalFilterChange(); + router.push(targetUrl, { scroll: false }); + }; - const currentValue = params.get(filterKey); - const nextValue = Array.isArray(value) - ? value.length > 0 - ? value.join(",") - : null - : value === null - ? null - : value; + const updateFilter = (key: string, value: string | string[] | null) => { + const params = new URLSearchParams(searchParams.toString()); - // If effective value is unchanged, do nothing (avoids redundant fetches) - if (currentValue === nextValue) return; + const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`; - // Only reset page to 1 if page parameter already exists - if (params.has("page")) { - params.set("page", "1"); - } + const currentValue = params.get(filterKey); + const nextValue = Array.isArray(value) + ? value.length > 0 + ? value.join(",") + : null + : value === null + ? null + : value; - if (nextValue === null) { - params.delete(filterKey); - } else { - params.set(filterKey, nextValue); - } + // If effective value is unchanged, do nothing (avoids redundant fetches) + if (currentValue === nextValue) return; - startTransition(() => { - router.push(`${pathname}?${params.toString()}`, { scroll: false }); - }); - }, - [router, searchParams, pathname, startTransition], - ); - - const clearFilter = useCallback( - (key: string) => { - const params = new URLSearchParams(searchParams.toString()); - const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`; + // Only reset page to 1 if page parameter already exists + if (params.has("page")) { + params.set("page", "1"); + } + if (nextValue === null) { params.delete(filterKey); + } else { + params.set(filterKey, nextValue); + } - // Only reset page to 1 if page parameter already exists - if (params.has("page")) { - params.set("page", "1"); - } + navigate(params); + }; - startTransition(() => { - router.push(`${pathname}?${params.toString()}`, { scroll: false }); - }); - }, - [router, searchParams, pathname, startTransition], - ); + const clearFilter = (key: string) => { + const params = new URLSearchParams(searchParams.toString()); + const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`; - const clearAllFilters = useCallback(() => { + params.delete(filterKey); + + // Only reset page to 1 if page parameter already exists + if (params.has("page")) { + params.set("page", "1"); + } + + navigate(params); + }; + + const clearAllFilters = () => { const params = new URLSearchParams(searchParams.toString()); Array.from(params.keys()).forEach((key) => { if (key.startsWith("filter[") || key === "sort") { @@ -93,17 +94,33 @@ export const useUrlFilters = () => { params.delete("page"); - startTransition(() => { - router.push(`${pathname}?${params.toString()}`, { scroll: false }); - }); - }, [router, searchParams, pathname, startTransition]); + navigate(params); + }; - const hasFilters = useCallback(() => { + const hasFilters = () => { const params = new URLSearchParams(searchParams.toString()); return Array.from(params.keys()).some( (key) => key.startsWith("filter[") || key === "sort", ); - }, [searchParams]); + }; + + /** + * Low-level navigation function for complex filter updates that need + * to modify multiple params atomically (e.g., setting provider_type + * while clearing provider_id). The modifier receives a mutable + * URLSearchParams; page is auto-reset if already present. + */ + const navigateWithParams = (modifier: (params: URLSearchParams) => void) => { + const params = new URLSearchParams(searchParams.toString()); + modifier(params); + + // Only reset page to 1 if page parameter already exists + if (params.has("page")) { + params.set("page", "1"); + } + + navigate(params); + }; return { updateFilter, @@ -111,5 +128,6 @@ export const useUrlFilters = () => { clearAllFilters, hasFilters, isPending, + navigateWithParams, }; }; diff --git a/ui/lib/compliance/compliance-mapper.ts b/ui/lib/compliance/compliance-mapper.ts index 2446156e2b..0c739fa322 100644 --- a/ui/lib/compliance/compliance-mapper.ts +++ b/ui/lib/compliance/compliance-mapper.ts @@ -4,6 +4,7 @@ import { AWSWellArchitectedCustomDetails } from "@/components/compliance/complia import { C5CustomDetails } from "@/components/compliance/compliance-custom-details/c5-details"; import { CCCCustomDetails } from "@/components/compliance/compliance-custom-details/ccc-details"; import { CISCustomDetails } from "@/components/compliance/compliance-custom-details/cis-details"; +import { CSACustomDetails } from "@/components/compliance/compliance-custom-details/csa-details"; import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details"; import { GenericCustomDetails } from "@/components/compliance/compliance-custom-details/generic-details"; import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details"; @@ -37,6 +38,10 @@ import { toAccordionItems as toCISAccordionItems, } from "./cis"; import { calculateCategoryHeatmapData, getTopFailedSections } from "./commons"; +import { + mapComplianceData as mapCSAComplianceData, + toAccordionItems as toCSAAccordionItems, +} from "./csa"; import { mapComplianceData as mapENSComplianceData, toAccordionItems as toENSAccordionItems, @@ -179,6 +184,15 @@ const getComplianceMappers = (): Record => ({ getDetailsComponent: (requirement: Requirement) => createElement(CCCCustomDetails, { requirement }), }, + "CSA-CCM": { + mapComplianceData: mapCSAComplianceData, + toAccordionItems: toCSAAccordionItems, + getTopFailedSections, + calculateCategoryHeatmapData: (data: Framework[]) => + calculateCategoryHeatmapData(data), + getDetailsComponent: (requirement: Requirement) => + createElement(CSACustomDetails, { requirement }), + }, }); /** diff --git a/ui/lib/compliance/compliance-report-types.ts b/ui/lib/compliance/compliance-report-types.ts index c95cbf6362..c7d65ac726 100644 --- a/ui/lib/compliance/compliance-report-types.ts +++ b/ui/lib/compliance/compliance-report-types.ts @@ -13,6 +13,7 @@ export const COMPLIANCE_REPORT_TYPES = { THREATSCORE: "threatscore", ENS: "ens", NIS2: "nis2", + CSA_CCM: "csa", // Future report types can be added here: // CIS: "cis", // NIST: "nist", @@ -34,6 +35,7 @@ export const COMPLIANCE_REPORT_DISPLAY_NAMES: Record< [COMPLIANCE_REPORT_TYPES.THREATSCORE]: "ThreatScore", [COMPLIANCE_REPORT_TYPES.ENS]: "ENS RD2022", [COMPLIANCE_REPORT_TYPES.NIS2]: "NIS2", + [COMPLIANCE_REPORT_TYPES.CSA_CCM]: "CSA CCM", // Add display names for future report types here }; @@ -47,6 +49,7 @@ export const COMPLIANCE_REPORT_BUTTON_LABELS: Record< [COMPLIANCE_REPORT_TYPES.THREATSCORE]: "PDF ThreatScore Report", [COMPLIANCE_REPORT_TYPES.ENS]: "PDF ENS Report", [COMPLIANCE_REPORT_TYPES.NIS2]: "PDF NIS2 Report", + [COMPLIANCE_REPORT_TYPES.CSA_CCM]: "PDF CSA CCM Report", // Add button labels for future report types here }; @@ -58,6 +61,7 @@ const FRAMEWORK_TO_REPORT_TYPE: Record = { ProwlerThreatScore: COMPLIANCE_REPORT_TYPES.THREATSCORE, ENS: COMPLIANCE_REPORT_TYPES.ENS, NIS2: COMPLIANCE_REPORT_TYPES.NIS2, + "CSA-CCM": COMPLIANCE_REPORT_TYPES.CSA_CCM, // Add new framework mappings here as PDF support is added: // "CIS-1.5": COMPLIANCE_REPORT_TYPES.CIS, // "NIST-800-53": COMPLIANCE_REPORT_TYPES.NIST, diff --git a/ui/lib/compliance/csa.tsx b/ui/lib/compliance/csa.tsx new file mode 100644 index 0000000000..93974d6b47 --- /dev/null +++ b/ui/lib/compliance/csa.tsx @@ -0,0 +1,143 @@ +import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content"; +import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title"; +import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title"; +import { AccordionItemProps } from "@/components/ui/accordion/Accordion"; +import { FindingStatus } from "@/components/ui/table/status-finding-badge"; +import { + AttributesData, + CSAAttributesMetadata, + Framework, + Requirement, + REQUIREMENT_STATUS, + RequirementsData, + RequirementStatus, +} from "@/types/compliance"; + +import { + calculateFrameworkCounters, + createRequirementsMap, + findOrCreateCategory, + findOrCreateControl, + findOrCreateFramework, +} from "./commons"; + +export interface CSAMappingSection { + title: string; + key: keyof Requirement; + colorClasses: string; +} + +export const CSA_MAPPING_SECTIONS: CSAMappingSection[] = [ + { + title: "Scope Applicability", + key: "scope_applicability", + colorClasses: + "bg-blue-50 text-blue-700 ring-blue-600/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/20", + }, +]; + +const getStatusCounters = (status: RequirementStatus) => ({ + pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0, + fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0, + manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0, +}); + +export const mapComplianceData = ( + attributesData: AttributesData, + requirementsData: RequirementsData, +): Framework[] => { + const attributes = attributesData?.data || []; + const requirementsMap = createRequirementsMap(requirementsData); + const frameworks: Framework[] = []; + + for (const attributeItem of attributes) { + const id = attributeItem.id; + const metadataArray = attributeItem.attributes?.attributes + ?.metadata as unknown as CSAAttributesMetadata[]; + const attrs = metadataArray?.[0]; + if (!attrs) continue; + + const requirementData = requirementsMap.get(id); + if (!requirementData) continue; + + const frameworkName = attributeItem.attributes.framework; + const categoryName = attrs.Section; + const requirementName = attributeItem.attributes.name || ""; + const description = attributeItem.attributes.description; + const status = requirementData.attributes.status || ""; + const checks = attributeItem.attributes.attributes.check_ids || []; + + const framework = findOrCreateFramework(frameworks, frameworkName); + const category = findOrCreateCategory(framework.categories, categoryName); + // Use a single control per category to keep a flat 2-level structure + const control = findOrCreateControl(category.controls, categoryName); + + const finalStatus: RequirementStatus = status as RequirementStatus; + const requirement: Requirement = { + name: requirementName ? `${id} - ${requirementName}` : id, + description, + status: finalStatus, + check_ids: checks, + ...getStatusCounters(finalStatus), + ccm_lite: attrs.CCMLite, + iaas: attrs.IaaS, + paas: attrs.PaaS, + saas: attrs.SaaS, + scope_applicability: attrs.ScopeApplicability, + }; + + control.requirements.push(requirement); + } + + calculateFrameworkCounters(frameworks); + + return frameworks; +}; + +export const toAccordionItems = ( + data: Framework[], + scanId: string | undefined, +): AccordionItemProps[] => { + const safeId = scanId || ""; + + return data.flatMap((framework) => + framework.categories.map((category) => ({ + key: `${framework.name}-${category.name}`, + title: ( + + ), + content: "", + // Flatten: requirements are direct children of the section + items: category.controls.flatMap((control) => + control.requirements.map((requirement, reqIndex) => ({ + key: `${framework.name}-${category.name}-req-${reqIndex}`, + title: ( + + ), + content: ( + + ), + items: [], + })), + ), + })), + ); +}; diff --git a/ui/lib/provider-filters.ts b/ui/lib/provider-filters.ts new file mode 100644 index 0000000000..62edc5bb64 --- /dev/null +++ b/ui/lib/provider-filters.ts @@ -0,0 +1,144 @@ +import { PROVIDER_TYPES, type ProviderType } from "@/types/providers"; + +export type ProviderFilterValue = string | string[] | undefined; +export type ProviderFilters = Record; + +interface AppendProviderFiltersOptions { + ensuredInFilterKey?: string; + excludedKeys?: string[]; + excludedKeyIncludes?: string[]; +} + +type AppendSanitizedProviderTypeFiltersOptions = Omit< + AppendProviderFiltersOptions, + "ensuredInFilterKey" +>; + +const SUPPORTED_PROVIDER_TYPES_CSV = PROVIDER_TYPES.join(","); +export const PROVIDER_IN_FILTER_KEY = "filter[provider__in]"; +export const PROVIDER_TYPE_IN_FILTER_KEY = "filter[provider_type__in]"; + +const PROVIDER_TYPE_IN_KEYS = new Set([ + PROVIDER_TYPE_IN_FILTER_KEY, + "provider_type__in", +]); + +const PROVIDER_SINGLE_KEYS = new Set([ + "filter[provider_type]", + "provider_type", +]); + +const toCsvString = (value: unknown): string => { + if (Array.isArray(value)) return value.join(","); + if (typeof value === "string") return value; + return ""; +}; + +const isSupportedProviderType = (value: string): value is ProviderType => + (PROVIDER_TYPES as readonly string[]).includes(value); + +export const sanitizeProviderTypesCsv = (value?: unknown): string => { + const rawValue = toCsvString(value); + if (!rawValue.trim()) return SUPPORTED_PROVIDER_TYPES_CSV; + + const supportedProviderTypes = Array.from( + new Set( + rawValue + .split(",") + .map((providerType) => providerType.trim()) + .filter(isSupportedProviderType), + ), + ); + + return supportedProviderTypes.length > 0 + ? supportedProviderTypes.join(",") + : SUPPORTED_PROVIDER_TYPES_CSV; +}; + +export const sanitizeProviderType = ( + value?: unknown, +): ProviderType | undefined => { + const rawValue = toCsvString(value); + if (!rawValue.trim()) return undefined; + + return rawValue + .split(",") + .map((providerType) => providerType.trim()) + .find(isSupportedProviderType); +}; + +export const sanitizeProviderFilters = ( + filters: ProviderFilters = {}, + ensuredInFilterKey?: string, +): ProviderFilters => { + const sanitizedFilters: ProviderFilters = { ...filters }; + + Object.keys(sanitizedFilters).forEach((key) => { + if (PROVIDER_TYPE_IN_KEYS.has(key)) { + sanitizedFilters[key] = sanitizeProviderTypesCsv(sanitizedFilters[key]); + return; + } + + if (PROVIDER_SINGLE_KEYS.has(key)) { + const providerType = sanitizeProviderType(sanitizedFilters[key]); + if (providerType) { + sanitizedFilters[key] = providerType; + } else { + delete sanitizedFilters[key]; + } + } + }); + + if (ensuredInFilterKey) { + sanitizedFilters[ensuredInFilterKey] = sanitizeProviderTypesCsv( + sanitizedFilters[ensuredInFilterKey], + ); + } + + return sanitizedFilters; +}; + +export const appendSanitizedProviderFilters = ( + url: URL, + filters: ProviderFilters = {}, + { + ensuredInFilterKey, + excludedKeys = ["filter[search]"], + excludedKeyIncludes = [], + }: AppendProviderFiltersOptions = {}, +): void => { + const sanitizedFilters = sanitizeProviderFilters(filters, ensuredInFilterKey); + const excludedKeysSet = new Set(excludedKeys); + + Object.entries(sanitizedFilters).forEach(([key, value]) => { + if ( + value === undefined || + excludedKeysSet.has(key) || + excludedKeyIncludes.some((excludedKey) => key.includes(excludedKey)) + ) { + return; + } + + url.searchParams.append(key, String(value)); + }); +}; + +export const appendSanitizedProviderTypeFilters = ( + url: URL, + filters: ProviderFilters = {}, + options: AppendSanitizedProviderTypeFiltersOptions = {}, +): void => + appendSanitizedProviderFilters(url, filters, { + ...options, + ensuredInFilterKey: PROVIDER_TYPE_IN_FILTER_KEY, + }); + +export const appendSanitizedProviderInFilters = ( + url: URL, + filters: ProviderFilters = {}, + options: AppendSanitizedProviderTypeFiltersOptions = {}, +): void => + appendSanitizedProviderFilters(url, filters, { + ...options, + ensuredInFilterKey: PROVIDER_IN_FILTER_KEY, + }); diff --git a/ui/package.json b/ui/package.json index 46ee0bead2..c13ac23f3f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,6 +15,9 @@ "format:check": "./node_modules/.bin/prettier --check ./app", "format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app", "prepare": "husky", + "test": "vitest", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage", "test:e2e": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans", "test:e2e:ui": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans --ui", "test:e2e:debug": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans --debug", @@ -25,14 +28,14 @@ }, "dependencies": { "@ai-sdk/react": "2.0.111", - "@aws-sdk/client-bedrock-runtime": "3.948.0", + "@aws-sdk/client-bedrock-runtime": "3.988.0", "@extractus/feed-extractor": "7.1.7", "@heroui/react": "2.8.4", "@hookform/resolvers": "5.2.2", "@internationalized/date": "3.10.0", "@langchain/aws": "1.1.0", "@langchain/core": "1.1.15", - "@langchain/mcp-adapters": "1.0.3", + "@langchain/mcp-adapters": "1.1.3", "@langchain/openai": "1.1.3", "@next/third-parties": "16.1.6", "@radix-ui/react-alert-dialog": "1.1.14", @@ -59,7 +62,7 @@ "@react-types/datepicker": "3.13.2", "@react-types/shared": "3.26.0", "@sentry/nextjs": "10.27.0", - "@tailwindcss/postcss": "4.1.13", + "@tailwindcss/postcss": "4.1.18", "@tailwindcss/typography": "0.5.16", "@tanstack/react-table": "8.21.3", "@types/dagre": "0.7.53", @@ -113,6 +116,9 @@ "@iconify/react": "5.2.1", "@next/eslint-plugin-next": "16.1.6", "@playwright/test": "1.56.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "16.3.2", + "@testing-library/user-event": "14.6.1", "@types/d3": "7.4.3", "@types/geojson": "7946.0.16", "@types/node": "24.10.8", @@ -123,6 +129,8 @@ "@types/uuid": "10.0.0", "@typescript-eslint/eslint-plugin": "8.53.0", "@typescript-eslint/parser": "8.53.0", + "@vitejs/plugin-react": "5.1.2", + "@vitest/coverage-v8": "4.0.18", "autoprefixer": "10.4.19", "babel-plugin-react-compiler": "1.0.0", "dotenv-expand": "12.0.3", @@ -140,14 +148,16 @@ "eslint-plugin-unused-imports": "4.3.0", "globals": "17.0.0", "husky": "9.1.7", + "jsdom": "27.4.0", "lint-staged": "15.5.2", "postcss": "8.4.38", "prettier": "3.6.2", "prettier-plugin-tailwindcss": "0.6.14", - "shadcn": "3.4.1", + "shadcn": "3.8.4", "tailwind-variants": "0.1.20", - "tailwindcss": "4.1.13", - "typescript": "5.5.4" + "tailwindcss": "4.1.18", + "typescript": "5.5.4", + "vitest": "4.0.18" }, "pnpm": { "overrides": { @@ -158,8 +168,14 @@ "@react-aria/ssr>react": "19.2.4", "@react-aria/ssr>react-dom": "19.2.4", "@react-aria/visually-hidden>react": "19.2.4", - "@react-aria/interactions>react": "19.2.4" + "@react-aria/interactions>react": "19.2.4", + "lodash": "4.17.23", + "lodash-es": "4.17.23", + "hono": "4.11.7", + "@isaacs/brace-expansion": "5.0.1", + "fast-xml-parser": "5.3.4" } }, - "version": "0.0.1" + "version": "0.0.1", + "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a" } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 86bac336d4..f3e0175004 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -13,6 +13,11 @@ overrides: '@react-aria/ssr>react-dom': 19.2.4 '@react-aria/visually-hidden>react': 19.2.4 '@react-aria/interactions>react': 19.2.4 + lodash: 4.17.23 + lodash-es: 4.17.23 + hono: 4.11.7 + '@isaacs/brace-expansion': 5.0.1 + fast-xml-parser: 5.3.4 importers: @@ -22,14 +27,14 @@ importers: specifier: 2.0.111 version: 2.0.111(react@19.2.4)(zod@4.1.11) '@aws-sdk/client-bedrock-runtime': - specifier: 3.948.0 - version: 3.948.0 + specifier: 3.988.0 + version: 3.988.0 '@extractus/feed-extractor': specifier: 7.1.7 version: 7.1.7 '@heroui/react': specifier: 2.8.4 - version: 2.8.4(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.13) + version: 2.8.4(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.18) '@hookform/resolvers': specifier: 5.2.2 version: 5.2.2(react-hook-form@7.62.0(react@19.2.4)) @@ -38,16 +43,16 @@ importers: version: 3.10.0 '@langchain/aws': specifier: 1.1.0 - version: 1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11))) + version: 1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))) '@langchain/core': specifier: 1.1.15 - version: 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)) + version: 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)) '@langchain/mcp-adapters': - specifier: 1.0.3 - version: 1.0.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))(@langchain/langgraph@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11))(hono@4.11.4) + specifier: 1.1.3 + version: 1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(@langchain/langgraph@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11)) '@langchain/openai': specifier: 1.1.3 - version: 1.1.3(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11))) + version: 1.1.3(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(ws@8.19.0) '@next/third-parties': specifier: 16.1.6 version: 16.1.6(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) @@ -124,11 +129,11 @@ importers: specifier: 10.27.0 version: 10.27.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.104.1) '@tailwindcss/postcss': - specifier: 4.1.13 - version: 4.1.13 + specifier: 4.1.18 + version: 4.1.18 '@tailwindcss/typography': specifier: 0.5.16 - version: 0.5.16(tailwindcss@4.1.13) + version: 0.5.16(tailwindcss@4.1.18) '@tanstack/react-table': specifier: 8.21.3 version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -182,7 +187,7 @@ importers: version: 4.0.0 langchain: specifier: 1.2.10 - version: 1.2.10(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11)) + version: 1.2.10(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11)) lucide-react: specifier: 0.543.0 version: 0.543.0(react@19.2.4) @@ -242,7 +247,7 @@ importers: version: 3.3.1 tailwindcss-animate: specifier: 1.0.7 - version: 1.0.7(tailwindcss@4.1.13) + version: 1.0.7(tailwindcss@4.1.18) topojson-client: specifier: 3.1.0 version: 3.1.0 @@ -280,6 +285,15 @@ importers: '@playwright/test': specifier: 1.56.1 version: 1.56.1 + '@testing-library/jest-dom': + specifier: 6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: 16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@testing-library/user-event': + specifier: 14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) '@types/d3': specifier: 7.4.3 version: 7.4.3 @@ -310,6 +324,12 @@ importers: '@typescript-eslint/parser': specifier: 8.53.0 version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4) + '@vitejs/plugin-react': + specifier: 5.1.2 + version: 5.1.2(vite@7.3.1(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2)) + '@vitest/coverage-v8': + specifier: 4.0.18 + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.8)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.7(@types/node@24.10.8)(typescript@5.5.4))(terser@5.46.0)(yaml@2.8.2)) autoprefixer: specifier: 10.4.19 version: 10.4.19(postcss@8.4.38) @@ -361,6 +381,9 @@ importers: husky: specifier: 9.1.7 version: 9.1.7 + jsdom: + specifier: 27.4.0 + version: 27.4.0(@noble/hashes@1.8.0) lint-staged: specifier: 15.5.2 version: 15.5.2 @@ -374,20 +397,29 @@ importers: specifier: 0.6.14 version: 0.6.14(prettier@3.6.2) shadcn: - specifier: 3.4.1 - version: 3.4.1(@cfworker/json-schema@4.1.1)(@types/node@24.10.8)(hono@4.11.4)(typescript@5.5.4) + specifier: 3.8.4 + version: 3.8.4(@cfworker/json-schema@4.1.1)(@types/node@24.10.8)(typescript@5.5.4) tailwind-variants: specifier: 0.1.20 - version: 0.1.20(tailwindcss@4.1.13) + version: 0.1.20(tailwindcss@4.1.18) tailwindcss: - specifier: 4.1.13 - version: 4.1.13 + specifier: 4.1.18 + version: 4.1.18 typescript: specifier: 5.5.4 version: 5.5.4 + vitest: + specifier: 4.0.18 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.8)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.7(@types/node@24.10.8)(typescript@5.5.4))(terser@5.46.0)(yaml@2.8.2) packages: + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@ai-sdk/gateway@2.0.18': resolution: {integrity: sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==} engines: {node: '>=18'} @@ -431,6 +463,15 @@ packages: '@apm-js-collab/tracing-hooks@0.3.1': resolution: {integrity: sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw==} + '@asamuzakjp/css-color@4.1.2': + resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@auth/core@0.41.0': resolution: {integrity: sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==} peerDependencies: @@ -466,200 +507,191 @@ packages: resolution: {integrity: sha512-QDD0sl0Wfm1FI/JK6Yexg/j1l51LtPL/8aaqMPGpa4yZpIU3K9wJd5LKPOoO0dMB9M0PrVq+JbsUpJ+mS8bgAA==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-bedrock-runtime@3.948.0': - resolution: {integrity: sha512-JRlqANr0wY63ZXZPKaWIoH0zYXsllROynPVj8XdOFwiO/pRr/2hol8popfMhD7T5Zb6yZQ/FM8Tu5Mc61l2HHQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/client-bedrock-runtime@3.988.0': + resolution: {integrity: sha512-NZlsQ8rjmAG0zRteqEiRakV97/nToIwDqT0zbye+j+HN60wiRSESAFCEozdwiiuVr0xl69NcoTiMg64xbh2I9g==} + engines: {node: '>=20.0.0'} '@aws-sdk/client-kendra@3.971.0': resolution: {integrity: sha512-iDkT0oMDfgjPctzR+g61Ends/QX/SeyFJL0m9tZqMAgDWsoht+N4INgtuKCmmIXggvh39BwWHM1/tQEbPIcq9A==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-sso@3.948.0': - resolution: {integrity: sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==} - engines: {node: '>=18.0.0'} - '@aws-sdk/client-sso@3.971.0': resolution: {integrity: sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.947.0': - resolution: {integrity: sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/client-sso@3.988.0': + resolution: {integrity: sha512-ThqQ7aF1k0Zz4yJRwegHw+T1rM3a7ZPvvEUSEdvn5Z8zTeWgJAbtqW/6ejPsMLmFOlHgNcwDQN/e69OvtEOoIQ==} + engines: {node: '>=20.0.0'} '@aws-sdk/core@3.970.0': resolution: {integrity: sha512-klpzObldOq8HXzDjDlY6K8rMhYZU6mXRz6P9F9N+tWnjoYFfeBMra8wYApydElTUYQKP1O7RLHwH1OKFfKcqIA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.947.0': - resolution: {integrity: sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/core@3.973.8': + resolution: {integrity: sha512-WeYJ2sfvRLbbUIrjGMUXcEHGu5SJk53jz3K9F8vFP42zWyROzPJ2NB6lMu9vWl5hnMwzwabX7pJc9Euh3JyMGw==} + engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-env@3.970.0': resolution: {integrity: sha512-rtVzXzEtAfZBfh+lq3DAvRar4c3jyptweOAJR2DweyXx71QSMY+O879hjpMwES7jl07a3O1zlnFIDo4KP/96kQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.947.0': - resolution: {integrity: sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-env@3.972.6': + resolution: {integrity: sha512-+dYEBWgTqkQQHFUllvBL8SLyXyLKWdxLMD1LmKJRvmb0NMJuaJFG/qg78C+LE67eeGbipYcE+gJ48VlLBGHlMw==} + engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-http@3.970.0': resolution: {integrity: sha512-CjDbWL7JxjLc9ZxQilMusWSw05yRvUJKRpz59IxDpWUnSMHC9JMMUUkOy5Izk8UAtzi6gupRWArp4NG4labt9Q==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.948.0': - resolution: {integrity: sha512-Cl//Qh88e8HBL7yYkJNpF5eq76IO6rq8GsatKcfVBm7RFVxCqYEPSSBtkHdbtNwQdRQqAMXc6E/lEB/CZUDxnA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-http@3.972.8': + resolution: {integrity: sha512-z3QkozMV8kOFisN2pgRag/f0zPDrw96mY+ejAM0xssV/+YQ2kklbylRNI/TcTQUDnGg0yPxNjyV6F2EM2zPTwg==} + engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-ini@3.971.0': resolution: {integrity: sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.948.0': - resolution: {integrity: sha512-gcKO2b6eeTuZGp3Vvgr/9OxajMrD3W+FZ2FCyJox363ZgMoYJsyNid1vuZrEuAGkx0jvveLXfwiVS0UXyPkgtw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-ini@3.972.6': + resolution: {integrity: sha512-6tkIYFv3sZH1XsjQq+veOmx8XWRnyqTZ5zx/sMtdu/xFRIzrJM1Y2wAXeCJL1rhYSB7uJSZ1PgALI2WVTj78ow==} + engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-login@3.971.0': resolution: {integrity: sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.948.0': - resolution: {integrity: sha512-ep5vRLnrRdcsP17Ef31sNN4g8Nqk/4JBydcUJuFRbGuyQtrZZrVT81UeH2xhz6d0BK6ejafDB9+ZpBjXuWT5/Q==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-login@3.972.6': + resolution: {integrity: sha512-LXsoBoaTSGHdRCQXlWSA0CHHh05KWncb592h9ElklnPus++8kYn1Ic6acBR4LKFQ0RjjMVgwe5ypUpmTSUOjPA==} + engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-node@3.971.0': resolution: {integrity: sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.947.0': - resolution: {integrity: sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-node@3.972.7': + resolution: {integrity: sha512-PuJ1IkISG7ZDpBFYpGotaay6dYtmriBYuHJ/Oko4VHxh8YN5vfoWnMNYFEWuzOfyLmP7o9kDVW0BlYIpb3skvw==} + engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-process@3.970.0': resolution: {integrity: sha512-0XeT8OaT9iMA62DFV9+m6mZfJhrD0WNKf4IvsIpj2Z7XbaYfz3CoDDvNoALf3rPY9NzyMHgDxOspmqdvXP00mw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.948.0': - resolution: {integrity: sha512-gqLhX1L+zb/ZDnnYbILQqJ46j735StfWV5PbDjxRzBKS7GzsiYoaf6MyHseEopmWrez5zl5l6aWzig7UpzSeQQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-process@3.972.6': + resolution: {integrity: sha512-Yf34cjIZJHVnD92jnVYy3tNjM+Q4WJtffLK2Ehn0nKpZfqd1m7SI0ra22Lym4C53ED76oZENVSS2wimoXJtChQ==} + engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-sso@3.971.0': resolution: {integrity: sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.948.0': - resolution: {integrity: sha512-MvYQlXVoJyfF3/SmnNzOVEtANRAiJIObEUYYyjTqKZTmcRIVVky0tPuG26XnB8LmTYgtESwJIZJj/Eyyc9WURQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-sso@3.972.6': + resolution: {integrity: sha512-2+5UVwUYdD4BBOkLpKJ11MQ8wQeyJGDVMDRH5eWOULAh9d6HJq07R69M/mNNMC9NTjr3mB1T0KGDn4qyQh5jzg==} + engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-web-identity@3.971.0': resolution: {integrity: sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w==} engines: {node: '>=20.0.0'} - '@aws-sdk/eventstream-handler-node@3.936.0': - resolution: {integrity: sha512-4zIbhdRmol2KosIHmU31ATvNP0tkJhDlRj9GuawVJoEnMvJA1pd2U3SRdiOImJU3j8pT46VeS4YMmYxfjGHByg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.6': + resolution: {integrity: sha512-pdJzwKtlDxBnvZ04pWMqttijmkUIlwOsS0GcxCjzEVyUMpARysl0S0ks74+gs2Pdev3Ujz+BTAjOc1tQgAxGqA==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-eventstream@3.936.0': - resolution: {integrity: sha512-XQSH8gzLkk8CDUDxyt4Rdm9owTpRIPdtg2yw9Y2Wl5iSI55YQSiC3x8nM3c4Y4WqReJprunFPK225ZUDoYCfZA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/eventstream-handler-node@3.972.5': + resolution: {integrity: sha512-xEmd3dnyn83K6t4AJxBJA63wpEoCD45ERFG0XMTViD2E/Ohls9TLxjOWPb1PAxR9/46cKy/TImez1GoqP6xVNQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-host-header@3.936.0': - resolution: {integrity: sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-eventstream@3.972.3': + resolution: {integrity: sha512-pbvZ6Ye/Ks6BAZPa3RhsNjHrvxU9li25PMhSdDpbX0jzdpKpAkIR65gXSNKmA/REnSdEMWSD4vKUW+5eMFzB6w==} + engines: {node: '>=20.0.0'} '@aws-sdk/middleware-host-header@3.969.0': resolution: {integrity: sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-logger@3.936.0': - resolution: {integrity: sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-host-header@3.972.3': + resolution: {integrity: sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==} + engines: {node: '>=20.0.0'} '@aws-sdk/middleware-logger@3.969.0': resolution: {integrity: sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-recursion-detection@3.948.0': - resolution: {integrity: sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-logger@3.972.3': + resolution: {integrity: sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==} + engines: {node: '>=20.0.0'} '@aws-sdk/middleware-recursion-detection@3.969.0': resolution: {integrity: sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.947.0': - resolution: {integrity: sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-recursion-detection@3.972.3': + resolution: {integrity: sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==} + engines: {node: '>=20.0.0'} '@aws-sdk/middleware-user-agent@3.970.0': resolution: {integrity: sha512-dnSJGGUGSFGEX2NzvjwSefH+hmZQ347AwbLhAsi0cdnISSge+pcGfOFrJt2XfBIypwFe27chQhlfuf/gWdzpZg==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-websocket@3.936.0': - resolution: {integrity: sha512-bPe3rqeugyj/MmjP0yBSZox2v1Wa8Dv39KN+RxVbQroLO8VUitBo6xyZ0oZebhZ5sASwSg58aDcMlX0uFLQnTA==} - engines: {node: '>= 14.0.0'} + '@aws-sdk/middleware-user-agent@3.972.8': + resolution: {integrity: sha512-3PGL+Kvh1PhB0EeJeqNqOWQgipdqFheO4OUKc6aYiFwEpM5t9AyE5hjjxZ5X6iSj8JiduWFZLPwASzF6wQRgFg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.948.0': - resolution: {integrity: sha512-zcbJfBsB6h254o3NuoEkf0+UY1GpE9ioiQdENWv7odo69s8iaGBEQ4BDpsIMqcuiiUXw1uKIVNxCB1gUGYz8lw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-websocket@3.972.6': + resolution: {integrity: sha512-1DedO6N3m8zQ/vG6twNiHtsdwBgk773VdavLEbB3NXeKZDlzSK1BTviqWwvJdKx5UnIy4kGGP6WWpCEFEt/bhQ==} + engines: {node: '>= 14.0.0'} '@aws-sdk/nested-clients@3.971.0': resolution: {integrity: sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g==} engines: {node: '>=20.0.0'} - '@aws-sdk/region-config-resolver@3.936.0': - resolution: {integrity: sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/nested-clients@3.988.0': + resolution: {integrity: sha512-OgYV9k1oBCQ6dOM+wWAMNNehXA8L4iwr7ydFV+JDHyuuu0Ko7tDXnLEtEmeQGYRcAFU3MGasmlBkMB8vf4POrg==} + engines: {node: '>=20.0.0'} '@aws-sdk/region-config-resolver@3.969.0': resolution: {integrity: sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.948.0': - resolution: {integrity: sha512-V487/kM4Teq5dcr1t5K6eoUKuqlGr9FRWL3MIMukMERJXHZvio6kox60FZ/YtciRHRI75u14YUqm2Dzddcu3+A==} - engines: {node: '>=18.0.0'} + '@aws-sdk/region-config-resolver@3.972.3': + resolution: {integrity: sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==} + engines: {node: '>=20.0.0'} '@aws-sdk/token-providers@3.971.0': resolution: {integrity: sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.936.0': - resolution: {integrity: sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/token-providers@3.988.0': + resolution: {integrity: sha512-xvXVlRVKHnF2h6fgWBm64aPP5J+58aJyGfRrQa/uFh8a9mcK68mLfJOYq+ZSxQy/UN3McafJ2ILAy7IWzT9kRw==} + engines: {node: '>=20.0.0'} '@aws-sdk/types@3.969.0': resolution: {integrity: sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-endpoints@3.936.0': - resolution: {integrity: sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==} - engines: {node: '>=18.0.0'} + '@aws-sdk/types@3.973.1': + resolution: {integrity: sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==} + engines: {node: '>=20.0.0'} '@aws-sdk/util-endpoints@3.970.0': resolution: {integrity: sha512-TZNZqFcMUtjvhZoZRtpEGQAdULYiy6rcGiXAbLU7e9LSpIYlRqpLa207oMNfgbzlL2PnHko+eVg8rajDiSOYCg==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-format-url@3.936.0': - resolution: {integrity: sha512-MS5eSEtDUFIAMHrJaMERiHAvDPdfxc/T869ZjDNFAIiZhyc037REw0aoTNeimNXDNy2txRNZJaAUn/kE4RwN+g==} - engines: {node: '>=18.0.0'} + '@aws-sdk/util-endpoints@3.988.0': + resolution: {integrity: sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-format-url@3.972.3': + resolution: {integrity: sha512-n7F2ycckcKFXa01vAsT/SJdjFHfKH9s96QHcs5gn8AaaigASICeME8WdUL9uBp8XV/OVwEt8+6gzn6KFUgQa8g==} + engines: {node: '>=20.0.0'} '@aws-sdk/util-locate-window@3.965.2': resolution: {integrity: sha512-qKgO7wAYsXzhwCHhdbaKFyxd83Fgs8/1Ka+jjSPrv2Ll7mB55Wbwlo0kkfMLh993/yEc8aoDIAc1Fz9h4Spi4Q==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-user-agent-browser@3.936.0': - resolution: {integrity: sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==} - '@aws-sdk/util-user-agent-browser@3.969.0': resolution: {integrity: sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g==} - '@aws-sdk/util-user-agent-node@3.947.0': - resolution: {integrity: sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - aws-crt: '>=1.0.0' - peerDependenciesMeta: - aws-crt: - optional: true + '@aws-sdk/util-user-agent-browser@3.972.3': + resolution: {integrity: sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==} '@aws-sdk/util-user-agent-node@3.971.0': resolution: {integrity: sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ==} @@ -670,14 +702,23 @@ packages: aws-crt: optional: true - '@aws-sdk/xml-builder@3.930.0': - resolution: {integrity: sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/util-user-agent-node@3.972.6': + resolution: {integrity: sha512-966xH8TPqkqOXP7EwnEThcKKz0SNP9kVJBKd9M8bNXE4GSqVouMKKnFBwYnzbWVKuLXubzX5seokcX4a0JLJIA==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true '@aws-sdk/xml-builder@3.969.0': resolution: {integrity: sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/xml-builder@3.972.4': + resolution: {integrity: sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==} + engines: {node: '>=20.0.0'} + '@aws/lambda-invoke-store@0.2.3': resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} engines: {node: '>=18.0.0'} @@ -769,6 +810,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-syntax-jsx@7.28.6': resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} engines: {node: '>=6.9.0'} @@ -787,6 +833,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.28.6': resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} engines: {node: '>=6.9.0'} @@ -815,6 +873,14 @@ packages: resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@braintree/sanitize-url@7.1.1': resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} @@ -836,6 +902,37 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@csstools/color-helpers@6.0.1': + resolution: {integrity: sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.1.1': + resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.0.1': + resolution: {integrity: sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.0.27': + resolution: {integrity: sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==} + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} @@ -858,6 +955,162 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -896,6 +1149,15 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@exodus/bytes@1.14.1': + resolution: {integrity: sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@extractus/feed-extractor@7.1.7': resolution: {integrity: sha512-eNeddvKK9rBxWSHj5zBo6ODihJqJtq+QzEQdVeadkOK48avmdela+c2JAfMcPEBMFaWcAYV4bUMhI9Tqi8mX2Q==} engines: {node: '>= 20'} @@ -1487,7 +1749,7 @@ packages: resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} engines: {node: '>=18.14.1'} peerDependencies: - hono: ^4 + hono: 4.11.7 '@hookform/resolvers@5.2.2': resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} @@ -1814,18 +2076,14 @@ packages: resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + '@isaacs/brace-expansion@5.0.1': + resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} engines: {node: 20 || >=22} '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1886,8 +2144,8 @@ packages: zod-to-json-schema: optional: true - '@langchain/mcp-adapters@1.0.3': - resolution: {integrity: sha512-tQacqDcqikrli/gH7CGAOUfNJ0pFOwtoKPWq2mRP7BTApOPWyfZ8Okw5nSwPDZ7Eau/QyLAH7DaWfNHmwzXPGw==} + '@langchain/mcp-adapters@1.1.3': + resolution: {integrity: sha512-OPHIQNkTUJjnRj1pr+cp2nguMBZeF3Q1pVT1hCbgU7BrHgV7lov99wbU8po8Cm4zZzmeRtVO/T9X1SrDD1ogtQ==} engines: {node: '>=20.10.0'} peerDependencies: '@langchain/core': ^1.0.0 @@ -1902,8 +2160,8 @@ packages: '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} - '@modelcontextprotocol/sdk@1.25.2': - resolution: {integrity: sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==} + '@modelcontextprotocol/sdk@1.26.0': + resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -3742,6 +4000,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + '@rollup/plugin-commonjs@28.0.1': resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==} engines: {node: '>=16.0.0 || 14 >= 14.17'} @@ -4060,6 +4321,10 @@ packages: resolution: {integrity: sha512-aO7jmh3CtrmPsIJxUwYIzI5WVlMK8BMCPQ4D4nTzqTqBhbzvxHNzBMGcEg13yg/z9R2Qsz49NUFl0F0lVbTVFw==} engines: {node: '>=18.0.0'} + '@smithy/core@3.23.0': + resolution: {integrity: sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg==} + engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.2.8': resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==} engines: {node: '>=18.0.0'} @@ -4108,6 +4373,10 @@ packages: resolution: {integrity: sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==} engines: {node: '>=18.0.0'} + '@smithy/middleware-endpoint@4.4.14': + resolution: {integrity: sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-endpoint@4.4.8': resolution: {integrity: sha512-TV44qwB/T0OMMzjIuI+JeS0ort3bvlPJ8XIH0MSlGADraXpZqmyND27ueuAL3E14optleADWqtd7dUgc2w+qhQ==} engines: {node: '>=18.0.0'} @@ -4116,6 +4385,10 @@ packages: resolution: {integrity: sha512-yiUY1UvnbUFfP5izoKLtfxDSTRv724YRRwyiC/5HYY6vdsVDcDOXKSXmkJl/Hovcxt5r+8tZEUAdrOaCJwrl9Q==} engines: {node: '>=18.0.0'} + '@smithy/middleware-retry@4.4.31': + resolution: {integrity: sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-serde@4.2.9': resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==} engines: {node: '>=18.0.0'} @@ -4128,6 +4401,10 @@ packages: resolution: {integrity: sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==} engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.4.10': + resolution: {integrity: sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA==} + engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.4.8': resolution: {integrity: sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==} engines: {node: '>=18.0.0'} @@ -4164,6 +4441,10 @@ packages: resolution: {integrity: sha512-Je0EvGXVJ0Vrrr2lsubq43JGRIluJ/hX17aN/W/A0WfE+JpoMdI8kwk2t9F0zTX9232sJDGcoH4zZre6m6f/sg==} engines: {node: '>=18.0.0'} + '@smithy/smithy-client@4.11.3': + resolution: {integrity: sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg==} + engines: {node: '>=18.0.0'} + '@smithy/types@4.12.0': resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==} engines: {node: '>=18.0.0'} @@ -4200,10 +4481,18 @@ packages: resolution: {integrity: sha512-mMg+r/qDfjfF/0psMbV4zd7F/i+rpyp7Hjh0Wry7eY15UnzTEId+xmQTGDU8IdZtDfbGQxuWNfgBZKBj+WuYbA==} engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-browser@4.3.30': + resolution: {integrity: sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng==} + engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-node@4.2.26': resolution: {integrity: sha512-EQqe/WkbCinah0h1lMWh9ICl0Ob4lyl20/10WTB35SC9vDQfD8zWsOT+x2FIOXKAoZQ8z/y0EFMoodbcqWJY/w==} engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-node@4.2.33': + resolution: {integrity: sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA==} + engines: {node: '>=18.0.0'} + '@smithy/util-endpoints@3.2.8': resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==} engines: {node: '>=18.0.0'} @@ -4224,6 +4513,10 @@ packages: resolution: {integrity: sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==} engines: {node: '>=18.0.0'} + '@smithy/util-stream@4.5.12': + resolution: {integrity: sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg==} + engines: {node: '>=18.0.0'} + '@smithy/util-uri-escape@4.2.0': resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} engines: {node: '>=18.0.0'} @@ -4252,65 +4545,65 @@ packages: '@swc/helpers@0.5.18': resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} - '@tailwindcss/node@4.1.13': - resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==} + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} - '@tailwindcss/oxide-android-arm64@4.1.13': - resolution: {integrity: sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==} + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.13': - resolution: {integrity: sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==} + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.13': - resolution: {integrity: sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==} + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.13': - resolution: {integrity: sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==} + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': - resolution: {integrity: sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': - resolution: {integrity: sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.13': - resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.13': - resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.13': - resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==} + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.13': - resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==} + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -4321,24 +4614,24 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': - resolution: {integrity: sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.13': - resolution: {integrity: sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.13': - resolution: {integrity: sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==} + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} engines: {node: '>= 10'} - '@tailwindcss/postcss@4.1.13': - resolution: {integrity: sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==} + '@tailwindcss/postcss@4.1.18': + resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} '@tailwindcss/typography@0.5.16': resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} @@ -4365,12 +4658,59 @@ packages: '@tanstack/virtual-core@3.11.3': resolution: {integrity: sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w==} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -4473,6 +4813,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -4553,6 +4896,9 @@ packages: '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/validate-npm-package-name@4.0.2': + resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + '@typescript-eslint/eslint-plugin@8.53.0': resolution: {integrity: sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4714,6 +5060,50 @@ packages: resolution: {integrity: sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==} engines: {node: '>= 20'} + '@vitejs/plugin-react@5.1.2': + resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/coverage-v8@4.0.18': + resolution: {integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==} + peerDependencies: + '@vitest/browser': 4.0.18 + vitest: 4.0.18 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -4876,6 +5266,9 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -4912,6 +5305,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -4919,6 +5316,9 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} + ast-v8-to-istanbul@0.3.11: + resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==} + async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -4958,6 +5358,9 @@ packages: resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==} hasBin: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -4987,6 +5390,10 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -5017,6 +5424,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -5049,10 +5460,6 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} @@ -5213,11 +5620,22 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -5387,6 +5805,10 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -5453,10 +5875,22 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -5490,6 +5924,12 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -5573,6 +6013,9 @@ packages: resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} @@ -5592,6 +6035,11 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -5796,6 +6244,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -5834,8 +6285,12 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} - express-rate-limit@7.5.1: - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -5877,8 +6332,8 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-xml-parser@5.2.5: - resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} + fast-xml-parser@5.3.4: + resolution: {integrity: sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==} hasBin: true fastq@1.20.1: @@ -6179,13 +6634,20 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hono@4.11.4: - resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} + hono@4.11.7: + resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} engines: {node: '>=16.9.0'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-entities@2.6.0: resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -6196,6 +6658,10 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -6248,6 +6714,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -6274,6 +6744,10 @@ packages: intl-messageformat@10.7.16: resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==} + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -6332,6 +6806,11 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -6363,6 +6842,15 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-interactive@2.0.0: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} @@ -6398,6 +6886,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -6464,6 +6955,10 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -6474,6 +6969,18 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -6498,6 +7005,9 @@ packages: js-tiktoken@1.0.21: resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -6505,6 +7015,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -6613,68 +7132,74 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lightningcss-darwin-arm64@1.30.1: - resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.30.1: - resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.30.1: - resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.30.1: - resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.30.1: - resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.30.1: - resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.30.1: - resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.30.1: - resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-win32-arm64-msvc@1.30.1: - resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.30.1: - resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.30.1: - resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} lilconfig@3.1.3: @@ -6701,11 +7226,8 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - - lodash-es@4.17.22: - resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==} + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} lodash.castarray@4.4.0: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} @@ -6716,8 +7238,8 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} @@ -6737,6 +7259,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -6750,6 +7276,10 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -6757,6 +7287,13 @@ packages: resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -6822,6 +7359,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -6988,6 +7528,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@10.1.1: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} @@ -7006,10 +7550,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@3.1.0: - resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} - engines: {node: '>= 18'} - mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -7192,6 +7732,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -7217,6 +7760,10 @@ packages: oniguruma-to-es@4.3.4: resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + openai@6.16.0: resolution: {integrity: sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg==} hasBin: true @@ -7300,6 +7847,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -7396,6 +7946,10 @@ packages: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -7427,6 +7981,10 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + preact-render-to-string@6.5.11: resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} peerDependencies: @@ -7509,6 +8067,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -7589,6 +8151,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -7598,6 +8163,10 @@ packages: '@types/react': '>=18' react: '>=18' + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -7668,6 +8237,10 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -7801,6 +8374,10 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -7828,6 +8405,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -7876,8 +8457,8 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shadcn@3.4.1: - resolution: {integrity: sha512-gKLLCGN0lZ9vjbndoGY2cZJKvDtUYGRyF0XoyejHBbCr9lwE6NGlrtRQbA50Wcd/pDdYFyOzT/1wl//HUjSSkw==} + shadcn@3.8.4: + resolution: {integrity: sha512-pSad/m1+PGzB0aLsRBV0EkyGg9al1nJqYUuucg6d8v8xZspPZ5/ehGNEp5M4b1KQYqdO5/gGPbkhVbgmXqG9Pw==} hasBin: true sharp@0.33.5: @@ -7915,6 +8496,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -7956,6 +8540,9 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stacktrace-parser@0.1.11: resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} engines: {node: '>=6'} @@ -7964,6 +8551,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -8050,6 +8640,10 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -8096,6 +8690,9 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.12: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -8131,17 +8728,13 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders' - tailwindcss@4.1.13: - resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==} + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - tar@7.5.3: - resolution: {integrity: sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==} - engines: {node: '>=18'} - terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} @@ -8173,6 +8766,9 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -8181,6 +8777,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + tldts-core@7.0.19: resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} @@ -8207,6 +8807,10 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -8423,6 +9027,10 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + validate-npm-package-name@7.0.2: + resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} + engines: {node: ^20.17.0 || >=22.9.0} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -8445,6 +9053,80 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} @@ -8465,6 +9147,10 @@ packages: vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + watchpack@2.5.1: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} @@ -8479,6 +9165,10 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-sources@3.3.3: resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} @@ -8496,6 +9186,18 @@ packages: webpack-cli: optional: true + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -8525,6 +9227,11 @@ packages: engines: {node: ^16.13.0 || >=18.0.0} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -8551,6 +9258,29 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -8562,10 +9292,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} @@ -8631,6 +9357,10 @@ packages: snapshots: + '@acemir/cssom@0.9.31': {} + + '@adobe/css-tools@4.4.4': {} + '@ai-sdk/gateway@2.0.18(zod@4.1.11)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -8683,6 +9413,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@asamuzakjp/css-color@4.1.2': + dependencies: + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.2.6 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.6 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@auth/core@0.41.0': dependencies: '@panva/hkdf': 1.2.1 @@ -8694,7 +9442,7 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.936.0 + '@aws-sdk/types': 3.973.1 tslib: 2.8.1 '@aws-crypto/sha256-browser@5.2.0': @@ -8702,7 +9450,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.936.0 + '@aws-sdk/types': 3.973.1 '@aws-sdk/util-locate-window': 3.965.2 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -8710,7 +9458,7 @@ snapshots: '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.936.0 + '@aws-sdk/types': 3.973.1 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -8719,7 +9467,7 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.936.0 + '@aws-sdk/types': 3.973.1 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -8770,27 +9518,27 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-bedrock-runtime@3.948.0': + '@aws-sdk/client-bedrock-runtime@3.988.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.947.0 - '@aws-sdk/credential-provider-node': 3.948.0 - '@aws-sdk/eventstream-handler-node': 3.936.0 - '@aws-sdk/middleware-eventstream': 3.936.0 - '@aws-sdk/middleware-host-header': 3.936.0 - '@aws-sdk/middleware-logger': 3.936.0 - '@aws-sdk/middleware-recursion-detection': 3.948.0 - '@aws-sdk/middleware-user-agent': 3.947.0 - '@aws-sdk/middleware-websocket': 3.936.0 - '@aws-sdk/region-config-resolver': 3.936.0 - '@aws-sdk/token-providers': 3.948.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-endpoints': 3.936.0 - '@aws-sdk/util-user-agent-browser': 3.936.0 - '@aws-sdk/util-user-agent-node': 3.947.0 + '@aws-sdk/core': 3.973.8 + '@aws-sdk/credential-provider-node': 3.972.7 + '@aws-sdk/eventstream-handler-node': 3.972.5 + '@aws-sdk/middleware-eventstream': 3.972.3 + '@aws-sdk/middleware-host-header': 3.972.3 + '@aws-sdk/middleware-logger': 3.972.3 + '@aws-sdk/middleware-recursion-detection': 3.972.3 + '@aws-sdk/middleware-user-agent': 3.972.8 + '@aws-sdk/middleware-websocket': 3.972.6 + '@aws-sdk/region-config-resolver': 3.972.3 + '@aws-sdk/token-providers': 3.988.0 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.988.0 + '@aws-sdk/util-user-agent-browser': 3.972.3 + '@aws-sdk/util-user-agent-node': 3.972.6 '@smithy/config-resolver': 4.4.6 - '@smithy/core': 3.20.7 + '@smithy/core': 3.23.0 '@smithy/eventstream-serde-browser': 4.2.8 '@smithy/eventstream-serde-config-resolver': 4.3.8 '@smithy/eventstream-serde-node': 4.2.8 @@ -8798,25 +9546,25 @@ snapshots: '@smithy/hash-node': 4.2.8 '@smithy/invalid-dependency': 4.2.8 '@smithy/middleware-content-length': 4.2.8 - '@smithy/middleware-endpoint': 4.4.8 - '@smithy/middleware-retry': 4.4.24 + '@smithy/middleware-endpoint': 4.4.14 + '@smithy/middleware-retry': 4.4.31 '@smithy/middleware-serde': 4.2.9 '@smithy/middleware-stack': 4.2.8 '@smithy/node-config-provider': 4.3.8 - '@smithy/node-http-handler': 4.4.8 + '@smithy/node-http-handler': 4.4.10 '@smithy/protocol-http': 5.3.8 - '@smithy/smithy-client': 4.10.9 + '@smithy/smithy-client': 4.11.3 '@smithy/types': 4.12.0 '@smithy/url-parser': 4.2.8 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.23 - '@smithy/util-defaults-mode-node': 4.2.26 + '@smithy/util-defaults-mode-browser': 4.3.30 + '@smithy/util-defaults-mode-node': 4.2.33 '@smithy/util-endpoints': 3.2.8 '@smithy/util-middleware': 4.2.8 '@smithy/util-retry': 4.2.8 - '@smithy/util-stream': 4.5.10 + '@smithy/util-stream': 4.5.12 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: @@ -8866,49 +9614,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso@3.948.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.947.0 - '@aws-sdk/middleware-host-header': 3.936.0 - '@aws-sdk/middleware-logger': 3.936.0 - '@aws-sdk/middleware-recursion-detection': 3.948.0 - '@aws-sdk/middleware-user-agent': 3.947.0 - '@aws-sdk/region-config-resolver': 3.936.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-endpoints': 3.936.0 - '@aws-sdk/util-user-agent-browser': 3.936.0 - '@aws-sdk/util-user-agent-node': 3.947.0 - '@smithy/config-resolver': 4.4.6 - '@smithy/core': 3.20.7 - '@smithy/fetch-http-handler': 5.3.9 - '@smithy/hash-node': 4.2.8 - '@smithy/invalid-dependency': 4.2.8 - '@smithy/middleware-content-length': 4.2.8 - '@smithy/middleware-endpoint': 4.4.8 - '@smithy/middleware-retry': 4.4.24 - '@smithy/middleware-serde': 4.2.9 - '@smithy/middleware-stack': 4.2.8 - '@smithy/node-config-provider': 4.3.8 - '@smithy/node-http-handler': 4.4.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/smithy-client': 4.10.9 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.23 - '@smithy/util-defaults-mode-node': 4.2.26 - '@smithy/util-endpoints': 3.2.8 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-retry': 4.2.8 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/client-sso@3.971.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -8952,21 +9657,48 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.947.0': + '@aws-sdk/client-sso@3.988.0': dependencies: - '@aws-sdk/types': 3.936.0 - '@aws-sdk/xml-builder': 3.930.0 - '@smithy/core': 3.20.7 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.8 + '@aws-sdk/middleware-host-header': 3.972.3 + '@aws-sdk/middleware-logger': 3.972.3 + '@aws-sdk/middleware-recursion-detection': 3.972.3 + '@aws-sdk/middleware-user-agent': 3.972.8 + '@aws-sdk/region-config-resolver': 3.972.3 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.988.0 + '@aws-sdk/util-user-agent-browser': 3.972.3 + '@aws-sdk/util-user-agent-node': 3.972.6 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.23.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.14 + '@smithy/middleware-retry': 4.4.31 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 '@smithy/node-config-provider': 4.3.8 - '@smithy/property-provider': 4.2.8 + '@smithy/node-http-handler': 4.4.10 '@smithy/protocol-http': 5.3.8 - '@smithy/signature-v4': 5.3.8 - '@smithy/smithy-client': 4.10.9 + '@smithy/smithy-client': 4.11.3 '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.30 + '@smithy/util-defaults-mode-node': 4.2.33 + '@smithy/util-endpoints': 3.2.8 '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt '@aws-sdk/core@3.970.0': dependencies: @@ -8984,12 +9716,20 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.947.0': + '@aws-sdk/core@3.973.8': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/xml-builder': 3.972.4 + '@smithy/core': 3.23.0 + '@smithy/node-config-provider': 4.3.8 '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.11.3 '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 '@aws-sdk/credential-provider-env@3.970.0': @@ -9000,17 +9740,12 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.947.0': + '@aws-sdk/credential-provider-env@3.972.6': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@smithy/fetch-http-handler': 5.3.9 - '@smithy/node-http-handler': 4.4.8 + '@aws-sdk/core': 3.973.8 + '@aws-sdk/types': 3.973.1 '@smithy/property-provider': 4.2.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/smithy-client': 4.10.9 '@smithy/types': 4.12.0 - '@smithy/util-stream': 4.5.10 tslib: 2.8.1 '@aws-sdk/credential-provider-http@3.970.0': @@ -9026,24 +9761,18 @@ snapshots: '@smithy/util-stream': 4.5.10 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.948.0': + '@aws-sdk/credential-provider-http@3.972.8': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/credential-provider-env': 3.947.0 - '@aws-sdk/credential-provider-http': 3.947.0 - '@aws-sdk/credential-provider-login': 3.948.0 - '@aws-sdk/credential-provider-process': 3.947.0 - '@aws-sdk/credential-provider-sso': 3.948.0 - '@aws-sdk/credential-provider-web-identity': 3.948.0 - '@aws-sdk/nested-clients': 3.948.0 - '@aws-sdk/types': 3.936.0 - '@smithy/credential-provider-imds': 4.2.8 + '@aws-sdk/core': 3.973.8 + '@aws-sdk/types': 3.973.1 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.10 '@smithy/property-provider': 4.2.8 - '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.3 '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.12 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt '@aws-sdk/credential-provider-ini@3.971.0': dependencies: @@ -9064,13 +9793,19 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-login@3.948.0': + '@aws-sdk/credential-provider-ini@3.972.6': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/nested-clients': 3.948.0 - '@aws-sdk/types': 3.936.0 + '@aws-sdk/core': 3.973.8 + '@aws-sdk/credential-provider-env': 3.972.6 + '@aws-sdk/credential-provider-http': 3.972.8 + '@aws-sdk/credential-provider-login': 3.972.6 + '@aws-sdk/credential-provider-process': 3.972.6 + '@aws-sdk/credential-provider-sso': 3.972.6 + '@aws-sdk/credential-provider-web-identity': 3.972.6 + '@aws-sdk/nested-clients': 3.988.0 + '@aws-sdk/types': 3.973.1 + '@smithy/credential-provider-imds': 4.2.8 '@smithy/property-provider': 4.2.8 - '@smithy/protocol-http': 5.3.8 '@smithy/shared-ini-file-loader': 4.4.3 '@smithy/types': 4.12.0 tslib: 2.8.1 @@ -9090,17 +9825,13 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.948.0': + '@aws-sdk/credential-provider-login@3.972.6': dependencies: - '@aws-sdk/credential-provider-env': 3.947.0 - '@aws-sdk/credential-provider-http': 3.947.0 - '@aws-sdk/credential-provider-ini': 3.948.0 - '@aws-sdk/credential-provider-process': 3.947.0 - '@aws-sdk/credential-provider-sso': 3.948.0 - '@aws-sdk/credential-provider-web-identity': 3.948.0 - '@aws-sdk/types': 3.936.0 - '@smithy/credential-provider-imds': 4.2.8 + '@aws-sdk/core': 3.973.8 + '@aws-sdk/nested-clients': 3.988.0 + '@aws-sdk/types': 3.973.1 '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 '@smithy/shared-ini-file-loader': 4.4.3 '@smithy/types': 4.12.0 tslib: 2.8.1 @@ -9124,14 +9855,22 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.947.0': + '@aws-sdk/credential-provider-node@3.972.7': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 + '@aws-sdk/credential-provider-env': 3.972.6 + '@aws-sdk/credential-provider-http': 3.972.8 + '@aws-sdk/credential-provider-ini': 3.972.6 + '@aws-sdk/credential-provider-process': 3.972.6 + '@aws-sdk/credential-provider-sso': 3.972.6 + '@aws-sdk/credential-provider-web-identity': 3.972.6 + '@aws-sdk/types': 3.973.1 + '@smithy/credential-provider-imds': 4.2.8 '@smithy/property-provider': 4.2.8 '@smithy/shared-ini-file-loader': 4.4.3 '@smithy/types': 4.12.0 tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt '@aws-sdk/credential-provider-process@3.970.0': dependencies: @@ -9142,18 +9881,14 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.948.0': + '@aws-sdk/credential-provider-process@3.972.6': dependencies: - '@aws-sdk/client-sso': 3.948.0 - '@aws-sdk/core': 3.947.0 - '@aws-sdk/token-providers': 3.948.0 - '@aws-sdk/types': 3.936.0 + '@aws-sdk/core': 3.973.8 + '@aws-sdk/types': 3.973.1 '@smithy/property-provider': 4.2.8 '@smithy/shared-ini-file-loader': 4.4.3 '@smithy/types': 4.12.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt '@aws-sdk/credential-provider-sso@3.971.0': dependencies: @@ -9168,11 +9903,12 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.948.0': + '@aws-sdk/credential-provider-sso@3.972.6': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/nested-clients': 3.948.0 - '@aws-sdk/types': 3.936.0 + '@aws-sdk/client-sso': 3.988.0 + '@aws-sdk/core': 3.973.8 + '@aws-sdk/token-providers': 3.988.0 + '@aws-sdk/types': 3.973.1 '@smithy/property-provider': 4.2.8 '@smithy/shared-ini-file-loader': 4.4.3 '@smithy/types': 4.12.0 @@ -9192,23 +9928,28 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/eventstream-handler-node@3.936.0': + '@aws-sdk/credential-provider-web-identity@3.972.6': dependencies: - '@aws-sdk/types': 3.936.0 + '@aws-sdk/core': 3.973.8 + '@aws-sdk/nested-clients': 3.988.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/eventstream-handler-node@3.972.5': + dependencies: + '@aws-sdk/types': 3.973.1 '@smithy/eventstream-codec': 4.2.8 '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-eventstream@3.936.0': + '@aws-sdk/middleware-eventstream@3.972.3': dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/protocol-http': 5.3.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-host-header@3.936.0': - dependencies: - '@aws-sdk/types': 3.936.0 + '@aws-sdk/types': 3.973.1 '@smithy/protocol-http': 5.3.8 '@smithy/types': 4.12.0 tslib: 2.8.1 @@ -9220,9 +9961,10 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.936.0': + '@aws-sdk/middleware-host-header@3.972.3': dependencies: - '@aws-sdk/types': 3.936.0 + '@aws-sdk/types': 3.973.1 + '@smithy/protocol-http': 5.3.8 '@smithy/types': 4.12.0 tslib: 2.8.1 @@ -9232,11 +9974,9 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.948.0': + '@aws-sdk/middleware-logger@3.972.3': dependencies: - '@aws-sdk/types': 3.936.0 - '@aws/lambda-invoke-store': 0.2.3 - '@smithy/protocol-http': 5.3.8 + '@aws-sdk/types': 3.973.1 '@smithy/types': 4.12.0 tslib: 2.8.1 @@ -9248,12 +9988,10 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.947.0': + '@aws-sdk/middleware-recursion-detection@3.972.3': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-endpoints': 3.936.0 - '@smithy/core': 3.20.7 + '@aws-sdk/types': 3.973.1 + '@aws/lambda-invoke-store': 0.2.3 '@smithy/protocol-http': 5.3.8 '@smithy/types': 4.12.0 tslib: 2.8.1 @@ -9268,61 +10006,30 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-websocket@3.936.0': + '@aws-sdk/middleware-user-agent@3.972.8': dependencies: - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-format-url': 3.936.0 + '@aws-sdk/core': 3.973.8 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.988.0 + '@smithy/core': 3.23.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.6': + dependencies: + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-format-url': 3.972.3 '@smithy/eventstream-codec': 4.2.8 '@smithy/eventstream-serde-browser': 4.2.8 '@smithy/fetch-http-handler': 5.3.9 '@smithy/protocol-http': 5.3.8 '@smithy/signature-v4': 5.3.8 '@smithy/types': 4.12.0 - '@smithy/util-hex-encoding': 4.2.0 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.948.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.947.0 - '@aws-sdk/middleware-host-header': 3.936.0 - '@aws-sdk/middleware-logger': 3.936.0 - '@aws-sdk/middleware-recursion-detection': 3.948.0 - '@aws-sdk/middleware-user-agent': 3.947.0 - '@aws-sdk/region-config-resolver': 3.936.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-endpoints': 3.936.0 - '@aws-sdk/util-user-agent-browser': 3.936.0 - '@aws-sdk/util-user-agent-node': 3.947.0 - '@smithy/config-resolver': 4.4.6 - '@smithy/core': 3.20.7 - '@smithy/fetch-http-handler': 5.3.9 - '@smithy/hash-node': 4.2.8 - '@smithy/invalid-dependency': 4.2.8 - '@smithy/middleware-content-length': 4.2.8 - '@smithy/middleware-endpoint': 4.4.8 - '@smithy/middleware-retry': 4.4.24 - '@smithy/middleware-serde': 4.2.9 - '@smithy/middleware-stack': 4.2.8 - '@smithy/node-config-provider': 4.3.8 - '@smithy/node-http-handler': 4.4.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/smithy-client': 4.10.9 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.23 - '@smithy/util-defaults-mode-node': 4.2.26 - '@smithy/util-endpoints': 3.2.8 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-retry': 4.2.8 + '@smithy/util-hex-encoding': 4.2.0 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt '@aws-sdk/nested-clients@3.971.0': dependencies: @@ -9367,13 +10074,48 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/region-config-resolver@3.936.0': + '@aws-sdk/nested-clients@3.988.0': dependencies: - '@aws-sdk/types': 3.936.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.8 + '@aws-sdk/middleware-host-header': 3.972.3 + '@aws-sdk/middleware-logger': 3.972.3 + '@aws-sdk/middleware-recursion-detection': 3.972.3 + '@aws-sdk/middleware-user-agent': 3.972.8 + '@aws-sdk/region-config-resolver': 3.972.3 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.988.0 + '@aws-sdk/util-user-agent-browser': 3.972.3 + '@aws-sdk/util-user-agent-node': 3.972.6 '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.23.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.14 + '@smithy/middleware-retry': 4.4.31 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.10 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.3 '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.30 + '@smithy/util-defaults-mode-node': 4.2.33 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt '@aws-sdk/region-config-resolver@3.969.0': dependencies: @@ -9383,17 +10125,13 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.948.0': + '@aws-sdk/region-config-resolver@3.972.3': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/nested-clients': 3.948.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.8 - '@smithy/shared-ini-file-loader': 4.4.3 + '@aws-sdk/types': 3.973.1 + '@smithy/config-resolver': 4.4.6 + '@smithy/node-config-provider': 4.3.8 '@smithy/types': 4.12.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt '@aws-sdk/token-providers@3.971.0': dependencies: @@ -9407,22 +10145,26 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/types@3.936.0': + '@aws-sdk/token-providers@3.988.0': dependencies: + '@aws-sdk/core': 3.973.8 + '@aws-sdk/nested-clients': 3.988.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 '@smithy/types': 4.12.0 tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt '@aws-sdk/types@3.969.0': dependencies: '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/util-endpoints@3.936.0': + '@aws-sdk/types@3.973.1': dependencies: - '@aws-sdk/types': 3.936.0 '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-endpoints': 3.2.8 tslib: 2.8.1 '@aws-sdk/util-endpoints@3.970.0': @@ -9433,9 +10175,17 @@ snapshots: '@smithy/util-endpoints': 3.2.8 tslib: 2.8.1 - '@aws-sdk/util-format-url@3.936.0': + '@aws-sdk/util-endpoints@3.988.0': dependencies: - '@aws-sdk/types': 3.936.0 + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-endpoints': 3.2.8 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 '@smithy/querystring-builder': 4.2.8 '@smithy/types': 4.12.0 tslib: 2.8.1 @@ -9444,13 +10194,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.936.0': - dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/types': 4.12.0 - bowser: 2.13.1 - tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.969.0': dependencies: '@aws-sdk/types': 3.969.0 @@ -9458,12 +10201,11 @@ snapshots: bowser: 2.13.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.947.0': + '@aws-sdk/util-user-agent-browser@3.972.3': dependencies: - '@aws-sdk/middleware-user-agent': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@smithy/node-config-provider': 4.3.8 + '@aws-sdk/types': 3.973.1 '@smithy/types': 4.12.0 + bowser: 2.13.1 tslib: 2.8.1 '@aws-sdk/util-user-agent-node@3.971.0': @@ -9474,16 +10216,24 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.930.0': + '@aws-sdk/util-user-agent-node@3.972.6': dependencies: + '@aws-sdk/middleware-user-agent': 3.972.8 + '@aws-sdk/types': 3.973.1 + '@smithy/node-config-provider': 4.3.8 '@smithy/types': 4.12.0 - fast-xml-parser: 5.2.5 tslib: 2.8.1 '@aws-sdk/xml-builder@3.969.0': dependencies: '@smithy/types': 4.12.0 - fast-xml-parser: 5.2.5 + fast-xml-parser: 5.3.4 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.4': + dependencies: + '@smithy/types': 4.12.0 + fast-xml-parser: 5.3.4 tslib: 2.8.1 '@aws/lambda-invoke-store@0.2.3': {} @@ -9611,6 +10361,10 @@ snapshots: dependencies: '@babel/types': 7.28.6 + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 @@ -9629,6 +10383,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.28.6)': dependencies: '@babel/core': 7.28.6 @@ -9676,6 +10440,13 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@1.0.2': {} + '@braintree/sanitize-url@7.1.1': {} '@cfworker/json-schema@4.1.1': {} @@ -9684,12 +10455,12 @@ snapshots: dependencies: '@chevrotain/gast': 11.0.3 '@chevrotain/types': 11.0.3 - lodash-es: 4.17.21 + lodash-es: 4.17.23 '@chevrotain/gast@11.0.3': dependencies: '@chevrotain/types': 11.0.3 - lodash-es: 4.17.21 + lodash-es: 4.17.23 '@chevrotain/regexp-to-ast@11.0.3': {} @@ -9697,6 +10468,28 @@ snapshots: '@chevrotain/utils@11.0.3': {} + '@csstools/color-helpers@6.0.1': {} + + '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.1 + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.0.27': {} + + '@csstools/css-tokenizer@4.0.0': {} + '@date-fns/tz@1.4.1': {} '@dotenvx/dotenvx@1.51.4': @@ -9731,6 +10524,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': dependencies: eslint: 9.39.2(jiti@2.6.1) @@ -9777,11 +10648,15 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@exodus/bytes@1.14.1(@noble/hashes@1.8.0)': + optionalDependencies: + '@noble/hashes': 1.8.0 + '@extractus/feed-extractor@7.1.7': dependencies: '@ndaidong/bellajs': 12.0.1 cross-fetch: 4.1.0 - fast-xml-parser: 5.2.5 + fast-xml-parser: 5.3.4 html-entities: 2.6.0 transitivePeerDependencies: - encoding @@ -9829,17 +10704,17 @@ snapshots: dependencies: tslib: 2.8.1 - '@heroui/accordion@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/accordion@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) - '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-aria-accordion': 2.2.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -9850,23 +10725,23 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/alert@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/alert@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@react-stately/utils': 3.10.8(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) transitivePeerDependencies: - framer-motion - '@heroui/aria-utils@2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/aria-utils@2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/utils': 3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-stately/collections': 3.12.7(react@19.2.4) '@react-types/overlays': 3.9.1(react@19.2.4) @@ -9877,20 +10752,20 @@ snapshots: - '@heroui/theme' - framer-motion - '@heroui/autocomplete@2.3.28(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/autocomplete@2.3.28(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/input': 2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/listbox': 2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/input': 2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/listbox': 2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) - '@heroui/scroll-shadow': 2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/scroll-shadow': 2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4) '@react-aria/combobox': 3.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -9903,48 +10778,48 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@heroui/avatar@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/avatar@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-image': 2.1.12(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/badge@2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/badge@2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/breadcrumbs@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/breadcrumbs@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@react-aria/breadcrumbs': 3.5.28(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-types/breadcrumbs': 3.7.16(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/button@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/button@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) - '@heroui/ripple': 2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/ripple': 2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-aria-button': 2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -9953,16 +10828,16 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/calendar@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/calendar@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) - '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-aria-button': 2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@internationalized/date': 3.10.0 '@react-aria/calendar': 3.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -9980,13 +10855,13 @@ snapshots: react-dom: 19.2.4(react@19.2.4) scroll-into-view-if-needed: 3.0.10 - '@heroui/card@2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/card@2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) - '@heroui/ripple': 2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/ripple': 2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-aria-button': 2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -9995,13 +10870,13 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/checkbox@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/checkbox@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-callback-ref': 2.1.8(react@19.2.4) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4) '@react-aria/checkbox': 3.16.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10014,34 +10889,34 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/chip@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/chip@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/code@2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/code@2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/date-input@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/date-input@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@internationalized/date': 3.10.0 '@react-aria/datepicker': 3.15.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10051,19 +10926,19 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/date-picker@2.3.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/date-picker@2.3.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/calendar': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/date-input': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/calendar': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/date-input': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@internationalized/date': 3.10.0 '@react-aria/datepicker': 3.15.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10075,11 +10950,11 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/divider@2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/divider@2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-rsc-utils': 2.1.9(react@19.2.4) - '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@react-types/shared': 3.26.0(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -10088,28 +10963,28 @@ snapshots: dependencies: framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/drawer@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/drawer@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/modal': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/modal': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) transitivePeerDependencies: - framer-motion - '@heroui/dropdown@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/dropdown@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/menu': 2.2.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/menu': 2.2.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/menu': 3.19.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-stately/menu': 3.9.7(react@19.2.4) @@ -10118,20 +10993,20 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/form@2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/form@2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@react-stately/form': 3.2.1(react@19.2.4) '@react-types/form': 3.7.15(react@19.2.4) '@react-types/shared': 3.26.0(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/framer-utils@2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/framer-utils@2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/use-measure': 2.1.8(react@19.2.4) framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 @@ -10139,23 +11014,23 @@ snapshots: transitivePeerDependencies: - '@heroui/theme' - '@heroui/image@2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/image@2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-image': 2.1.12(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/input-otp@2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/input-otp@2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-form-reset': 2.0.1(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/form': 3.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10166,14 +11041,14 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/input@2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/input@2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10187,36 +11062,36 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@heroui/kbd@2.2.21(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/kbd@2.2.21(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/link@2.2.22(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/link@2.2.22(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-aria-link': 2.2.20(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-types/link': 3.6.4(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/listbox@2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/listbox@2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-is-mobile': 2.2.12(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10229,14 +11104,14 @@ snapshots: transitivePeerDependencies: - framer-motion - '@heroui/menu@2.2.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/menu@2.2.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-is-mobile': 2.2.12(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10249,15 +11124,15 @@ snapshots: transitivePeerDependencies: - framer-motion - '@heroui/modal@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/modal@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) - '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-aria-button': 2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/use-aria-modal-overlay': 2.2.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/use-disclosure': 2.2.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10271,14 +11146,14 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/navbar@2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/navbar@2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) - '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-resize': 2.1.8(react@19.2.4) '@heroui/use-scroll-position': 2.1.8(react@19.2.4) '@react-aria/button': 3.14.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10291,15 +11166,15 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/number-input@2.0.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/number-input@2.0.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10314,13 +11189,13 @@ snapshots: transitivePeerDependencies: - framer-motion - '@heroui/pagination@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/pagination@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-intersection-observer': 2.2.14(react@19.2.4) '@heroui/use-pagination': 2.2.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10331,16 +11206,16 @@ snapshots: react-dom: 19.2.4(react@19.2.4) scroll-into-view-if-needed: 3.0.10 - '@heroui/popover@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/popover@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) - '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-aria-button': 2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/use-aria-overlay': 2.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4) @@ -10353,25 +11228,25 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/progress@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/progress@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-is-mounted': 2.1.8(react@19.2.4) '@react-aria/progress': 3.4.26(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-types/progress': 3.5.15(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/radio@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/radio@2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/radio': 3.12.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10392,57 +11267,57 @@ snapshots: '@heroui/shared-utils': 2.1.11 react: 19.2.4 - '@heroui/react@2.8.4(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.13)': + '@heroui/react@2.8.4(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.18)': dependencies: - '@heroui/accordion': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/alert': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/autocomplete': 2.3.28(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/avatar': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/badge': 2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/breadcrumbs': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/calendar': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/card': 2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/checkbox': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/chip': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/code': 2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/date-input': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/date-picker': 2.3.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/drawer': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/dropdown': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/image': 2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/input': 2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/input-otp': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/kbd': 2.2.21(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/link': 2.2.22(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/listbox': 2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/menu': 2.2.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/modal': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/navbar': 2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/number-input': 2.0.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/pagination': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/progress': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/radio': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/ripple': 2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/scroll-shadow': 2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/select': 2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/skeleton': 2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/slider': 2.4.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/snippet': 2.2.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/spacer': 2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/switch': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/table': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/tabs': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) - '@heroui/toast': 2.0.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/tooltip': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/user': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/accordion': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/alert': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/autocomplete': 2.3.28(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/avatar': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/badge': 2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/breadcrumbs': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/calendar': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/card': 2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/checkbox': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/chip': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/code': 2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/date-input': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/date-picker': 2.3.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/divider': 2.2.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/drawer': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/dropdown': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/image': 2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/input': 2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/input-otp': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/kbd': 2.2.21(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/link': 2.2.22(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/listbox': 2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/menu': 2.2.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/modal': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/navbar': 2.2.24(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/number-input': 2.0.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/pagination': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/progress': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/radio': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/ripple': 2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/scroll-shadow': 2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/select': 2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/skeleton': 2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/slider': 2.4.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/snippet': 2.2.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/spacer': 2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/switch': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/table': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/tabs': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) + '@heroui/toast': 2.0.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/tooltip': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/user': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/visually-hidden': 3.8.27(react-dom@19.2.4(react@19.2.4))(react@19.2.4) framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 @@ -10451,39 +11326,39 @@ snapshots: - '@types/react' - tailwindcss - '@heroui/ripple@2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/ripple@2.2.19(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/scroll-shadow@2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/scroll-shadow@2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-data-scroll-overflow': 2.2.12(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/select@2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/select@2.4.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/listbox': 2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/form': 2.1.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/listbox': 2.3.25(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/popover': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) - '@heroui/scroll-shadow': 2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/scroll-shadow': 2.3.17(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-aria-button': 2.2.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/use-aria-multiselect': 2.4.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/use-form-reset': 2.0.1(react@19.2.4) @@ -10504,21 +11379,21 @@ snapshots: '@heroui/shared-utils@2.1.11': {} - '@heroui/skeleton@2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/skeleton@2.2.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/slider@2.4.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/slider@2.4.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) - '@heroui/tooltip': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) + '@heroui/tooltip': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10530,47 +11405,47 @@ snapshots: transitivePeerDependencies: - framer-motion - '@heroui/snippet@2.2.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/snippet@2.2.27(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/button': 2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) - '@heroui/tooltip': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) + '@heroui/tooltip': 2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/use-clipboard': 2.1.9(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/spacer@2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/spacer@2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/spinner@2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/spinner@2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) transitivePeerDependencies: - framer-motion - '@heroui/switch@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/switch@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10580,17 +11455,17 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/system-rsc@2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react@19.2.4)': + '@heroui/system-rsc@2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4)': dependencies: - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@react-types/shared': 3.26.0(react@19.2.4) clsx: 1.2.1 react: 19.2.4 - '@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) - '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react@19.2.4) + '@heroui/system-rsc': 2.3.19(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react@19.2.4) '@react-aria/i18n': 3.12.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/overlays': 3.29.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/utils': 3.30.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10600,15 +11475,15 @@ snapshots: transitivePeerDependencies: - '@heroui/theme' - '@heroui/table@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/table@2.2.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/checkbox': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/checkbox': 2.3.26(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/spacer': 2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/spacer': 2.2.20(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/table': 3.17.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10621,13 +11496,13 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/tabs@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/tabs@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-is-mounted': 2.1.8(react@19.2.4) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10639,7 +11514,7 @@ snapshots: react-dom: 19.2.4(react@19.2.4) scroll-into-view-if-needed: 3.0.10 - '@heroui/theme@2.4.22(tailwindcss@4.1.13)': + '@heroui/theme@2.4.22(tailwindcss@4.1.18)': dependencies: '@heroui/shared-utils': 2.1.11 clsx: 1.2.1 @@ -10648,17 +11523,17 @@ snapshots: deepmerge: 4.3.1 flat: 5.0.2 tailwind-merge: 3.3.1 - tailwind-variants: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.13) - tailwindcss: 4.1.13 + tailwind-variants: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.18) + tailwindcss: 4.1.18 - '@heroui/toast@2.0.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/toast@2.0.16(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-icons': 2.1.10(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/spinner': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-is-mobile': 2.2.12(react@19.2.4) '@react-aria/interactions': 3.25.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/toast': 3.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10667,15 +11542,15 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@heroui/tooltip@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/tooltip@2.2.23(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/aria-utils': 2.2.23(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/dom-animation': 2.1.10(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) - '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/framer-utils': 2.1.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@heroui/use-aria-overlay': 2.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.4) '@react-aria/overlays': 3.29.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10838,20 +11713,20 @@ snapshots: dependencies: react: 19.2.4 - '@heroui/user@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@heroui/user@2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@heroui/avatar': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.13))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/avatar': 2.2.21(@heroui/system@2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@heroui/theme@2.4.22(tailwindcss@4.1.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroui/react-utils': 2.1.13(react@19.2.4) '@heroui/shared-utils': 2.1.11 - '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.13))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@heroui/theme': 2.4.22(tailwindcss@4.1.13) + '@heroui/system': 2.4.22(@heroui/theme@2.4.22(tailwindcss@4.1.18))(framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@heroui/theme': 2.4.22(tailwindcss@4.1.18) '@react-aria/focus': 3.21.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@hono/node-server@1.19.9(hono@4.11.4)': + '@hono/node-server@1.19.9(hono@4.11.7)': dependencies: - hono: 4.11.4 + hono: 4.11.7 '@hookform/resolvers@5.2.2(react-hook-form@7.62.0(react@19.2.4))': dependencies: @@ -11101,7 +11976,7 @@ snapshots: '@isaacs/balanced-match@4.0.1': {} - '@isaacs/brace-expansion@5.0.0': + '@isaacs/brace-expansion@5.0.1': dependencies: '@isaacs/balanced-match': 4.0.1 @@ -11114,10 +11989,6 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.2 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -11142,24 +12013,24 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@langchain/aws@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))': + '@langchain/aws@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))': dependencies: '@aws-sdk/client-bedrock-agent-runtime': 3.971.0 - '@aws-sdk/client-bedrock-runtime': 3.948.0 + '@aws-sdk/client-bedrock-runtime': 3.988.0 '@aws-sdk/client-kendra': 3.971.0 '@aws-sdk/credential-provider-node': 3.971.0 - '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)) + '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)) transitivePeerDependencies: - aws-crt - '@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11))': + '@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))': dependencies: '@cfworker/json-schema': 4.1.1 ansi-styles: 5.2.0 camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.4.7(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)) + langsmith: 0.4.7(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)) mustache: 4.2.0 p-queue: 6.6.2 uuid: 10.0.0 @@ -11170,26 +12041,26 @@ snapshots: - '@opentelemetry/sdk-trace-base' - openai - '@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))': + '@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))': dependencies: - '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)) + '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)) uuid: 10.0.0 - '@langchain/langgraph-sdk@1.5.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@langchain/langgraph-sdk@1.5.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: p-queue: 9.1.0 p-retry: 7.1.1 uuid: 13.0.0 optionalDependencies: - '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)) + '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@langchain/langgraph@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11)': + '@langchain/langgraph@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11)': dependencies: - '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)) - '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11))) - '@langchain/langgraph-sdk': 1.5.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)) + '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))) + '@langchain/langgraph-sdk': 1.5.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) uuid: 10.0.0 zod: 4.1.11 optionalDependencies: @@ -11198,25 +12069,24 @@ snapshots: - react - react-dom - '@langchain/mcp-adapters@1.0.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))(@langchain/langgraph@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11))(hono@4.11.4)': + '@langchain/mcp-adapters@1.1.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(@langchain/langgraph@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11))': dependencies: - '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)) - '@langchain/langgraph': 1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11) - '@modelcontextprotocol/sdk': 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@4.1.11) + '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)) + '@langchain/langgraph': 1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11) + '@modelcontextprotocol/sdk': 1.26.0(@cfworker/json-schema@4.1.1)(zod@4.1.11) debug: 4.4.3 zod: 4.1.11 optionalDependencies: extended-eventsource: 1.7.0 transitivePeerDependencies: - '@cfworker/json-schema' - - hono - supports-color - '@langchain/openai@1.1.3(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))': + '@langchain/openai@1.1.3(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(ws@8.19.0)': dependencies: - '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)) + '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)) js-tiktoken: 1.0.21 - openai: 6.16.0(zod@4.1.11) + openai: 6.16.0(ws@8.19.0)(zod@4.1.11) zod: 4.1.11 transitivePeerDependencies: - ws @@ -11225,9 +12095,9 @@ snapshots: dependencies: langium: 3.3.1 - '@modelcontextprotocol/sdk@1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.26.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)': dependencies: - '@hono/node-server': 1.19.9(hono@4.11.4) + '@hono/node-server': 1.19.9(hono@4.11.7) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -11236,7 +12106,8 @@ snapshots: eventsource: 3.0.7 eventsource-parser: 3.0.6 express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.11.7 jose: 6.1.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -11246,12 +12117,11 @@ snapshots: optionalDependencies: '@cfworker/json-schema': 4.1.1 transitivePeerDependencies: - - hono - supports-color - '@modelcontextprotocol/sdk@1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@4.1.11)': + '@modelcontextprotocol/sdk@1.26.0(@cfworker/json-schema@4.1.1)(zod@4.1.11)': dependencies: - '@hono/node-server': 1.19.9(hono@4.11.4) + '@hono/node-server': 1.19.9(hono@4.11.7) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -11260,7 +12130,8 @@ snapshots: eventsource: 3.0.7 eventsource-parser: 3.0.6 express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.11.7 jose: 6.1.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -11270,7 +12141,6 @@ snapshots: optionalDependencies: '@cfworker/json-schema': 4.1.1 transitivePeerDependencies: - - hono - supports-color '@mswjs/interceptors@0.40.0': @@ -13643,6 +14513,8 @@ snapshots: '@react-types/shared': 3.26.0(react@19.2.4) react: 19.2.4 + '@rolldown/pluginutils@1.0.0-beta.53': {} + '@rollup/plugin-commonjs@28.0.1(rollup@4.55.2)': dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.55.2) @@ -14006,6 +14878,19 @@ snapshots: '@smithy/uuid': 1.1.0 tslib: 2.8.1 + '@smithy/core@3.23.0': + dependencies: + '@smithy/middleware-serde': 4.2.9 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.12 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + '@smithy/credential-provider-imds@4.2.8': dependencies: '@smithy/node-config-provider': 4.3.8 @@ -14078,6 +14963,17 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 + '@smithy/middleware-endpoint@4.4.14': + dependencies: + '@smithy/core': 3.23.0 + '@smithy/middleware-serde': 4.2.9 + '@smithy/node-config-provider': 4.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-middleware': 4.2.8 + tslib: 2.8.1 + '@smithy/middleware-endpoint@4.4.8': dependencies: '@smithy/core': 3.20.7 @@ -14101,6 +14997,18 @@ snapshots: '@smithy/uuid': 1.1.0 tslib: 2.8.1 + '@smithy/middleware-retry@4.4.31': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/service-error-classification': 4.2.8 + '@smithy/smithy-client': 4.11.3 + '@smithy/types': 4.12.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + '@smithy/middleware-serde@4.2.9': dependencies: '@smithy/protocol-http': 5.3.8 @@ -14119,6 +15027,14 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 + '@smithy/node-http-handler@4.4.10': + dependencies: + '@smithy/abort-controller': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + '@smithy/node-http-handler@4.4.8': dependencies: '@smithy/abort-controller': 4.2.8 @@ -14178,6 +15094,16 @@ snapshots: '@smithy/util-stream': 4.5.10 tslib: 2.8.1 + '@smithy/smithy-client@4.11.3': + dependencies: + '@smithy/core': 3.23.0 + '@smithy/middleware-endpoint': 4.4.14 + '@smithy/middleware-stack': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.12 + tslib: 2.8.1 + '@smithy/types@4.12.0': dependencies: tslib: 2.8.1 @@ -14223,6 +15149,13 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 + '@smithy/util-defaults-mode-browser@4.3.30': + dependencies: + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.11.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + '@smithy/util-defaults-mode-node@4.2.26': dependencies: '@smithy/config-resolver': 4.4.6 @@ -14233,6 +15166,16 @@ snapshots: '@smithy/types': 4.12.0 tslib: 2.8.1 + '@smithy/util-defaults-mode-node@4.2.33': + dependencies: + '@smithy/config-resolver': 4.4.6 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.11.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + '@smithy/util-endpoints@3.2.8': dependencies: '@smithy/node-config-provider': 4.3.8 @@ -14265,6 +15208,17 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 + '@smithy/util-stream@4.5.12': + dependencies: + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.10 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + '@smithy/util-uri-escape@4.2.0': dependencies: tslib: 2.8.1 @@ -14295,85 +15249,82 @@ snapshots: dependencies: tslib: 2.8.1 - '@tailwindcss/node@4.1.13': + '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 enhanced-resolve: 5.18.4 jiti: 2.6.1 - lightningcss: 1.30.1 + lightningcss: 1.30.2 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.1.13 + tailwindcss: 4.1.18 - '@tailwindcss/oxide-android-arm64@4.1.13': + '@tailwindcss/oxide-android-arm64@4.1.18': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.13': + '@tailwindcss/oxide-darwin-arm64@4.1.18': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.13': + '@tailwindcss/oxide-darwin-x64@4.1.18': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.13': + '@tailwindcss/oxide-freebsd-x64@4.1.18': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.13': + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.13': + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.13': + '@tailwindcss/oxide-linux-x64-musl@4.1.18': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.13': + '@tailwindcss/oxide-wasm32-wasi@4.1.18': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.13': + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': optional: true - '@tailwindcss/oxide@4.1.13': - dependencies: - detect-libc: 2.1.2 - tar: 7.5.3 + '@tailwindcss/oxide@4.1.18': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.13 - '@tailwindcss/oxide-darwin-arm64': 4.1.13 - '@tailwindcss/oxide-darwin-x64': 4.1.13 - '@tailwindcss/oxide-freebsd-x64': 4.1.13 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.13 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.13 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.13 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.13 - '@tailwindcss/oxide-linux-x64-musl': 4.1.13 - '@tailwindcss/oxide-wasm32-wasi': 4.1.13 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.13 + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/postcss@4.1.13': + '@tailwindcss/postcss@4.1.18': dependencies: '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.1.13 - '@tailwindcss/oxide': 4.1.13 + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 postcss: 8.5.6 - tailwindcss: 4.1.13 + tailwindcss: 4.1.18 - '@tailwindcss/typography@0.5.16(tailwindcss@4.1.13)': + '@tailwindcss/typography@0.5.16(tailwindcss@4.1.18)': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 4.1.13 + tailwindcss: 4.1.18 '@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: @@ -14391,6 +15342,40 @@ snapshots: '@tanstack/virtual-core@3.11.3': {} + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/runtime': 7.28.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@testing-library/dom': 10.4.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 @@ -14402,6 +15387,34 @@ snapshots: tslib: 2.8.1 optional: true + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.6 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.6 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/connect@3.4.38': dependencies: '@types/node': 24.10.8 @@ -14529,6 +15542,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -14615,6 +15630,8 @@ snapshots: '@types/uuid@10.0.0': {} + '@types/validate-npm-package-name@4.0.2': {} + '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4))(eslint@9.39.2(jiti@2.6.1))(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -14769,6 +15786,72 @@ snapshots: '@vercel/oidc@3.0.5': {} + '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2))': + dependencies: + '@babel/core': 7.28.6 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.6) + '@rolldown/pluginutils': 1.0.0-beta.53 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.1(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.8)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.7(@types/node@24.10.8)(typescript@5.5.4))(terser@5.46.0)(yaml@2.8.2))': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.0.18 + ast-v8-to-istanbul: 0.3.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.2 + obug: 2.1.1 + std-env: 3.10.0 + tinyrainbow: 3.0.3 + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.8)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.7(@types/node@24.10.8)(typescript@5.5.4))(terser@5.46.0)(yaml@2.8.2) + + '@vitest/expect@4.0.18': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.18(msw@2.12.7(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.1(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.7(@types/node@24.10.8)(typescript@5.5.4) + vite: 7.3.1(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2) + + '@vitest/pretty-format@4.0.18': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.18': + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -14945,6 +16028,10 @@ snapshots: dependencies: tslib: 2.8.1 + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: @@ -15014,12 +16101,20 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} ast-types@0.16.1: dependencies: tslib: 2.8.1 + ast-v8-to-istanbul@0.3.11: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + async-function@1.0.0: {} autoprefixer@10.4.19(postcss@8.4.38): @@ -15052,6 +16147,10 @@ snapshots: baseline-browser-mapping@2.9.15: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} body-parser@2.2.2: @@ -15093,6 +16192,10 @@ snapshots: buffer-from@1.1.2: {} + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -15120,6 +16223,8 @@ snapshots: ccount@2.0.1: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -15138,7 +16243,7 @@ snapshots: chevrotain-allstar@0.3.1(chevrotain@11.0.3): dependencies: chevrotain: 11.0.3 - lodash-es: 4.17.22 + lodash-es: 4.17.23 chevrotain@11.0.3: dependencies: @@ -15147,7 +16252,7 @@ snapshots: '@chevrotain/regexp-to-ast': 11.0.3 '@chevrotain/types': 11.0.3 '@chevrotain/utils': 11.0.3 - lodash-es: 4.17.21 + lodash-es: 4.17.23 chokidar@3.6.0: dependencies: @@ -15161,8 +16266,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chownr@3.0.0: {} - chrome-trace-event@1.0.4: {} cjs-module-lexer@1.4.3: {} @@ -15302,8 +16405,22 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + cssesc@3.0.0: {} + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.2 + '@csstools/css-syntax-patches-for-csstree': 1.0.27 + css-tree: 3.1.0 + lru-cache: 11.2.6 + csstype@3.2.3: {} cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): @@ -15488,17 +16605,22 @@ snapshots: dagre-d3-es@7.0.13: dependencies: d3: 7.9.0 - lodash-es: 4.17.22 + lodash-es: 4.17.23 dagre@0.8.5: dependencies: graphlib: 2.1.8 - lodash: 4.17.21 + lodash: 4.17.23 damerau-levenshtein@1.0.8: {} data-uri-to-buffer@4.0.1: {} + data-urls@6.0.1: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -15547,12 +16669,21 @@ snapshots: deepmerge@4.3.1: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 es-errors: 1.3.0 gopd: 1.2.0 + define-lazy-prop@3.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -15581,6 +16712,10 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.28.6 @@ -15720,6 +16855,8 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 + es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: @@ -15743,6 +16880,35 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -16015,6 +17181,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} etag@1.8.1: {} @@ -16070,9 +17240,12 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 - express-rate-limit@7.5.1(express@5.2.1): + expect-type@1.3.0: {} + + express-rate-limit@8.2.1(express@5.2.1): dependencies: express: 5.2.1 + ip-address: 10.0.1 express@5.2.1: dependencies: @@ -16140,7 +17313,7 @@ snapshots: fast-uri@3.1.0: {} - fast-xml-parser@5.2.5: + fast-xml-parser@5.3.4: dependencies: strnum: 2.1.2 @@ -16338,7 +17511,7 @@ snapshots: graphlib@2.1.8: dependencies: - lodash: 4.17.21 + lodash: 4.17.23 graphql@16.12.0: {} @@ -16500,10 +17673,18 @@ snapshots: dependencies: react-is: 16.13.1 - hono@4.11.4: {} + hono@4.11.7: {} + + html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0): + dependencies: + '@exodus/bytes': 1.14.1(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' html-entities@2.6.0: {} + html-escaper@2.0.2: {} + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} @@ -16516,6 +17697,13 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -16564,6 +17752,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + inherits@2.0.4: {} inline-style-parser@0.2.7: {} @@ -16590,6 +17780,8 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.2 tslib: 2.8.1 + ip-address@10.0.1: {} + ipaddr.js@1.9.1: {} is-alphabetical@2.0.1: {} @@ -16653,6 +17845,8 @@ snapshots: is-decimal@2.0.1: {} + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -16681,6 +17875,12 @@ snapshots: is-hexadecimal@2.0.1: {} + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-interactive@2.0.0: {} is-map@2.0.3: {} @@ -16702,6 +17902,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} is-reference@1.2.1: @@ -16759,12 +17961,29 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + isarray@2.0.5: {} isexe@2.0.0: {} isexe@3.1.1: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -16796,12 +18015,42 @@ snapshots: dependencies: base64-js: 1.5.1 + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: dependencies: argparse: 2.0.1 + jsdom@27.4.0(@noble/hashes@1.8.0): + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@exodus/bytes': 1.14.1(@noble/hashes@1.8.0) + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0) + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -16853,12 +18102,12 @@ snapshots: kleur@4.1.5: {} - langchain@1.2.10(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11)): + langchain@1.2.10(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11)): dependencies: - '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)) - '@langchain/langgraph': 1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11) - '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11))) - langsmith: 0.4.7(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)) + '@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)) + '@langchain/langgraph': 1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.1.11))(zod@4.1.11) + '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11))) + langsmith: 0.4.7(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)) uuid: 10.0.0 zod: 4.1.11 transitivePeerDependencies: @@ -16878,7 +18127,7 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.0.8 - langsmith@0.4.7(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(zod@4.1.11)): + langsmith@0.4.7(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.16.0(ws@8.19.0)(zod@4.1.11)): dependencies: '@types/uuid': 10.0.0 chalk: 4.1.2 @@ -16889,7 +18138,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - openai: 6.16.0(zod@4.1.11) + openai: 6.16.0(ws@8.19.0)(zod@4.1.11) language-subtag-registry@0.3.23: {} @@ -16906,50 +18155,54 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lightningcss-darwin-arm64@1.30.1: + lightningcss-android-arm64@1.30.2: optional: true - lightningcss-darwin-x64@1.30.1: + lightningcss-darwin-arm64@1.30.2: optional: true - lightningcss-freebsd-x64@1.30.1: + lightningcss-darwin-x64@1.30.2: optional: true - lightningcss-linux-arm-gnueabihf@1.30.1: + lightningcss-freebsd-x64@1.30.2: optional: true - lightningcss-linux-arm64-gnu@1.30.1: + lightningcss-linux-arm-gnueabihf@1.30.2: optional: true - lightningcss-linux-arm64-musl@1.30.1: + lightningcss-linux-arm64-gnu@1.30.2: optional: true - lightningcss-linux-x64-gnu@1.30.1: + lightningcss-linux-arm64-musl@1.30.2: optional: true - lightningcss-linux-x64-musl@1.30.1: + lightningcss-linux-x64-gnu@1.30.2: optional: true - lightningcss-win32-arm64-msvc@1.30.1: + lightningcss-linux-x64-musl@1.30.2: optional: true - lightningcss-win32-x64-msvc@1.30.1: + lightningcss-win32-arm64-msvc@1.30.2: optional: true - lightningcss@1.30.1: + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: dependencies: detect-libc: 2.1.2 optionalDependencies: - lightningcss-darwin-arm64: 1.30.1 - lightningcss-darwin-x64: 1.30.1 - lightningcss-freebsd-x64: 1.30.1 - lightningcss-linux-arm-gnueabihf: 1.30.1 - lightningcss-linux-arm64-gnu: 1.30.1 - lightningcss-linux-arm64-musl: 1.30.1 - lightningcss-linux-x64-gnu: 1.30.1 - lightningcss-linux-x64-musl: 1.30.1 - lightningcss-win32-arm64-msvc: 1.30.1 - lightningcss-win32-x64-msvc: 1.30.1 + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 lilconfig@3.1.3: {} @@ -16985,9 +18238,7 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.17.21: {} - - lodash-es@4.17.22: {} + lodash-es@4.17.23: {} lodash.castarray@4.4.0: {} @@ -16995,7 +18246,7 @@ snapshots: lodash.merge@4.6.2: {} - lodash@4.17.21: {} + lodash@4.17.23: {} log-symbols@6.0.0: dependencies: @@ -17018,6 +18269,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.6: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -17030,6 +18283,8 @@ snapshots: dependencies: react: 19.2.4 + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -17038,6 +18293,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + markdown-table@3.0.4: {} marked@15.0.12: {} @@ -17211,6 +18476,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdn-data@2.12.2: {} + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -17235,7 +18502,7 @@ snapshots: dompurify: 3.3.1 katex: 0.16.27 khroma: 2.1.0 - lodash-es: 4.17.22 + lodash-es: 4.17.23 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.3.6 @@ -17498,9 +18765,11 @@ snapshots: mimic-function@5.0.1: {} + min-indent@1.0.1: {} + minimatch@10.1.1: dependencies: - '@isaacs/brace-expansion': 5.0.0 + '@isaacs/brace-expansion': 5.0.1 minimatch@3.1.2: dependencies: @@ -17514,10 +18783,6 @@ snapshots: minipass@7.1.2: {} - minizlib@3.1.0: - dependencies: - minipass: 7.1.2 - mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -17692,6 +18957,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -17720,8 +18987,18 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 - openai@6.16.0(zod@4.1.11): + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + openai@6.16.0(ws@8.19.0)(zod@4.1.11): optionalDependencies: + ws: 8.19.0 zod: 4.1.11 optionator@0.9.4: @@ -17814,6 +19091,10 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -17889,6 +19170,11 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-value-parser@4.2.0: {} postcss@8.4.31: @@ -17919,6 +19205,8 @@ snapshots: dependencies: xtend: 4.0.2 + powershell-utils@0.1.0: {} + preact-render-to-string@6.5.11(preact@10.24.3): dependencies: preact: 10.24.3 @@ -17937,6 +19225,12 @@ snapshots: prettier@3.6.2: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -18065,6 +19359,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-is@18.3.1: {} react-markdown@10.1.0(@types/react@19.2.8)(react@19.2.4): @@ -18085,6 +19381,8 @@ snapshots: transitivePeerDependencies: - supports-color + react-refresh@0.18.0: {} + react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.4): dependencies: react: 19.2.4 @@ -18160,7 +19458,7 @@ snapshots: dependencies: clsx: 2.1.1 eventemitter3: 4.0.7 - lodash: 4.17.21 + lodash: 4.17.23 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) react-is: 18.3.1 @@ -18169,6 +19467,11 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -18382,6 +19685,8 @@ snapshots: transitivePeerDependencies: - supports-color + run-applescript@7.1.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -18415,6 +19720,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} schema-utils@4.3.3: @@ -18487,7 +19796,7 @@ snapshots: setprototypeof@1.2.0: {} - shadcn@3.4.1(@cfworker/json-schema@4.1.1)(@types/node@24.10.8)(hono@4.11.4)(typescript@5.5.4): + shadcn@3.8.4(@cfworker/json-schema@4.1.1)(@types/node@24.10.8)(typescript@5.5.4): dependencies: '@antfu/ni': 25.0.0 '@babel/core': 7.28.6 @@ -18495,7 +19804,8 @@ snapshots: '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.28.6) '@babel/preset-typescript': 7.28.5(@babel/core@7.28.6) '@dotenvx/dotenvx': 1.51.4 - '@modelcontextprotocol/sdk': 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.4)(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.26.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) + '@types/validate-npm-package-name': 4.0.2 browserslist: 4.28.1 commander: 14.0.2 cosmiconfig: 9.0.0(typescript@5.5.4) @@ -18510,20 +19820,23 @@ snapshots: kleur: 4.1.5 msw: 2.12.7(@types/node@24.10.8)(typescript@5.5.4) node-fetch: 3.3.2 + open: 11.0.0 ora: 8.2.0 postcss: 8.5.6 + postcss-selector-parser: 7.1.1 prompts: 2.4.2 recast: 0.23.11 stringify-object: 5.0.0 + tailwind-merge: 3.3.1 ts-morph: 26.0.0 tsconfig-paths: 4.2.0 + validate-npm-package-name: 7.0.2 zod: 3.25.76 zod-to-json-schema: 3.25.1(zod@3.25.76) transitivePeerDependencies: - '@cfworker/json-schema' - '@types/node' - babel-plugin-macros - - hono - supports-color - typescript @@ -18630,6 +19943,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -18665,12 +19980,16 @@ snapshots: stable-hash@0.0.5: {} + stackback@0.0.2: {} + stacktrace-parser@0.1.11: dependencies: type-fest: 0.7.1 statuses@2.0.2: {} + std-env@3.10.0: {} + stdin-discarder@0.2.2: {} stop-iteration-iterator@1.1.0: @@ -18808,6 +20127,10 @@ snapshots: strip-final-newline@4.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-json-comments@3.1.1: {} strnum@2.1.2: {} @@ -18845,6 +20168,8 @@ snapshots: react: 19.2.4 use-sync-external-store: 1.6.0(react@19.2.4) + symbol-tree@3.2.4: {} + synckit@0.11.12: dependencies: '@pkgr/core': 0.2.9 @@ -18855,33 +20180,25 @@ snapshots: tailwind-merge@3.3.1: {} - tailwind-variants@0.1.20(tailwindcss@4.1.13): + tailwind-variants@0.1.20(tailwindcss@4.1.18): dependencies: tailwind-merge: 1.14.0 - tailwindcss: 4.1.13 + tailwindcss: 4.1.18 - tailwind-variants@3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.13): + tailwind-variants@3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.18): dependencies: - tailwindcss: 4.1.13 + tailwindcss: 4.1.18 optionalDependencies: tailwind-merge: 3.3.1 - tailwindcss-animate@1.0.7(tailwindcss@4.1.13): + tailwindcss-animate@1.0.7(tailwindcss@4.1.18): dependencies: - tailwindcss: 4.1.13 + tailwindcss: 4.1.18 - tailwindcss@4.1.13: {} + tailwindcss@4.1.18: {} tapable@2.3.0: {} - tar@7.5.3: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.1.0 - yallist: 5.0.0 - terser-webpack-plugin@5.3.16(webpack@5.104.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -18904,6 +20221,8 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -18911,6 +20230,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@3.0.3: {} + tldts-core@7.0.19: {} tldts@7.0.19: @@ -18933,6 +20254,10 @@ snapshots: tr46@0.0.3: {} + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -19182,6 +20507,8 @@ snapshots: uuid@9.0.1: {} + validate-npm-package-name@7.0.2: {} + vary@1.1.2: {} vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -19225,6 +20552,61 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite@7.3.1(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.55.2 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.8 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.46.0 + yaml: 2.8.2 + + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.8)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.7(@types/node@24.10.8)(typescript@5.5.4))(terser@5.46.0)(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(msw@2.12.7(@types/node@24.10.8)(typescript@5.5.4))(vite@7.3.1(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.1(@types/node@24.10.8)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 24.10.8 + jsdom: 27.4.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + vscode-jsonrpc@8.2.0: {} vscode-languageserver-protocol@3.17.5: @@ -19242,6 +20624,10 @@ snapshots: vscode-uri@3.0.8: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + watchpack@2.5.1: dependencies: glob-to-regexp: 0.4.1 @@ -19253,6 +20639,8 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@8.0.1: {} + webpack-sources@3.3.3: {} webpack-virtual-modules@0.5.0: {} @@ -19289,6 +20677,15 @@ snapshots: - esbuild - uglify-js + whatwg-mimetype@4.0.0: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -19343,6 +20740,11 @@ snapshots: dependencies: isexe: 3.1.1 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} world-atlas@2.0.2: {} @@ -19373,14 +20775,23 @@ snapshots: wrappy@1.0.2: {} + ws@8.19.0: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.0 + powershell-utils: 0.1.0 + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xtend@4.0.2: {} y18n@5.0.8: {} yallist@3.1.1: {} - yallist@5.0.0: {} - yaml@2.8.2: {} yargs-parser@21.1.1: {} diff --git a/ui/proxy.ts b/ui/proxy.ts index 5827f39844..98b0725dea 100644 --- a/ui/proxy.ts +++ b/ui/proxy.ts @@ -50,20 +50,6 @@ export default auth((req: NextRequest & { auth: any }) => { } } - // Redirect /findings to include default muted filter if not present - if ( - pathname === "/findings" && - !req.nextUrl.searchParams.has("filter[muted]") - ) { - const findingsUrl = new URL("/findings", req.url); - // Preserve existing search params - req.nextUrl.searchParams.forEach((value, key) => { - findingsUrl.searchParams.set(key, value); - }); - findingsUrl.searchParams.set("filter[muted]", "false"); - return NextResponse.redirect(findingsUrl); - } - return NextResponse.next(); }); diff --git a/ui/styles/globals.css b/ui/styles/globals.css index e44046cc48..daad4a6985 100644 --- a/ui/styles/globals.css +++ b/ui/styles/globals.css @@ -396,3 +396,10 @@ @apply bg-background text-foreground; } } + +/* Override vaul's injected user-select: none to allow text selection in drawers */ +@media (hover: hover) and (pointer: fine) { + [data-vaul-drawer][data-vaul-drawer] { + user-select: text; + } +} diff --git a/ui/tests/invitations/invitations-page.ts b/ui/tests/invitations/invitations-page.ts index 2a5b2f6826..72d6d4fdc9 100644 --- a/ui/tests/invitations/invitations-page.ts +++ b/ui/tests/invitations/invitations-page.ts @@ -34,7 +34,7 @@ export class InvitationsPage extends BasePage { this.emailInput = page.getByRole("textbox", { name: "Email" }); // Form select - this.roleSelect = page.getByRole("button", { name: /Role|Select a role/i }); + this.roleSelect = page.getByRole("combobox", { name: /Role|Select a role/i }); // Form details this.reviewInvitationDetailsButton = page.getByRole('button', { name: /Review Invitation Details/i }); diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts index 50dcd9c892..b96caa8362 100644 --- a/ui/tests/providers/providers-page.ts +++ b/ui/tests/providers/providers-page.ts @@ -43,6 +43,12 @@ export interface OCIProviderData { alias?: string; } +// AlibabaCloud provider data +export interface AlibabaCloudProviderData { + accountId: string; + alias?: string; +} + // AWS credential options export const AWS_CREDENTIAL_OPTIONS = { AWS_ROLE_ARN: "role", @@ -167,6 +173,25 @@ export interface OCIProviderCredential { region?: string; } +// AlibabaCloud credential options +export const ALIBABACLOUD_CREDENTIAL_OPTIONS = { + ALIBABACLOUD_CREDENTIALS: "credentials", + ALIBABACLOUD_ROLE: "role", +} as const; + +// AlibabaCloud credential type +type AlibabaCloudCredentialType = + (typeof ALIBABACLOUD_CREDENTIAL_OPTIONS)[keyof typeof ALIBABACLOUD_CREDENTIAL_OPTIONS]; + +// AlibabaCloud provider credential +export interface AlibabaCloudProviderCredential { + type: AlibabaCloudCredentialType; + accessKeyId: string; + accessKeySecret: string; + roleArn?: string; + roleSessionName?: string; +} + // Providers page export class ProvidersPage extends BasePage { // Alias input @@ -184,6 +209,7 @@ export class ProvidersPage extends BasePage { readonly kubernetesProviderRadio: Locator; readonly githubProviderRadio: Locator; readonly ociProviderRadio: Locator; + readonly alibabacloudProviderRadio: Locator; // AWS provider form elements readonly accountIdInput: Locator; @@ -247,6 +273,15 @@ export class ProvidersPage extends BasePage { readonly ociKeyContentInput: Locator; readonly ociRegionInput: Locator; + // AlibabaCloud provider form elements + readonly alibabacloudAccountIdInput: Locator; + readonly alibabacloudAccessKeyIdInput: Locator; + readonly alibabacloudAccessKeySecretInput: Locator; + readonly alibabacloudRoleArnInput: Locator; + readonly alibabacloudRoleSessionNameInput: Locator; + readonly alibabacloudStaticCredentialsRadio: Locator; + readonly alibabacloudRoleCredentialsRadio: Locator; + // Delete button readonly deleteProviderConfirmationButton: Locator; @@ -290,6 +325,10 @@ export class ProvidersPage extends BasePage { this.ociProviderRadio = page.getByRole("option", { name: /Oracle Cloud Infrastructure/i, }); + // Alibaba Cloud + this.alibabacloudProviderRadio = page.getByRole("option", { + name: /Alibaba Cloud/i, + }); // AWS provider form inputs this.accountIdInput = page.getByRole("textbox", { name: "Account ID" }); @@ -354,6 +393,30 @@ export class ProvidersPage extends BasePage { }); this.ociRegionInput = page.getByRole("textbox", { name: /Region/i }); + // AlibabaCloud provider form inputs + this.alibabacloudAccountIdInput = page.getByRole("textbox", { + name: "Account ID", + }); + this.alibabacloudAccessKeyIdInput = page.getByRole("textbox", { + name: "Access Key ID", + }); + this.alibabacloudAccessKeySecretInput = page.getByRole("textbox", { + name: "Access Key Secret", + }); + this.alibabacloudRoleArnInput = page.getByRole("textbox", { + name: "Role ARN", + }); + this.alibabacloudRoleSessionNameInput = page.getByRole("textbox", { + name: "Role Session Name", + }); + // Radios for selecting AlibabaCloud credentials method + this.alibabacloudStaticCredentialsRadio = page.getByRole("radio", { + name: /Connect via Access Keys/i, + }); + this.alibabacloudRoleCredentialsRadio = page.getByRole("radio", { + name: /Connect assuming RAM Role/i, + }); + // Alias input this.aliasInput = page.getByRole("textbox", { name: "Provider alias (optional)", @@ -878,6 +941,101 @@ export class ProvidersPage extends BasePage { await expect(this.ociRegionInput).toBeVisible(); } + async selectAlibabaCloudProvider(): Promise { + await this.selectProviderRadio(this.alibabacloudProviderRadio); + } + + async fillAlibabaCloudProviderDetails( + data: AlibabaCloudProviderData, + ): Promise { + // Fill the AlibabaCloud provider details + + await this.alibabacloudAccountIdInput.fill(data.accountId); + + if (data.alias) { + await this.aliasInput.fill(data.alias); + } + } + + async selectAlibabaCloudCredentialsType( + type: AlibabaCloudCredentialType, + ): Promise { + // Ensure we are on the add-credentials page where the selector exists + + await expect(this.page).toHaveURL(/\/providers\/add-credentials/); + + if (type === ALIBABACLOUD_CREDENTIAL_OPTIONS.ALIBABACLOUD_CREDENTIALS) { + await this.alibabacloudStaticCredentialsRadio.click({ force: true }); + } else if (type === ALIBABACLOUD_CREDENTIAL_OPTIONS.ALIBABACLOUD_ROLE) { + await this.alibabacloudRoleCredentialsRadio.click({ force: true }); + } else { + throw new Error(`Invalid AlibabaCloud credential type: ${type}`); + } + } + + async fillAlibabaCloudStaticCredentials( + credentials: AlibabaCloudProviderCredential, + ): Promise { + // Fill the AlibabaCloud static credentials form + + if (credentials.accessKeyId) { + await this.alibabacloudAccessKeyIdInput.fill(credentials.accessKeyId); + } + if (credentials.accessKeySecret) { + await this.alibabacloudAccessKeySecretInput.fill( + credentials.accessKeySecret, + ); + } + } + + async fillAlibabaCloudRoleCredentials( + credentials: AlibabaCloudProviderCredential, + ): Promise { + // Fill the AlibabaCloud RAM Role credentials form + + if (credentials.roleArn) { + await this.alibabacloudRoleArnInput.fill(credentials.roleArn); + } + if (credentials.accessKeyId) { + await this.alibabacloudAccessKeyIdInput.fill(credentials.accessKeyId); + } + if (credentials.accessKeySecret) { + await this.alibabacloudAccessKeySecretInput.fill( + credentials.accessKeySecret, + ); + } + if (credentials.roleSessionName) { + await this.alibabacloudRoleSessionNameInput.fill( + credentials.roleSessionName, + ); + } + } + + async verifyAlibabaCloudCredentialsPageLoaded(): Promise { + // Verify the AlibabaCloud credentials page is loaded + + await this.verifyPageHasProwlerTitle(); + await expect(this.alibabacloudStaticCredentialsRadio).toBeVisible(); + await expect(this.alibabacloudRoleCredentialsRadio).toBeVisible(); + } + + async verifyAlibabaCloudStaticCredentialsPageLoaded(): Promise { + // Verify the AlibabaCloud static credentials page is loaded + + await this.verifyPageHasProwlerTitle(); + await expect(this.alibabacloudAccessKeyIdInput).toBeVisible(); + await expect(this.alibabacloudAccessKeySecretInput).toBeVisible(); + } + + async verifyAlibabaCloudRoleCredentialsPageLoaded(): Promise { + // Verify the AlibabaCloud RAM Role credentials page is loaded + + await this.verifyPageHasProwlerTitle(); + await expect(this.alibabacloudRoleArnInput).toBeVisible(); + await expect(this.alibabacloudAccessKeyIdInput).toBeVisible(); + await expect(this.alibabacloudAccessKeySecretInput).toBeVisible(); + } + async verifyPageLoaded(): Promise { // Verify the providers page is loaded @@ -896,6 +1054,7 @@ export class ProvidersPage extends BasePage { await expect(this.m365ProviderRadio).toBeVisible(); await expect(this.kubernetesProviderRadio).toBeVisible(); await expect(this.githubProviderRadio).toBeVisible(); + await expect(this.alibabacloudProviderRadio).toBeVisible(); } async verifyCredentialsPageLoaded(): Promise { @@ -990,19 +1149,16 @@ export class ProvidersPage extends BasePage { } async selectAuthenticationMethod(method: AWSCredentialType): Promise { - // Select the authentication method + // Select the authentication method (shadcn Select renders as combobox + listbox) - const button = this.page.locator("button").filter({ + const trigger = this.page.locator('[role="combobox"]').filter({ hasText: /AWS SDK Default|Prowler Cloud will assume|Access & Secret Key/i, }); - await button.click(); + await trigger.click(); - const modal = this.page - .locator('[role="dialog"], .modal, [data-testid*="modal"]') - .first(); - - await expect(modal).toBeVisible({ timeout: 10000 }); + const listbox = this.page.getByRole("listbox"); + await expect(listbox).toBeVisible({ timeout: 10000 }); if (method === AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN) { await this.page diff --git a/ui/tests/providers/providers.md b/ui/tests/providers/providers.md index f58d9edc19..d7ff8e1106 100644 --- a/ui/tests/providers/providers.md +++ b/ui/tests/providers/providers.md @@ -766,3 +766,128 @@ - Requires PROVIDER-E2E-012 to be run first to create the OCI provider - This test validates the fix for OCI update credentials form failing silently due to missing provider UID - The provider UID is required for OCI credential validation (tenancy field auto-populated from UID) + +--- + +## Test Case: `PROVIDER-E2E-014` - Add AlibabaCloud Provider with Static Credentials + +**Priority:** `critical` + +**Tags:** + +- type → @e2e, @serial +- feature → @providers +- provider → @alibabacloud + +**Description/Objective:** Validates the complete flow of adding a new Alibaba Cloud provider using static credentials (Access Key ID and Access Key Secret) + +**Preconditions:** + +- Admin user authentication required (admin.auth.setup setup) +- Environment variables configured: E2E_ALIBABACLOUD_ACCOUNT_ID, E2E_ALIBABACLOUD_ACCESS_KEY_ID, E2E_ALIBABACLOUD_ACCESS_KEY_SECRET +- Remove any existing provider with the same Account ID before starting the test +- This test must be run serially and never in parallel with other tests, as it requires the Account ID not to be already registered beforehand. + +### Flow Steps: + +1. Navigate to providers page +2. Click "Add Provider" button +3. Select AlibabaCloud provider type +4. Fill provider details (account ID and alias) +5. Verify AlibabaCloud credentials page is loaded +6. Select static credentials type +7. Verify static credentials page is loaded +8. Fill AlibabaCloud credentials (access key ID and access key secret) +9. Launch initial scan +10. Verify redirect to Scans page +11. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan") + +### Expected Result: + +- AlibabaCloud provider successfully added with static credentials +- Initial scan launched successfully +- User redirected to Scans page +- Scheduled scan appears in Scans table with correct provider and scan name + +### Key verification points: + +- Provider page loads correctly +- Connect account page displays AlibabaCloud option +- Provider details form accepts account ID and alias +- Credentials page loads with credential type selection +- Static credentials page loads with access key ID and access key secret fields +- Static credentials are properly filled in the correct fields +- Launch scan page appears +- Successful redirect to Scans page after scan launch +- Provider exists in Scans table (verified by account ID) +- Scan name field contains "scheduled scan" + +### Notes: + +- Test uses environment variables for AlibabaCloud credentials +- Provider cleanup performed before each test to ensure clean state +- Requires valid Alibaba Cloud account with appropriate permissions +- Static credentials must have sufficient permissions for security scanning + +--- + +## Test Case: `PROVIDER-E2E-015` - Add AlibabaCloud Provider with RAM Role Credentials + +**Priority:** `critical` + +**Tags:** + +- type → @e2e, @serial +- feature → @providers +- provider → @alibabacloud + +**Description/Objective:** Validates the complete flow of adding a new Alibaba Cloud provider using RAM Role credentials (Access Key ID, Access Key Secret, and Role ARN) + +**Preconditions:** + +- Admin user authentication required (admin.auth.setup setup) +- Environment variables configured: E2E_ALIBABACLOUD_ACCOUNT_ID, E2E_ALIBABACLOUD_ACCESS_KEY_ID, E2E_ALIBABACLOUD_ACCESS_KEY_SECRET, E2E_ALIBABACLOUD_ROLE_ARN +- Remove any existing provider with the same Account ID before starting the test +- This test must be run serially and never in parallel with other tests, as it requires the Account ID not to be already registered beforehand. + +### Flow Steps: + +1. Navigate to providers page +2. Click "Add Provider" button +3. Select AlibabaCloud provider type +4. Fill provider details (account ID and alias) +5. Verify AlibabaCloud credentials page is loaded +6. Select RAM Role credentials type +7. Verify RAM Role credentials page is loaded +8. Fill AlibabaCloud RAM Role credentials (access key ID, access key secret, and role ARN) +9. Launch initial scan +10. Verify redirect to Scans page +11. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan") + +### Expected Result: + +- AlibabaCloud provider successfully added with RAM Role credentials +- Initial scan launched successfully +- User redirected to Scans page +- Scheduled scan appears in Scans table with correct provider and scan name + +### Key verification points: + +- Provider page loads correctly +- Connect account page displays AlibabaCloud option +- Provider details form accepts account ID and alias +- Credentials page loads with credential type selection +- RAM Role credentials page loads with access key ID, access key secret, and role ARN fields +- RAM Role credentials are properly filled in the correct fields +- Launch scan page appears +- Successful redirect to Scans page after scan launch +- Provider exists in Scans table (verified by account ID) +- Scan name field contains "scheduled scan" + +### Notes: + +- Test uses environment variables for AlibabaCloud RAM Role credentials +- Provider cleanup performed before each test to ensure clean state +- Requires valid Alibaba Cloud account with RAM Role configured +- RAM Role must have sufficient permissions for security scanning +- Role ARN must be properly configured and assumable diff --git a/ui/tests/providers/providers.spec.ts b/ui/tests/providers/providers.spec.ts index 07da089891..f19b20b454 100644 --- a/ui/tests/providers/providers.spec.ts +++ b/ui/tests/providers/providers.spec.ts @@ -22,6 +22,9 @@ import { OCIProviderData, OCIProviderCredential, OCI_CREDENTIAL_OPTIONS, + AlibabaCloudProviderData, + AlibabaCloudProviderCredential, + ALIBABACLOUD_CREDENTIAL_OPTIONS, } from "./providers-page"; import { ScansPage } from "../scans/scans-page"; import fs from "fs"; @@ -1138,6 +1141,192 @@ test.describe("Add Provider", () => { }, ); }); + + test.describe.serial("Add AlibabaCloud Provider", () => { + // Providers page object + let providersPage: ProvidersPage; + let scansPage: ScansPage; + + // Test data from environment variables + const accountId = process.env.E2E_ALIBABACLOUD_ACCOUNT_ID; + const accessKeyId = process.env.E2E_ALIBABACLOUD_ACCESS_KEY_ID; + const accessKeySecret = process.env.E2E_ALIBABACLOUD_ACCESS_KEY_SECRET; + const roleArn = process.env.E2E_ALIBABACLOUD_ROLE_ARN; + + // Validate required environment variable for beforeEach + if (!accountId) { + throw new Error( + "E2E_ALIBABACLOUD_ACCOUNT_ID environment variable is not set", + ); + } + + // Setup before each test + test.beforeEach(async ({ page }) => { + providersPage = new ProvidersPage(page); + // Clean up existing provider to ensure clean test state + await deleteProviderIfExists(providersPage, accountId); + }); + + // Use admin authentication for provider management + test.use({ storageState: "playwright/.auth/admin_user.json" }); + + test( + "should add a new AlibabaCloud provider with static credentials", + { + tag: [ + "@critical", + "@e2e", + "@providers", + "@alibabacloud", + "@serial", + "@PROVIDER-E2E-014", + ], + }, + async ({ page }) => { + // Validate required environment variables + if (!accessKeyId || !accessKeySecret) { + throw new Error( + "E2E_ALIBABACLOUD_ACCESS_KEY_ID and E2E_ALIBABACLOUD_ACCESS_KEY_SECRET environment variables are not set", + ); + } + + // Prepare test data for AlibabaCloud provider + const alibabacloudProviderData: AlibabaCloudProviderData = { + accountId: accountId, + alias: "Test E2E AlibabaCloud Account - Static Credentials", + }; + + // Prepare static credentials + const staticCredentials: AlibabaCloudProviderCredential = { + type: ALIBABACLOUD_CREDENTIAL_OPTIONS.ALIBABACLOUD_CREDENTIALS, + accessKeyId: accessKeyId, + accessKeySecret: accessKeySecret, + }; + + // Navigate to providers page + await providersPage.goto(); + await providersPage.verifyPageLoaded(); + + // Start adding new provider + await providersPage.clickAddProvider(); + await providersPage.verifyConnectAccountPageLoaded(); + + // Select AlibabaCloud provider + await providersPage.selectAlibabaCloudProvider(); + + // Fill provider details + await providersPage.fillAlibabaCloudProviderDetails( + alibabacloudProviderData, + ); + await providersPage.clickNext(); + + // Verify credentials page is loaded + await providersPage.verifyAlibabaCloudCredentialsPageLoaded(); + + // Select static credentials type + await providersPage.selectAlibabaCloudCredentialsType( + ALIBABACLOUD_CREDENTIAL_OPTIONS.ALIBABACLOUD_CREDENTIALS, + ); + + // Verify static credentials page is loaded + await providersPage.verifyAlibabaCloudStaticCredentialsPageLoaded(); + + // Fill static credentials + await providersPage.fillAlibabaCloudStaticCredentials(staticCredentials); + await providersPage.clickNext(); + + // Launch scan + await providersPage.verifyLaunchScanPageLoaded(); + await providersPage.clickNext(); + + // Wait for redirect to scan page + scansPage = new ScansPage(page); + await scansPage.verifyPageLoaded(); + + // Verify scan status is "Scheduled scan" + await scansPage.verifyScheduledScanStatus(accountId); + }, + ); + + test( + "should add a new AlibabaCloud provider with RAM Role credentials", + { + tag: [ + "@critical", + "@e2e", + "@providers", + "@alibabacloud", + "@serial", + "@PROVIDER-E2E-015", + ], + }, + async ({ page }) => { + // Validate required environment variables + if (!accessKeyId || !accessKeySecret || !roleArn) { + throw new Error( + "E2E_ALIBABACLOUD_ACCESS_KEY_ID, E2E_ALIBABACLOUD_ACCESS_KEY_SECRET, and E2E_ALIBABACLOUD_ROLE_ARN environment variables are not set", + ); + } + + // Prepare test data for AlibabaCloud provider + const alibabacloudProviderData: AlibabaCloudProviderData = { + accountId: accountId, + alias: "Test E2E AlibabaCloud Account - RAM Role Credentials", + }; + + // Prepare RAM Role credentials + const roleCredentials: AlibabaCloudProviderCredential = { + type: ALIBABACLOUD_CREDENTIAL_OPTIONS.ALIBABACLOUD_ROLE, + accessKeyId: accessKeyId, + accessKeySecret: accessKeySecret, + roleArn: roleArn, + }; + + // Navigate to providers page + await providersPage.goto(); + await providersPage.verifyPageLoaded(); + + // Start adding new provider + await providersPage.clickAddProvider(); + await providersPage.verifyConnectAccountPageLoaded(); + + // Select AlibabaCloud provider + await providersPage.selectAlibabaCloudProvider(); + + // Fill provider details + await providersPage.fillAlibabaCloudProviderDetails( + alibabacloudProviderData, + ); + await providersPage.clickNext(); + + // Verify credentials page is loaded + await providersPage.verifyAlibabaCloudCredentialsPageLoaded(); + + // Select RAM Role credentials type + await providersPage.selectAlibabaCloudCredentialsType( + ALIBABACLOUD_CREDENTIAL_OPTIONS.ALIBABACLOUD_ROLE, + ); + + // Verify RAM Role credentials page is loaded + await providersPage.verifyAlibabaCloudRoleCredentialsPageLoaded(); + + // Fill RAM Role credentials + await providersPage.fillAlibabaCloudRoleCredentials(roleCredentials); + await providersPage.clickNext(); + + // Launch scan + await providersPage.verifyLaunchScanPageLoaded(); + await providersPage.clickNext(); + + // Wait for redirect to scan page + scansPage = new ScansPage(page); + await scansPage.verifyPageLoaded(); + + // Verify scan status is "Scheduled scan" + await scansPage.verifyScheduledScanStatus(accountId); + }, + ); + }); }); test.describe("Update Provider Credentials", () => { diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 02037f4ac9..f95fbfb358 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -31,7 +31,11 @@ "target": "es5" }, "exclude": [ - "node_modules" + "node_modules", + "vitest.config.ts", + "vitest.setup.ts", + "**/*.test.ts", + "**/*.test.tsx" ], "include": [ "next-env.d.ts", diff --git a/ui/types/attack-paths.ts b/ui/types/attack-paths.ts index 7bd623e724..33a02e1178 100644 --- a/ui/types/attack-paths.ts +++ b/ui/types/attack-paths.ts @@ -43,6 +43,7 @@ export type ProviderType = (typeof PROVIDER_TYPES)[keyof typeof PROVIDER_TYPES]; export interface AttackPathScanAttributes { state: ScanState; progress: number; + graph_data_ready: boolean; provider_alias: string; provider_type: ProviderType; provider_uid: string; @@ -90,11 +91,18 @@ export interface AttackPathQueryParameter { required?: boolean; } +export interface AttackPathQueryAttribution { + text: string; + link: string; +} + export interface AttackPathQueryAttributes { name: string; + short_description: string; description: string; provider: string; parameters: AttackPathQueryParameter[]; + attribution: AttackPathQueryAttribution | null; } export interface AttackPathQuery { diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts index 5c77ed1ee0..1557671537 100644 --- a/ui/types/compliance.ts +++ b/ui/types/compliance.ts @@ -189,6 +189,18 @@ export interface GenericAttributesMetadata { Type: string | null; } +export interface CSAAttributesMetadata { + Section: string; + CCMLite: string; + IaaS: string; + PaaS: string; + SaaS: string; + ScopeApplicability: Array<{ + ReferenceId: string; + Identifiers: string[]; + }>; +} + export interface CCCAttributesMetadata { FamilyName: string; FamilyDescription: string; @@ -227,6 +239,7 @@ export interface AttributesItemData { | C5AttributesMetadata[] | MITREAttributesMetadata[] | CCCAttributesMetadata[] + | CSAAttributesMetadata[] | GenericAttributesMetadata[]; check_ids: string[]; // MITRE structure diff --git a/ui/types/index.ts b/ui/types/index.ts index 312d9c6e29..badcd6e7f9 100644 --- a/ui/types/index.ts +++ b/ui/types/index.ts @@ -6,3 +6,4 @@ export * from "./processors"; export * from "./providers"; export * from "./resources"; export * from "./scans"; +export * from "./tree"; diff --git a/ui/types/scans.ts b/ui/types/scans.ts index e23cc4d6b0..2cfabf009d 100644 --- a/ui/types/scans.ts +++ b/ui/types/scans.ts @@ -1,93 +1,106 @@ +import { PaginationLinks, RelationshipWrapper } from "./attack-paths"; import { ProviderType } from "./providers"; +export interface ScannerArgs { + only_logs?: boolean; + excluded_checks?: string[]; + aws_retries_max_attempts?: number; +} + +export const SCAN_TRIGGER = { + SCHEDULED: "scheduled", + MANUAL: "manual", +} as const; + +export type ScanTrigger = (typeof SCAN_TRIGGER)[keyof typeof SCAN_TRIGGER]; + +export const SCAN_STATE = { + AVAILABLE: "available", + SCHEDULED: "scheduled", + EXECUTING: "executing", + COMPLETED: "completed", + FAILED: "failed", + CANCELLED: "cancelled", +} as const; + +export type ScanState = (typeof SCAN_STATE)[keyof typeof SCAN_STATE]; + +export interface ScanAttributes { + name: string; + trigger: ScanTrigger; + state: ScanState; + unique_resource_count: number; + progress: number; + scanner_args: ScannerArgs | null; + duration: number; + started_at: string; + inserted_at: string; + completed_at: string; + scheduled_at: string; + next_scan_at: string; +} + +export interface ScanRelationships { + provider: RelationshipWrapper; + task: RelationshipWrapper; +} + +export interface ScanProviderInfo { + provider: ProviderType; + uid: string; + alias: string; +} + export interface ScanProps { type: "scans"; id: string; - attributes: { - name: string; - trigger: "scheduled" | "manual"; - state: - | "available" - | "scheduled" - | "executing" - | "completed" - | "failed" - | "cancelled"; - unique_resource_count: number; - progress: number; - scanner_args: { - only_logs?: boolean; - excluded_checks?: string[]; - aws_retries_max_attempts?: number; - } | null; - duration: number; - started_at: string; - inserted_at: string; - completed_at: string; - scheduled_at: string; - next_scan_at: string; - }; - relationships: { - provider: { - data: { - id: string; - type: "providers"; - }; - }; - task: { - data: { - id: string; - type: "tasks"; - }; - }; - }; - providerInfo?: { - provider: ProviderType; - uid: string; - alias: string; - }; + attributes: ScanAttributes; + relationships: ScanRelationships; + providerInfo?: ScanProviderInfo; +} + +export interface ScanEntityProviderInfo { + provider: ProviderType; + alias?: string; + uid?: string; +} + +export interface ScanEntityAttributes { + name?: string; + completed_at: string; } export interface ScanEntity { id: string; - providerInfo: { - provider: ProviderType; - alias?: string; - uid?: string; - }; - attributes: { - name?: string; - completed_at: string; - }; + providerInfo: ScanEntityProviderInfo; + attributes: ScanEntityAttributes; } + export interface ExpandedScanData extends ScanProps { - providerInfo: { - provider: ProviderType; - uid: string; - alias: string; - }; + providerInfo: ScanProviderInfo; +} + +export interface IncludedResource { + type: string; + id: string; + attributes: any; + relationships?: any; +} + +export interface ApiPagination { + page: number; + pages: number; + count: number; +} + +export interface ScansApiMeta { + pagination: ApiPagination; + version: string; } export interface ScansApiResponse { - links: { - first: string; - last: string; - next: string | null; - prev: string | null; - }; + links: PaginationLinks; data: ScanProps[]; - included?: Array<{ - type: string; - id: string; - attributes: any; - relationships?: any; - }>; - meta: { - pagination: { - page: number; - pages: number; - count: number; - }; - version: string; - }; + included?: IncludedResource[]; + meta: ScansApiMeta; } diff --git a/ui/types/tree.ts b/ui/types/tree.ts new file mode 100644 index 0000000000..ff18bf77c5 --- /dev/null +++ b/ui/types/tree.ts @@ -0,0 +1,114 @@ +/** + * Tree View Component Types + * + * Types for the TreeView component used to render hierarchical data structures + * with support for selection, expansion, and custom rendering. + */ + +/** + * Status indicator for tree items after loading completes + */ +export const TREE_ITEM_STATUS = { + SUCCESS: "success", + ERROR: "error", +} as const; + +export type TreeItemStatus = + (typeof TREE_ITEM_STATUS)[keyof typeof TREE_ITEM_STATUS]; + +/** + * Represents a single item in the tree structure. + * Items can have nested children to create a hierarchical tree. + */ +export interface TreeDataItem { + /** Unique identifier for the tree item */ + id: string; + /** Display name for the item */ + name: string; + /** Optional icon component to render alongside the name */ + icon?: React.ComponentType<{ className?: string }>; + /** Child items (if present, this node is expandable) */ + children?: TreeDataItem[]; + /** Whether the item is disabled (cannot be selected) */ + disabled?: boolean; + /** Whether the item is in a loading state (shows spinner) */ + isLoading?: boolean; + /** Status indicator shown after loading (success/error) */ + status?: TreeItemStatus; + /** Additional CSS classes for the item */ + className?: string; +} + +/** + * Props for the main TreeView component + */ +export interface TreeViewProps { + /** Tree data - can be a single root or array of roots */ + data: TreeDataItem[] | TreeDataItem; + /** Additional CSS classes for the root container */ + className?: string; + /** Controlled selected item IDs */ + selectedIds?: string[]; + /** Callback when selection changes */ + onSelectionChange?: (selectedIds: string[]) => void; + /** Controlled expanded item IDs */ + expandedIds?: string[]; + /** Callback when expansion state changes */ + onExpandedChange?: (expandedIds: string[]) => void; + /** Expand all nodes by default */ + expandAll?: boolean; + /** Show checkboxes for selection */ + showCheckboxes?: boolean; + /** Auto-select children when parent is selected */ + enableSelectChildren?: boolean; + /** Custom render function for each item */ + renderItem?: (params: TreeRenderItemParams) => React.ReactNode; +} + +/** + * Parameters passed to the custom renderItem function + */ +export interface TreeRenderItemParams { + /** The tree item being rendered */ + item: TreeDataItem; + /** Nesting depth level (0 = root) */ + level: number; + /** Whether this is a leaf node (no children) */ + isLeaf: boolean; + /** Whether this item is selected */ + isSelected: boolean; + /** Whether this item is expanded (only for non-leaf nodes) */ + isExpanded?: boolean; + /** Whether this item has partial child selection (indeterminate state) */ + isIndeterminate?: boolean; + /** Whether this item has children */ + hasChildren: boolean; +} + +/** + * Internal props for TreeNode component (expandable nodes) + */ +export interface TreeNodeProps { + item: TreeDataItem; + level: number; + selectedIds: string[]; + expandedIds: string[]; + onSelectionChange: (id: string, item: TreeDataItem) => void; + onExpandedChange: (ids: string[]) => void; + showCheckboxes: boolean; + renderItem?: (params: TreeRenderItemParams) => React.ReactNode; + expandAll: boolean; + enableSelectChildren: boolean; +} + +/** + * Internal props for TreeLeaf component (non-expandable nodes) + */ +export interface TreeLeafProps { + item: TreeDataItem; + level: number; + selectedIds: string[]; + onSelectionChange: (id: string, item: TreeDataItem) => void; + showCheckboxes: boolean; + renderItem?: (params: TreeRenderItemParams) => React.ReactNode; +} diff --git a/ui/vitest.config.ts b/ui/vitest.config.ts new file mode 100644 index 0000000000..d5b791f355 --- /dev/null +++ b/ui/vitest.config.ts @@ -0,0 +1,35 @@ +import react from "@vitejs/plugin-react"; +import path from "path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [react()], + test: { + environment: "jsdom", + globals: true, + setupFiles: ["./vitest.setup.ts"], + include: ["**/*.test.{ts,tsx}"], + exclude: [ + "node_modules", + ".next", + "tests/**/*", // Playwright E2E tests + ], + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + exclude: [ + "node_modules", + ".next", + "tests/**/*", + "**/*.test.{ts,tsx}", + "vitest.config.ts", + "vitest.setup.ts", + ], + }, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./"), + }, + }, +}); diff --git a/ui/vitest.setup.ts b/ui/vitest.setup.ts new file mode 100644 index 0000000000..f149f27ae4 --- /dev/null +++ b/ui/vitest.setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/util/update_oci_regions.py b/util/update_oci_regions.py new file mode 100644 index 0000000000..1d778b777a --- /dev/null +++ b/util/update_oci_regions.py @@ -0,0 +1,209 @@ +import base64 +import logging +import os +import re +import sys + +import oci + +# Logging config +logging.basicConfig( + stream=sys.stdout, + format="%(asctime)s [File: %(filename)s:%(lineno)d] \t[Module: %(module)s]\t %(levelname)s: %(message)s", + datefmt="%m/%d/%Y %I:%M:%S %p", + level=logging.INFO, +) + +# OCI regions are fetched dynamically from the Identity API +# No hardcoded display names - following AWS regions updater pattern +# This ensures full automation without manual maintenance + + +def setup_oci_client(): + """ + Set up OCI Identity client using credentials from environment variables. + + Returns: + oci.identity.IdentityClient: Authenticated OCI Identity client + + Raises: + ValueError: If required environment variables are missing + Exception: If authentication fails + """ + logging.info("Setting up OCI client authentication") + + # Validate required environment variables + required_vars = [ + "OCI_CLI_USER", + "OCI_CLI_FINGERPRINT", + "OCI_CLI_TENANCY", + "OCI_CLI_KEY_CONTENT", + ] + + missing_vars = [var for var in required_vars if not os.getenv(var)] + if missing_vars: + raise ValueError( + f"Missing required environment variables: {', '.join(missing_vars)}" + ) + + # Get credentials from environment + user_ocid = os.getenv("OCI_CLI_USER") + fingerprint = os.getenv("OCI_CLI_FINGERPRINT") + tenancy_ocid = os.getenv("OCI_CLI_TENANCY") + key_content_raw = os.getenv("OCI_CLI_KEY_CONTENT") + region = os.getenv("OCI_CLI_REGION", "us-ashburn-1") + + # Decode private key: the secret is stored base64-encoded (same format + # the UI/API use). If it's already raw PEM, use it directly. + if key_content_raw.strip().startswith("-----BEGIN"): + key_content = key_content_raw + else: + try: + key_content = base64.b64decode(key_content_raw).decode("utf-8") + except Exception as e: + raise ValueError(f"Failed to decode OCI_CLI_KEY_CONTENT: {e}") + + # Create OCI config dictionary + config = { + "user": user_ocid, + "fingerprint": fingerprint, + "tenancy": tenancy_ocid, + "key_content": key_content, + "region": region, + } + + # Initialize Identity client + try: + identity_client = oci.identity.IdentityClient(config) + logging.info(f"Successfully authenticated to OCI region: {region}") + return identity_client + except Exception as e: + raise Exception(f"Failed to create OCI Identity client: {e}") + + +def fetch_oci_regions(identity_client): + """ + Fetch all OCI commercial regions using the Identity service API. + + Args: + identity_client (oci.identity.IdentityClient): Authenticated OCI client + + Returns: + dict: Dictionary mapping region identifiers to themselves (for consistency) + + Raises: + Exception: If API call fails + """ + logging.info("Fetching OCI commercial regions from Identity service") + + try: + # Call list_regions() API - returns all OC1 commercial regions + response = identity_client.list_regions() + regions = response.data + + logging.info(f"Successfully fetched {len(regions)} regions from OCI API") + + # Build region dictionary (region_id -> region_id) + # Following AWS pattern: no display names, fully automated + region_dict = {} + for region in regions: + region_id = region.name + region_dict[region_id] = region_id + logging.debug(f"Added region: {region_id}") + + # Sort regions alphabetically by key + region_dict = dict(sorted(region_dict.items())) + + logging.info(f"Processed {len(region_dict)} commercial regions") + return region_dict + + except Exception as e: + raise Exception(f"Failed to fetch OCI regions: {e}") + + +def update_config_file(regions, config_file_path): + """ + Update the OCI config file with new commercial regions while preserving government regions. + + Args: + regions (dict): Dictionary of region identifiers to region identifiers + config_file_path (str): Path to the config.py file + + Raises: + Exception: If file operations fail or validation fails + """ + logging.info(f"Updating config file: {config_file_path}") + + # Read current config file + try: + with open(config_file_path, "r") as f: + config_content = f.read() + except Exception as e: + raise Exception(f"Failed to read config file: {e}") + + # Generate new OCI_COMMERCIAL_REGIONS dictionary + new_regions_dict = "OCI_COMMERCIAL_REGIONS = {\n" + for region_id in regions.keys(): + new_regions_dict += f' "{region_id}": "{region_id}",\n' + new_regions_dict += "}" + + # Replace OCI_COMMERCIAL_REGIONS using regex + pattern = r"OCI_COMMERCIAL_REGIONS\s*=\s*\{[^}]*\}" + updated_content = re.sub(pattern, new_regions_dict, config_content) + + # Validate that government regions still exist + if "OCI_GOVERNMENT_REGIONS" not in updated_content: + raise Exception( + "Validation failed: OCI_GOVERNMENT_REGIONS section missing after update. Aborting to prevent data loss." + ) + + # Verify the replacement was successful + if updated_content == config_content: + logging.warning("No changes detected in config file") + return + + # Write updated content back to file + try: + with open(config_file_path, "w") as f: + f.write(updated_content) + logging.info("Successfully updated config file") + except Exception as e: + raise Exception(f"Failed to write updated config file: {e}") + + # Log summary of changes + logging.info(f"Updated OCI_COMMERCIAL_REGIONS with {len(regions)} regions") + + +def main(): + """ + Main execution function for OCI regions updater. + """ + try: + # Setup OCI client with authentication + identity_client = setup_oci_client() + + # Fetch all commercial regions from OCI API + commercial_regions = fetch_oci_regions(identity_client) + + # Update config.py file + config_file_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "..", + "prowler", + "providers", + "oraclecloud", + "config.py", + ) + + update_config_file(commercial_regions, config_file_path) + + logging.info("OCI regions update completed successfully") + return 0 + + except Exception as e: + logging.error(f"Error during OCI regions update: {e}") + return 1 + + +if __name__ == "__main__": + sys.exit(main())