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..e013329e32 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,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: Pin prowler SDK to latest master commit + if: github.event_name == 'push' + run: | + LATEST_SHA=$(git ls-remote https://github.com/prowler-cloud/prowler.git refs/heads/master | cut -f1) + sed -i "s|prowler-cloud/prowler.git@master|prowler-cloud/prowler.git@${LATEST_SHA}|" api/pyproject.toml + - name: Login to DockerHub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -111,7 +117,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 +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 }} @@ -161,7 +167,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 +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/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/helm-chart-checks.yml b/.github/workflows/helm-chart-checks.yml new file mode 100644 index 0000000000..b1eec36fb5 --- /dev/null +++ b/.github/workflows/helm-chart-checks.yml @@ -0,0 +1,48 @@ +name: 'Helm: Chart Checks' +# DISCLAIMER: This workflow is not maintained by the Prowler team. Refer to contrib/k8s/helm/prowler-app for the source code. +on: + push: + branches: + - 'master' + - 'v5.*' + paths: + - 'contrib/k8s/helm/prowler-app/**' + pull_request: + branches: + - 'master' + - 'v5.*' + paths: + - 'contrib/k8s/helm/prowler-app/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CHART_PATH: contrib/k8s/helm/prowler-app + +jobs: + helm-lint: + if: github.repository == 'prowler-cloud/prowler' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: Set up Helm + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 + + - name: Update chart dependencies + run: helm dependency update ${{ env.CHART_PATH }} + + - name: Lint Helm chart + run: helm lint ${{ env.CHART_PATH }} + + - name: Validate Helm chart template rendering + run: helm template prowler ${{ env.CHART_PATH }} diff --git a/.github/workflows/helm-chart-release.yml b/.github/workflows/helm-chart-release.yml new file mode 100644 index 0000000000..960e611f43 --- /dev/null +++ b/.github/workflows/helm-chart-release.yml @@ -0,0 +1,54 @@ +name: 'Helm: Chart Release' +# DISCLAIMER: This workflow is not maintained by the Prowler team. Refer to contrib/k8s/helm/prowler-app for the source code. + +on: + release: + types: + - 'published' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + CHART_PATH: contrib/k8s/helm/prowler-app + +jobs: + release-helm-chart: + if: github.repository == 'prowler-cloud/prowler' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: Set up Helm + uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0 + + - name: Set appVersion from release tag + run: | + RELEASE_TAG="${GITHUB_EVENT_RELEASE_TAG_NAME}" + echo "Setting appVersion to ${RELEASE_TAG}" + sed -i "s/^appVersion:.*/appVersion: \"${RELEASE_TAG}\"/" ${{ env.CHART_PATH }}/Chart.yaml + env: + GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }} + + - name: Login to GHCR + run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${GITHUB_ACTOR} --password-stdin + + - name: Update chart dependencies + run: helm dependency update ${{ env.CHART_PATH }} + + - name: Package Helm chart + run: helm package ${{ env.CHART_PATH }} --destination .helm-packages + + - name: Push chart to GHCR + run: | + PACKAGE=$(ls .helm-packages/*.tgz) + helm push "$PACKAGE" oci://ghcr.io/${{ github.repository_owner }}/charts 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/README.md b/README.md index 3d1589f6cd..79b274e05b 100644 --- a/README.md +++ b/README.md @@ -109,14 +109,16 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically | GCP | 100 | 13 | 15 | 11 | Official | UI, API, CLI | | Kubernetes | 83 | 7 | 7 | 9 | Official | UI, API, CLI | | GitHub | 21 | 2 | 1 | 2 | Official | UI, API, CLI | -| M365 | 75 | 7 | 4 | 4 | Official | UI, API, CLI | -| OCI | 51 | 13 | 3 | 12 | Official | UI, API, CLI | +| M365 | 89 | 9 | 4 | 5 | Official | UI, API, CLI | +| OCI | 48 | 13 | 3 | 10 | Official | UI, API, CLI | | Alibaba Cloud | 61 | 9 | 3 | 9 | Official | UI, API, CLI | -| Cloudflare | 29 | 2 | 0 | 5 | Official | CLI, API | +| Cloudflare | 29 | 2 | 0 | 5 | Official | UI, API, CLI | | IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | UI, API, CLI | -| MongoDB Atlas | 10 | 3 | 0 | 3 | Official | UI, API, CLI | +| MongoDB Atlas | 10 | 3 | 0 | 8 | Official | UI, API, CLI | | LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | CLI | -| OpenStack | 1 | 1 | 0 | 2 | Official | CLI | +| Image | N/A | N/A | N/A | N/A | Official | CLI, API | +| Google Workspace | 1 | 1 | 0 | 1 | Official | CLI | +| OpenStack | 27 | 4 | 0 | 8 | Official | UI, API, CLI | | NHN | 6 | 2 | 1 | 0 | Unofficial | CLI | > [!Note] 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 22da548d71..e09b601197 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -2,6 +2,66 @@ All notable changes to the **Prowler API** are documented in this file. +## [1.23.0] (Prowler UNRELEASED) + +### 🐞 Fixed + +- Finding groups latest endpoint now aggregates the latest snapshot per provider before check-level totals, keeping impacted resources aligned across providers [(#10419)](https://github.com/prowler-cloud/prowler/pull/10419) +- Mute rule creation now triggers finding-group summary re-aggregation after historical muting, keeping stats in sync after mute operations [(#10419)](https://github.com/prowler-cloud/prowler/pull/10419) + +### 🔐 Security + +- Replace stdlib XML parser with `defusedxml` in SAML metadata parsing to prevent XML bomb (billion laughs) DoS attacks [(#10165)](https://github.com/prowler-cloud/prowler/pull/10165) + +--- + +## [1.22.1] (Prowler v5.21.1) + +### 🐞 Fixed + +- Threat score aggregation query to eliminate unnecessary JOINs and `COUNT(DISTINCT)` overhead [(#10394)](https://github.com/prowler-cloud/prowler/pull/10394) + +--- + +## [1.22.0] (Prowler v5.21.0) + +### 🚀 Added + +- `CORS_ALLOWED_ORIGINS` configurable via environment variable [(#10355)](https://github.com/prowler-cloud/prowler/pull/10355) +- Finding groups support `check_title` substring filtering [(#10377)](https://github.com/prowler-cloud/prowler/pull/10377) +- 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) +- Replace stdlib XML parser with `defusedxml` in SAML metadata parsing to prevent XML bomb (billion laughs) DoS attacks [(#10165)](https://github.com/prowler-cloud/prowler/pull/10165) + +--- + +## [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) + +### 🐞 Fixed + +- 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) + +--- + ## [1.20.0] (Prowler v5.19.0) ### 🚀 Added @@ -11,6 +71,7 @@ All notable changes to the **Prowler API** are documented in this file. - PDF report for the CSA CCM compliance framework [(#10088)](https://github.com/prowler-cloud/prowler/pull/10088) - `image` provider support for container image scanning [(#10128)](https://github.com/prowler-cloud/prowler/pull/10128) - Attack Paths: Custom query and Cartography schema endpoints (temporarily blocked) [(#10149)](https://github.com/prowler-cloud/prowler/pull/10149) +- `googleworkspace` provider support [(#10247)](https://github.com/prowler-cloud/prowler/pull/10247) ### 🔄 Changed 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..0b0da9c853 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "about-time" @@ -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" @@ -2699,6 +2699,18 @@ files = [ {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "deprecated" version = "1.3.1" @@ -2971,7 +2983,7 @@ files = [ [package.dependencies] autopep8 = "*" Django = ">=4.2" -gprof2dot = ">=2017.09.19" +gprof2dot = ">=2017.9.19" sqlparse = "*" [[package]] @@ -3095,21 +3107,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" @@ -4594,7 +4591,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -4802,7 +4799,7 @@ librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] mongodb = ["pymongo (==4.15.3)"] msgpack = ["msgpack (==1.1.2)"] pyro = ["pyro4 (==4.82)"] -qpid = ["qpid-python (==1.36.0-1)", "qpid-tools (==1.36.0-1)"] +qpid = ["qpid-python (==1.36.0.post1)", "qpid-tools (==1.36.0.post1)"] redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2,<6.5)"] slmq = ["softlayer_messaging (>=1.0.3)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] @@ -4823,7 +4820,7 @@ files = [ ] [package.dependencies] -certifi = ">=14.05.14" +certifi = ">=14.5.14" durationpy = ">=0.7" google-auth = ">=1.0.1" oauthlib = ">=3.2.2" @@ -6745,7 +6742,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" @@ -7185,7 +7182,7 @@ files = [ ] [package.dependencies] -astroid = ">=3.2.2,<=3.3.0-dev0" +astroid = ">=3.2.2,<=3.3.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, @@ -8199,10 +8196,10 @@ files = [ ] [package.dependencies] -botocore = ">=1.37.4,<2.0a.0" +botocore = ">=1.37.4,<2.0a0" [package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] [[package]] name = "safety" @@ -9397,4 +9394,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "42759b370c9e38da727e73f9d8ec0fa61bc6137eab18f11ccd7deff79a0dee69" +content-hash = "2ed5b4e47d81da81963814f21702220ac5619f50cd605fd779be53c8c46ffca5" diff --git a/api/pyproject.toml b/api/pyproject.toml index 05c24bb807..3a89f52e05 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "drf-nested-routers (>=0.94.1,<1.0.0)", "drf-spectacular==0.27.2", "drf-spectacular-jsonapi==0.5.1", + "defusedxml==0.7.1", "gunicorn==23.0.0", "lxml==5.3.2", "prowler @ git+https://github.com/prowler-cloud/prowler.git@master", @@ -37,7 +38,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 +50,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 a54bd664ca..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,8 +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""" - CALL apoc.create.vNode(['Internet'], {{id: 'Internet', name: 'Internet', provider_id: $provider_id}}) - YIELD node AS internet + 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) @@ -32,8 +31,7 @@ AWS_INTERNET_EXPOSED_EC2_SENSITIVE_S3_ACCESS = AttackPathsQueryDefinition( MATCH path_assume_role = (ec2)-[p:STS_ASSUMEROLE_ALLOW*1..9]-(r:AWSRole) - CALL apoc.create.vRelationship(internet, 'CAN_ACCESS', {{provider_id: $provider_id}}, ec2) - YIELD rel AS can_access + OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(ec2) UNWIND nodes(path_s3) + nodes(path_ec2) + nodes(path_role) + nodes(path_assume_role) as n OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) @@ -181,14 +179,12 @@ AWS_EC2_INSTANCES_INTERNET_EXPOSED = AttackPathsQueryDefinition( description="Find EC2 instances flagged as exposed to the internet within the selected account.", provider="aws", cypher=f""" - CALL apoc.create.vNode(['Internet'], {{id: 'Internet', name: 'Internet', provider_id: $provider_id}}) - YIELD node AS internet + OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}}) MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(ec2:EC2Instance) WHERE ec2.exposed_internet = true - CALL apoc.create.vRelationship(internet, 'CAN_ACCESS', {{provider_id: $provider_id}}, ec2) - YIELD rel AS can_access + OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(ec2) UNWIND nodes(path) as n OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) @@ -205,16 +201,14 @@ 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""" - CALL apoc.create.vNode(['Internet'], {{id: 'Internet', name: 'Internet', provider_id: $provider_id}}) - YIELD node AS internet + 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) WHERE ec2.exposed_internet = true AND ir.range = "0.0.0.0/0" - CALL apoc.create.vRelationship(internet, 'CAN_ACCESS', {{provider_id: $provider_id}}, ec2) - YIELD rel AS can_access + OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(ec2) UNWIND nodes(path_ec2) as n OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) @@ -231,14 +225,12 @@ AWS_CLASSIC_ELB_INTERNET_EXPOSED = AttackPathsQueryDefinition( description="Find Classic Load Balancers exposed to the internet along with their listeners.", provider="aws", cypher=f""" - CALL apoc.create.vNode(['Internet'], {{id: 'Internet', name: 'Internet', provider_id: $provider_id}}) - YIELD node AS internet + 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 - CALL apoc.create.vRelationship(internet, 'CAN_ACCESS', {{provider_id: $provider_id}}, elb) - YIELD rel AS can_access + OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(elb) UNWIND nodes(path) as n OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) @@ -255,14 +247,12 @@ AWS_ELBV2_INTERNET_EXPOSED = AttackPathsQueryDefinition( description="Find ELBv2 load balancers exposed to the internet along with their listeners.", provider="aws", cypher=f""" - CALL apoc.create.vNode(['Internet'], {{id: 'Internet', name: 'Internet', provider_id: $provider_id}}) - YIELD node AS internet + 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 - CALL apoc.create.vRelationship(internet, 'CAN_ACCESS', {{provider_id: $provider_id}}, elbv2) - YIELD rel AS can_access + OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(elbv2) UNWIND nodes(path) as n OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) @@ -279,31 +269,15 @@ 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""" - CALL apoc.create.vNode(['Internet'], {{id: 'Internet', name: 'Internet', provider_id: $provider_id}}) - YIELD node AS internet + OPTIONAL MATCH (internet:Internet {{{PROVIDER_ID_PROPERTY}: $provider_id}}) - CALL () {{ - MATCH path = (aws:AWSAccount {{id: $provider_uid}})-[r]-(x:EC2PrivateIp)-[q]-(y) - WHERE x.public_ip = $ip - RETURN path, x + 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) - UNION MATCH path = (aws:AWSAccount {{id: $provider_uid}})-[r]-(x:EC2Instance)-[q]-(y) - WHERE x.publicipaddress = $ip - RETURN path, x - - UNION MATCH path = (aws:AWSAccount {{id: $provider_uid}})-[r]-(x:NetworkInterface)-[q]-(y) - WHERE x.public_ip = $ip - RETURN path, x - - UNION MATCH path = (aws:AWSAccount {{id: $provider_uid}})-[r]-(x:ElasticIPAddress)-[q]-(y) - WHERE x.public_ip = $ip - RETURN path, x - }} - - WITH path, x, internet - - CALL apoc.create.vRelationship(internet, 'CAN_ACCESS', {{provider_id: $provider_id}}, x) - YIELD rel AS can_access + OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(x) UNWIND nodes(path) as n OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) 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/filters.py b/api/src/backend/api/filters.py index a64cc0ea13..d8b803d475 100644 --- a/api/src/backend/api/filters.py +++ b/api/src/backend/api/filters.py @@ -926,6 +926,9 @@ class FindingGroupSummaryFilter(FilterSet): check_id = CharFilter(field_name="check_id", lookup_expr="exact") check_id__in = CharInFilter(field_name="check_id", lookup_expr="in") check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains") + check_title__icontains = CharFilter( + field_name="check_title", lookup_expr="icontains" + ) # Provider filters provider_id = UUIDFilter(field_name="provider_id", lookup_expr="exact") @@ -1025,6 +1028,9 @@ class LatestFindingGroupSummaryFilter(FilterSet): check_id = CharFilter(field_name="check_id", lookup_expr="exact") check_id__in = CharInFilter(field_name="check_id", lookup_expr="in") check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains") + check_title__icontains = CharFilter( + field_name="check_title", lookup_expr="icontains" + ) # Provider filters provider_id = UUIDFilter(field_name="provider_id", lookup_expr="exact") diff --git a/api/src/backend/api/migrations/0084_googleworkspace_provider.py b/api/src/backend/api/migrations/0084_googleworkspace_provider.py new file mode 100644 index 0000000000..eb704bb6b5 --- /dev/null +++ b/api/src/backend/api/migrations/0084_googleworkspace_provider.py @@ -0,0 +1,39 @@ +from django.db import migrations + +import api.db_utils + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0083_image_provider"), + ] + + operations = [ + migrations.AlterField( + model_name="provider", + name="provider", + field=api.db_utils.ProviderEnumField( + choices=[ + ("aws", "AWS"), + ("azure", "Azure"), + ("gcp", "GCP"), + ("kubernetes", "Kubernetes"), + ("m365", "M365"), + ("github", "GitHub"), + ("mongodbatlas", "MongoDB Atlas"), + ("iac", "IaC"), + ("oraclecloud", "Oracle Cloud Infrastructure"), + ("alibabacloud", "Alibaba Cloud"), + ("cloudflare", "Cloudflare"), + ("openstack", "OpenStack"), + ("image", "Image"), + ("googleworkspace", "Google Workspace"), + ], + default="aws", + ), + ), + migrations.RunSQL( + "ALTER TYPE provider ADD VALUE IF NOT EXISTS 'googleworkspace';", + reverse_sql=migrations.RunSQL.noop, + ), + ] 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 1f82c4a7da..78bfec5262 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -1,7 +1,6 @@ import json import logging import re -import xml.etree.ElementTree as ET from datetime import datetime, timedelta, timezone from uuid import UUID, uuid4 @@ -9,6 +8,8 @@ from allauth.socialaccount.models import SocialApp from config.custom_logging import BackendLogger from config.settings.social_login import SOCIALACCOUNT_PROVIDERS from cryptography.fernet import Fernet, InvalidToken +import defusedxml +from defusedxml import ElementTree as ET from django.conf import settings from django.contrib.auth.models import AbstractBaseUser from django.contrib.postgres.fields import ArrayField @@ -293,6 +294,7 @@ class Provider(RowLevelSecurityProtectedModel): CLOUDFLARE = "cloudflare", _("Cloudflare") OPENSTACK = "openstack", _("OpenStack") IMAGE = "image", _("Image") + GOOGLEWORKSPACE = "googleworkspace", _("Google Workspace") @staticmethod def validate_aws_uid(value): @@ -342,6 +344,15 @@ class Provider(RowLevelSecurityProtectedModel): pointer="/data/attributes/uid", ) + @staticmethod + def validate_googleworkspace_uid(value): + if not re.match(r"^C[0-9a-zA-Z]+$", value): + raise ModelValidationError( + detail="Google Workspace Customer ID must start with 'C' followed by one or more alphanumeric characters (e.g., C01234abc, C12345678).", + code="googleworkspace-uid", + pointer="/data/attributes/uid", + ) + @staticmethod def validate_kubernetes_uid(value): if not re.match( @@ -1773,6 +1784,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: @@ -2048,6 +2068,8 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel): root = ET.fromstring(self.metadata_xml) except ET.ParseError as e: raise ValidationError({"metadata_xml": f"Invalid XML: {e}"}) + except defusedxml.DefusedXmlException as e: + raise ValidationError({"metadata_xml": f"Unsafe XML content rejected: {e}"}) # Entity ID entity_id = root.attrib.get("entityID") diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 0cabb2becf..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. @@ -356,7 +356,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -364,6 +364,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -385,13 +386,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -399,6 +401,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -422,6 +425,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -723,7 +727,7 @@ paths: $ref: '#/components/schemas/OpenApiResponseResponse' text/plain: schema: - type: string + $ref: '#/components/schemas/AttackPathsQueryResult' description: '' '403': description: Read-only queries are enforced @@ -769,7 +773,7 @@ paths: $ref: '#/components/schemas/OpenApiResponseResponse' text/plain: schema: - type: string + $ref: '#/components/schemas/AttackPathsQueryResult' description: '' '400': description: Bad request (e.g., Unknown Attack Paths query for the selected @@ -1331,7 +1335,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -1339,6 +1343,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -1360,6 +1365,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: @@ -1805,7 +1811,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -1813,6 +1819,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -1834,13 +1841,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -1848,6 +1856,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -1871,6 +1880,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -2403,7 +2413,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -2411,6 +2421,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -2432,13 +2443,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -2446,6 +2458,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -2469,6 +2482,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -2909,7 +2923,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -2917,6 +2931,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -2938,13 +2953,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -2952,6 +2968,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -2975,6 +2992,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -3413,7 +3431,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -3421,6 +3439,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -3442,13 +3461,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -3456,6 +3476,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -3479,6 +3500,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -3905,7 +3927,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -3913,6 +3935,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -3934,13 +3957,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -3948,6 +3972,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -3971,6 +3996,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -5738,7 +5764,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -5746,6 +5772,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -5767,13 +5794,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -5781,6 +5809,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -5804,6 +5833,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - name: filter[search] @@ -5909,7 +5939,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -5917,6 +5947,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -5938,13 +5969,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -5952,6 +5984,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -5975,6 +6008,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - name: filter[search] @@ -6077,6 +6111,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -6098,6 +6133,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: @@ -6111,6 +6147,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -6134,6 +6171,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - name: filter[search] @@ -6260,7 +6298,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -6268,6 +6306,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -6289,13 +6328,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -6303,6 +6343,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -6326,6 +6367,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -6465,7 +6507,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -6473,6 +6515,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -6494,13 +6537,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -6508,6 +6552,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -6531,6 +6576,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -6672,6 +6718,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -6693,6 +6740,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: @@ -6706,6 +6754,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -6729,6 +6778,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - name: filter[search] @@ -6904,7 +6954,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -6912,6 +6962,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -6933,13 +6984,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -6947,6 +6999,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -6970,6 +7023,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -7074,7 +7128,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -7082,6 +7136,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -7103,13 +7158,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -7117,6 +7173,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -7140,6 +7197,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -7268,7 +7326,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -7276,6 +7334,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -7297,13 +7356,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -7311,6 +7371,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -7334,6 +7395,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -8103,7 +8165,7 @@ paths: name: filter[provider] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -8111,6 +8173,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -8132,13 +8195,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -8146,6 +8210,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -8169,13 +8234,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -8183,6 +8249,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -8204,13 +8271,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -8218,6 +8286,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -8241,6 +8310,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - name: filter[search] @@ -8894,7 +8964,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -8902,6 +8972,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -8923,13 +8994,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -8937,6 +9009,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -8960,6 +9033,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -9437,7 +9511,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -9445,6 +9519,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -9466,13 +9541,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -9480,6 +9556,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -9503,6 +9580,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -9793,7 +9871,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -9801,6 +9879,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -9822,13 +9901,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -9836,6 +9916,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -9859,6 +9940,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -10155,7 +10237,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -10163,6 +10245,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -10184,13 +10267,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -10198,6 +10282,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -10221,6 +10306,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -11027,7 +11113,7 @@ paths: name: filter[provider_type] schema: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -11035,6 +11121,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -11056,13 +11143,14 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace - in: query name: filter[provider_type__in] schema: type: array items: type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 enum: - alibabacloud - aws @@ -11070,6 +11158,7 @@ paths: - cloudflare - gcp - github + - googleworkspace - iac - image - kubernetes @@ -11093,6 +11182,7 @@ paths: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace explode: false style: form - in: query @@ -18189,6 +18279,22 @@ components: description: The service account key for GCP. required: - service_account_key + - type: object + title: Google Workspace Service Account + properties: + credentials_content: + type: string + description: The service account JSON credentials content + for Google Workspace API access with domain-wide delegation + enabled. + delegated_user: + type: string + format: email + description: The email address of the Google Workspace super + admin user to impersonate for domain-wide delegation. + required: + - credentials_content + - delegated_user - type: object title: Kubernetes Static Credentials properties: @@ -19358,6 +19464,7 @@ components: - cloudflare - openstack - image + - googleworkspace type: string description: |- * `aws` - AWS @@ -19373,7 +19480,8 @@ components: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image - x-spec-enum-id: 7ae539a3796eb30a + * `googleworkspace` - Google Workspace + x-spec-enum-id: c0d56cad8ab9abe5 uid: type: string title: Unique identifier for the provider, set by the provider @@ -19492,8 +19600,9 @@ components: - cloudflare - openstack - image + - googleworkspace type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 description: |- Type of provider to create. @@ -19510,6 +19619,7 @@ components: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace uid: type: string title: Unique identifier for the provider, set by the provider @@ -19560,8 +19670,9 @@ components: - cloudflare - openstack - image + - googleworkspace type: string - x-spec-enum-id: 7ae539a3796eb30a + x-spec-enum-id: c0d56cad8ab9abe5 description: |- Type of provider to create. @@ -19578,6 +19689,7 @@ components: * `cloudflare` - Cloudflare * `openstack` - OpenStack * `image` - Image + * `googleworkspace` - Google Workspace uid: type: string minLength: 3 @@ -20249,6 +20361,21 @@ components: description: The service account key for GCP. required: - service_account_key + - type: object + title: Google Workspace Service Account + properties: + credentials_content: + type: string + description: The service account JSON credentials content for + Google Workspace API access with domain-wide delegation enabled. + delegated_user: + type: string + format: email + description: The email address of the Google Workspace super admin + user to impersonate for domain-wide delegation. + required: + - credentials_content + - delegated_user - type: object title: Kubernetes Static Credentials properties: @@ -20644,6 +20771,22 @@ components: description: The service account key for GCP. required: - service_account_key + - type: object + title: Google Workspace Service Account + properties: + credentials_content: + type: string + description: The service account JSON credentials content + for Google Workspace API access with domain-wide delegation + enabled. + delegated_user: + type: string + format: email + description: The email address of the Google Workspace super + admin user to impersonate for domain-wide delegation. + required: + - credentials_content + - delegated_user - type: object title: Kubernetes Static Credentials properties: @@ -21060,6 +21203,21 @@ components: description: The service account key for GCP. required: - service_account_key + - type: object + title: Google Workspace Service Account + properties: + credentials_content: + type: string + description: The service account JSON credentials content for + Google Workspace API access with domain-wide delegation enabled. + delegated_user: + type: string + format: email + description: The email address of the Google Workspace super admin + user to impersonate for domain-wide delegation. + required: + - credentials_content + - delegated_user - type: object title: Kubernetes Static Credentials properties: diff --git a/api/src/backend/api/tests/integration/test_authentication.py b/api/src/backend/api/tests/integration/test_authentication.py index 2abf725124..26d5b8e108 100644 --- a/api/src/backend/api/tests/integration/test_authentication.py +++ b/api/src/backend/api/tests/integration/test_authentication.py @@ -301,7 +301,7 @@ class TestTokenSwitchTenant: assert invalid_tenant_response.status_code == 400 assert invalid_tenant_response.json()["errors"][0]["code"] == "invalid" assert invalid_tenant_response.json()["errors"][0]["detail"] == ( - "Tenant does not exist or user is not a " "member." + "Tenant does not exist or user is not a member." ) @@ -912,10 +912,9 @@ class TestAPIKeyLifecycle: auth_response = client.get(reverse("provider-list"), headers=api_key_headers) # Must return 401 Unauthorized, not 500 Internal Server Error - assert auth_response.status_code == 401, ( - f"Expected 401 but got {auth_response.status_code}: " - f"{auth_response.json()}" - ) + assert ( + auth_response.status_code == 401 + ), f"Expected 401 but got {auth_response.status_code}: {auth_response.json()}" # Verify error message is present response_json = auth_response.json() diff --git a/api/src/backend/api/tests/test_apps.py b/api/src/backend/api/tests/test_apps.py index 712bc33882..5889b4e2cb 100644 --- a/api/src/backend/api/tests/test_apps.py +++ b/api/src/backend/api/tests/test_apps.py @@ -10,11 +10,11 @@ from django.conf import settings import api import api.apps as api_apps_module from api.apps import ( - ApiConfig, PRIVATE_KEY_FILE, PUBLIC_KEY_FILE, SIGNING_KEY_ENV, VERIFYING_KEY_ENV, + ApiConfig, ) @@ -187,9 +187,10 @@ def test_ready_initializes_driver_for_api_process(monkeypatch): _set_argv(monkeypatch, ["gunicorn"]) _set_testing(monkeypatch, False) - with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch( - "api.attack_paths.database.init_driver" - ) as init_driver: + with ( + patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), + patch("api.attack_paths.database.init_driver") as init_driver, + ): config.ready() init_driver.assert_called_once() @@ -200,9 +201,10 @@ def test_ready_skips_driver_for_celery(monkeypatch): _set_argv(monkeypatch, ["celery", "-A", "api"]) _set_testing(monkeypatch, False) - with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch( - "api.attack_paths.database.init_driver" - ) as init_driver: + with ( + patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), + patch("api.attack_paths.database.init_driver") as init_driver, + ): config.ready() init_driver.assert_not_called() @@ -213,9 +215,10 @@ def test_ready_skips_driver_for_manage_py_skip_command(monkeypatch): _set_argv(monkeypatch, ["manage.py", "migrate"]) _set_testing(monkeypatch, False) - with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch( - "api.attack_paths.database.init_driver" - ) as init_driver: + with ( + patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), + patch("api.attack_paths.database.init_driver") as init_driver, + ): config.ready() init_driver.assert_not_called() @@ -226,9 +229,10 @@ def test_ready_skips_driver_when_testing(monkeypatch): _set_argv(monkeypatch, ["gunicorn"]) _set_testing(monkeypatch, True) - with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch( - "api.attack_paths.database.init_driver" - ) as init_driver: + with ( + patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), + patch("api.attack_paths.database.init_driver") as init_driver, + ): config.ready() init_driver.assert_not_called() 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_models.py b/api/src/backend/api/tests/test_models.py index 13878794b1..b8b7f61dd1 100644 --- a/api/src/backend/api/tests/test_models.py +++ b/api/src/backend/api/tests/test_models.py @@ -243,6 +243,39 @@ class TestSAMLConfigurationModel: assert "Invalid XML" in errors["metadata_xml"][0] assert "not well-formed" in errors["metadata_xml"][0] + def test_xml_bomb_rejected(self, tenants_fixture): + """ + Regression test: a 'billion laughs' XML bomb in the SAML metadata field + must be rejected and not allowed to exhaust server memory / CPU. + + Before the fix, xml.etree.ElementTree was used directly, which does not + protect against entity-expansion attacks. The fix switches to defusedxml + which raises an exception for any XML containing entity definitions. + """ + tenant = tenants_fixture[0] + xml_bomb = ( + "" + "" + " " + " " + " " + "]>" + "" + ) + config = SAMLConfiguration( + email_domain="xmlbomb.com", + metadata_xml=xml_bomb, + tenant=tenant, + ) + + with pytest.raises(ValidationError) as exc_info: + config._parse_metadata() + + errors = exc_info.value.message_dict + assert "metadata_xml" in errors + def test_metadata_missing_sso_fails(self, tenants_fixture): tenant = tenants_fixture[0] xml = """ diff --git a/api/src/backend/api/tests/test_utils.py b/api/src/backend/api/tests/test_utils.py index 24c051717d..a3b1bf5736 100644 --- a/api/src/backend/api/tests/test_utils.py +++ b/api/src/backend/api/tests/test_utils.py @@ -23,6 +23,9 @@ from prowler.providers.azure.azure_provider import AzureProvider from prowler.providers.cloudflare.cloudflare_provider import CloudflareProvider from prowler.providers.gcp.gcp_provider import GcpProvider from prowler.providers.github.github_provider import GithubProvider +from prowler.providers.googleworkspace.googleworkspace_provider import ( + GoogleworkspaceProvider, +) from prowler.providers.iac.iac_provider import IacProvider from prowler.providers.image.image_provider import ImageProvider from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider @@ -113,6 +116,7 @@ class TestReturnProwlerProvider: [ (Provider.ProviderChoices.AWS.value, AwsProvider), (Provider.ProviderChoices.GCP.value, GcpProvider), + (Provider.ProviderChoices.GOOGLEWORKSPACE.value, GoogleworkspaceProvider), (Provider.ProviderChoices.AZURE.value, AzureProvider), (Provider.ProviderChoices.KUBERNETES.value, KubernetesProvider), (Provider.ProviderChoices.M365.value, M365Provider), @@ -248,6 +252,10 @@ class TestGetProwlerProviderKwargs: Provider.ProviderChoices.GCP.value, {"project_ids": ["provider_uid"]}, ), + ( + Provider.ProviderChoices.GOOGLEWORKSPACE.value, + {}, + ), ( Provider.ProviderChoices.KUBERNETES.value, {"context": "provider_uid"}, diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 22e7ab091e..b2e82d2ea1 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -45,6 +45,7 @@ from api.models import ( ComplianceRequirementOverview, DailySeveritySummary, Finding, + FindingGroupDailySummary, Integration, Invitation, LighthouseProviderConfiguration, @@ -1190,6 +1191,26 @@ class TestProviderViewSet: "uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "alias": "OpenStack Project", }, + { + "provider": "googleworkspace", + "uid": "C01234abc", + "alias": "Google Workspace Customer", + }, + { + "provider": "googleworkspace", + "uid": "C12345678", + "alias": "Google Workspace All Digits", + }, + { + "provider": "googleworkspace", + "uid": "CABCDEF123", + "alias": "Google Workspace Uppercase", + }, + { + "provider": "googleworkspace", + "uid": "C12", + "alias": "Google Workspace Minimum Length", + }, ] ), ) @@ -1342,7 +1363,11 @@ class TestProviderViewSet: response = authenticated_client.post( reverse("provider-list"), data=provider_json_payload, format="json" ) - assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.status_code == status.HTTP_409_CONFLICT + error = response.json()["errors"][0] + assert error["detail"] == "Provider already exists." + assert error["code"] == "conflict" + assert error["source"]["pointer"] == "/data/attributes/uid" mock_delete_task.reset_mock() mock_delete_task.return_value = task_mock @@ -1634,6 +1659,36 @@ class TestProviderViewSet: "min_length", "uid", ), + # Google Workspace UID validation - missing 'C' prefix + ( + { + "provider": "googleworkspace", + "uid": "01234abc", + "alias": "test", + }, + "googleworkspace-uid", + "uid", + ), + # Google Workspace UID validation - contains special characters + ( + { + "provider": "googleworkspace", + "uid": "C0123-abc", + "alias": "test", + }, + "googleworkspace-uid", + "uid", + ), + # Google Workspace UID validation - lowercase 'c' prefix + ( + { + "provider": "googleworkspace", + "uid": "c12345678", + "alias": "test", + }, + "googleworkspace-uid", + "uid", + ), ] ), ) @@ -1807,21 +1862,21 @@ class TestProviderViewSet: ( "uid.icontains", "1", - 10, + 11, ), ("alias", "aws_testing_1", 1), ("alias.icontains", "aws", 2), - ("inserted_at", TODAY, 11), + ("inserted_at", TODAY, 12), ( "inserted_at.gte", "2024-01-01", - 11, + 12, ), ("inserted_at.lte", "2024-01-01", 0), ( "updated_at.gte", "2024-01-01", - 11, + 12, ), ("updated_at.lte", "2024-01-01", 0), ] @@ -2437,6 +2492,15 @@ class TestProviderSecretViewSet: "clouds_yaml_cloud": "mycloud", }, ), + # Google Workspace with service account credentials + ( + Provider.ProviderChoices.GOOGLEWORKSPACE.value, + ProviderSecret.TypeChoices.STATIC, + { + "credentials_content": '{"type": "service_account", "project_id": "test-project", "private_key_id": "key123", "private_key": "-----BEGIN PRIVATE KEY-----\\ntest\\n-----END PRIVATE KEY-----\\n", "client_email": "test@test-project.iam.gserviceaccount.com", "client_id": "123456789"}', + "delegated_user": "admin@example.com", + }, + ), ], ) def test_provider_secrets_create_valid( @@ -3747,6 +3811,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 { @@ -4348,8 +4418,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, @@ -4407,7 +4475,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, @@ -4462,7 +4529,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, @@ -4504,7 +4570,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, @@ -4531,7 +4596,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, @@ -4569,9 +4633,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, @@ -4621,7 +5019,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, @@ -4656,7 +5053,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, @@ -7463,8 +7859,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 @@ -14290,10 +14690,16 @@ class TestMuteRuleViewSet: assert len(data) == 2 assert data[0]["id"] == str(mute_rules_fixture[first_index].id) - @patch("tasks.tasks.mute_historical_findings_task.apply_async") + @patch("api.v1.views.chain") + @patch("api.v1.views.aggregate_finding_group_summaries_task.si") + @patch("api.v1.views.mute_historical_findings_task.si") + @patch("api.v1.views.transaction.on_commit", side_effect=lambda fn: fn()) def test_mute_rules_create_valid( self, - mock_task, + _mock_on_commit, + mock_mute_signature, + mock_aggregate_signature, + mock_chain, authenticated_client, findings_fixture, create_test_user, @@ -14331,8 +14737,14 @@ class TestMuteRuleViewSet: assert finding.muted_at is not None assert finding.muted_reason == "Security exception approved" - # Verify background task was called - mock_task.assert_called_once() + # Verify background task chain was called + mock_mute_signature.assert_called_once() + mock_aggregate_signature.assert_called_once() + mock_chain.assert_called_once_with( + mock_mute_signature.return_value, + mock_aggregate_signature.return_value, + ) + mock_chain.return_value.apply_async.assert_called_once() @patch("tasks.tasks.mute_historical_findings_task.apply_async") def test_mute_rules_create_converts_finding_ids_to_uids( @@ -15127,6 +15539,22 @@ class TestFindingGroupViewSet: assert len(response.json()["data"]) == 1 assert "bucket" in response.json()["data"][0]["id"].lower() + def test_finding_groups_check_title_icontains( + self, authenticated_client, finding_groups_fixture + ): + """Test searching check titles with icontains.""" + response = authenticated_client.get( + reverse("finding-group-list"), + { + "filter[inserted_at]": TODAY, + "filter[check_title.icontains]": "public access", + }, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) == 1 + assert data[0]["id"] == "s3_bucket_public_access" + def test_resources_not_found(self, authenticated_client): """Test 404 returned for nonexistent check_id.""" response = authenticated_client.get( @@ -15425,6 +15853,48 @@ class TestFindingGroupViewSet: assert len(data) == 1 assert data[0]["id"] == "cloudtrail_enabled" + def test_finding_groups_latest_aggregates_latest_per_provider( + self, authenticated_client, providers_fixture + ): + """Test /latest aggregates latest summary from each provider for the same check.""" + provider1 = providers_fixture[0] + provider2 = providers_fixture[1] + + check_id = "cross_provider_latest_resources_total" + now = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0) + + FindingGroupDailySummary.objects.create( + tenant_id=provider1.tenant_id, + provider=provider1, + check_id=check_id, + inserted_at=now - timedelta(days=1), + resources_total=20, + resources_fail=20, + fail_count=20, + ) + FindingGroupDailySummary.objects.create( + tenant_id=provider2.tenant_id, + provider=provider2, + check_id=check_id, + inserted_at=now, + resources_total=7, + resources_fail=7, + fail_count=7, + ) + + response = authenticated_client.get( + reverse("finding-group-latest"), + {"filter[check_id]": check_id}, + ) + + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) == 1 + attrs = data[0]["attributes"] + assert attrs["resources_total"] == 27 + assert attrs["resources_fail"] == 27 + assert attrs["fail_count"] == 27 + def test_finding_groups_latest_provider_type_filter( self, authenticated_client, finding_groups_fixture ): diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index d856bd5e6a..6d4102af55 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -27,6 +27,9 @@ if TYPE_CHECKING: from prowler.providers.cloudflare.cloudflare_provider import CloudflareProvider from prowler.providers.gcp.gcp_provider import GcpProvider from prowler.providers.github.github_provider import GithubProvider + from prowler.providers.googleworkspace.googleworkspace_provider import ( + GoogleworkspaceProvider, + ) from prowler.providers.iac.iac_provider import IacProvider from prowler.providers.image.image_provider import ImageProvider from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider @@ -83,6 +86,7 @@ def return_prowler_provider( | CloudflareProvider | GcpProvider | GithubProvider + | GoogleworkspaceProvider | IacProvider | ImageProvider | KubernetesProvider @@ -97,7 +101,7 @@ def return_prowler_provider( provider (Provider): The provider object containing the provider type and associated secrets. Returns: - AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | ImageProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider: The corresponding provider class. + AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | GoogleworkspaceProvider | IacProvider | ImageProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider: The corresponding provider class. Raises: ValueError: If the provider type specified in `provider.provider` is not supported. @@ -111,6 +115,12 @@ def return_prowler_provider( from prowler.providers.gcp.gcp_provider import GcpProvider prowler_provider = GcpProvider + case Provider.ProviderChoices.GOOGLEWORKSPACE.value: + from prowler.providers.googleworkspace.googleworkspace_provider import ( + GoogleworkspaceProvider, + ) + + prowler_provider = GoogleworkspaceProvider case Provider.ProviderChoices.AZURE.value: from prowler.providers.azure.azure_provider import AzureProvider @@ -263,6 +273,7 @@ def initialize_prowler_provider( | CloudflareProvider | GcpProvider | GithubProvider + | GoogleworkspaceProvider | IacProvider | ImageProvider | KubernetesProvider @@ -278,7 +289,7 @@ def initialize_prowler_provider( mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration. Returns: - AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | ImageProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider: An instance of the corresponding provider class + AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | GoogleworkspaceProvider | IacProvider | ImageProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider: An instance of the corresponding provider class initialized with the provider's secrets. """ prowler_provider = return_prowler_provider(provider) diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index 83ea942793..034bc79cb2 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -191,6 +191,22 @@ from rest_framework_json_api import serializers }, "required": ["service_account_key"], }, + { + "type": "object", + "title": "Google Workspace Service Account", + "properties": { + "credentials_content": { + "type": "string", + "description": "The service account JSON credentials content for Google Workspace API access with domain-wide delegation enabled.", + }, + "delegated_user": { + "type": "string", + "format": "email", + "description": "The email address of the Google Workspace super admin user to impersonate for domain-wide delegation.", + }, + }, + "required": ["credentials_content", "delegated_user"], + }, { "type": "object", "title": "Kubernetes Static Credentials", diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 7c4bb41ca5..63e60cd88f 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -6,6 +6,7 @@ from django.conf import settings from django.contrib.auth import authenticate from django.contrib.auth.models import update_last_login from django.contrib.auth.password_validation import validate_password +from django.core.exceptions import ValidationError as DjangoValidationError from django.db import IntegrityError from drf_spectacular.utils import extend_schema_field from jwt.exceptions import InvalidKeyError @@ -959,6 +960,26 @@ class ProviderCreateSerializer(RLSSerializer, BaseWriteSerializer): }, } + def create(self, validated_data): + try: + return super().create(validated_data) + except DjangoValidationError as e: + if "unique_provider_uids" in str(e): + raise ConflictException( + detail="Provider already exists.", + pointer="/data/attributes/uid", + ) + raise + except IntegrityError as e: + # Handle race conditions where the unique constraint is enforced at the DB level + # after validation has already passed. + if "unique_provider_uids" in str(e): + raise ConflictException( + detail="Provider already exists.", + pointer="/data/attributes/uid", + ) + raise + class ProviderUpdateSerializer(BaseWriteSerializer): """ @@ -1220,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" @@ -1520,6 +1541,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer): serializer = AzureProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.GCP.value: serializer = GCPProviderSecret(data=secret) + elif provider_type == Provider.ProviderChoices.GOOGLEWORKSPACE.value: + serializer = GoogleWorkspaceProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.GITHUB.value: serializer = GithubProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.IAC.value: @@ -1655,6 +1678,14 @@ class GCPServiceAccountProviderSecret(serializers.Serializer): resource_name = "provider-secrets" +class GoogleWorkspaceProviderSecret(serializers.Serializer): + credentials_content = serializers.CharField() + delegated_user = serializers.EmailField() + + class Meta: + resource_name = "provider-secrets" + + class MongoDBAtlasProviderSecret(serializers.Serializer): atlas_public_key = serializers.CharField() atlas_private_key = serializers.CharField() 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 b23c589262..85c3062965 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -3,7 +3,7 @@ import glob import json import logging import os - +import time from collections import defaultdict from copy import deepcopy from datetime import datetime, timedelta, timezone @@ -11,12 +11,12 @@ from decimal import ROUND_HALF_UP, Decimal, InvalidOperation from urllib.parse import urljoin import sentry_sdk - from allauth.socialaccount.models import SocialAccount, SocialApp from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter from allauth.socialaccount.providers.saml.views import FinishACSView, LoginView from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError +from celery import chain from celery.result import AsyncResult from config.custom_logging import BackendLogger from config.env import env @@ -75,12 +75,14 @@ from rest_framework.exceptions import ( ) from rest_framework.generics import GenericAPIView, get_object_or_404 from rest_framework.permissions import SAFE_METHODS +from rest_framework_json_api import filters as jsonapi_filters from rest_framework_json_api.views import RelationshipView, Response from rest_framework_simplejwt.exceptions import InvalidToken, TokenError from tasks.beat import schedule_provider_scan from tasks.jobs.attack_paths import db_utils as attack_paths_db_utils from tasks.jobs.export import get_s3_client from tasks.tasks import ( + aggregate_finding_group_summaries_task, backfill_compliance_summaries_task, backfill_scan_resource_summaries_task, check_integration_connection_task, @@ -99,7 +101,6 @@ from api.attack_paths import database as graph_database from api.attack_paths import get_queries_for_provider, get_query_by_id from api.attack_paths import views_helpers as attack_paths_views_helpers from api.base_views import BaseRLSViewSet, BaseTenantViewset, BaseUserViewset -from api.renderers import APIJSONRenderer, PlainTextRenderer from api.compliance import ( PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE, get_compliance_frameworks, @@ -198,6 +199,7 @@ from api.models import ( ) from api.pagination import ComplianceOverviewPagination from api.rbac.permissions import Permissions, get_providers, get_role +from api.renderers import APIJSONRenderer, PlainTextRenderer from api.rls import Tenant from api.utils import ( CustomOAuth2Client, @@ -407,7 +409,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." ) @@ -2451,6 +2453,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 = [] @@ -2570,14 +2577,35 @@ class AttackPathsScanViewSet(BaseRLSViewSet): provider_id, ) + start = time.monotonic() graph = attack_paths_views_helpers.execute_query( database_name, query_definition, parameters, provider_id, ) + query_duration = time.monotonic() - start graph_database.clear_cache(database_name) + result_nodes = len(graph.get("nodes", [])) + result_relationships = len(graph.get("relationships", [])) + logger.info( + "attack_paths_query_run", + extra={ + "user_id": str(request.user.id), + "tenant_id": str(attack_paths_scan.provider.tenant_id), + "metadata": { + "query_id": query_definition.id, + "provider": query_definition.provider, + "scan_id": pk, + "provider_id": provider_id, + "result_nodes": result_nodes, + "result_relationships": result_relationships, + "query_duration": round(query_duration, 3), + }, + }, + ) + status_code = status.HTTP_200_OK if not graph.get("nodes"): status_code = status.HTTP_404_NOT_FOUND @@ -2618,13 +2646,35 @@ class AttackPathsScanViewSet(BaseRLSViewSet): ) provider_id = str(attack_paths_scan.provider_id) + start = time.monotonic() graph = attack_paths_views_helpers.execute_custom_query( database_name, serializer.validated_data["query"], provider_id, ) + query_duration = time.monotonic() - start graph_database.clear_cache(database_name) + query_length = len(serializer.validated_data["query"]) + result_nodes = len(graph.get("nodes", [])) + result_relationships = len(graph.get("relationships", [])) + logger.info( + "attack_paths_custom_query_run", + extra={ + "user_id": str(request.user.id), + "tenant_id": str(attack_paths_scan.provider.tenant_id), + "metadata": { + "provider": attack_paths_scan.provider.provider, + "scan_id": pk, + "provider_id": provider_id, + "query_length": query_length, + "result_nodes": result_nodes, + "result_relationships": result_relationships, + "query_duration": round(query_duration, 3), + }, + }, + ) + status_code = status.HTTP_200_OK if not graph.get("nodes"): status_code = status.HTTP_404_NOT_FOUND @@ -6677,10 +6727,25 @@ class MuteRuleViewSet(BaseRLSViewSet): ) # Launch background task for historical muting - with transaction.atomic(): - mute_historical_findings_task.apply_async( - kwargs={"tenant_id": tenant_id, "mute_rule_id": str(mute_rule.id)} - ) + latest_scan_id = ( + Scan.objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED) + .order_by("-completed_at", "-inserted_at") + .values_list("id", flat=True) + .first() + ) + + transaction.on_commit( + lambda: chain( + mute_historical_findings_task.si( + tenant_id=tenant_id, + mute_rule_id=str(mute_rule.id), + ), + aggregate_finding_group_summaries_task.si( + tenant_id=tenant_id, + scan_id=str(latest_scan_id), + ), + ).apply_async() + ) # Return the created mute rule serializer = self.get_serializer(mute_rule) @@ -6728,13 +6793,29 @@ class FindingGroupViewSet(BaseRLSViewSet): queryset = FindingGroupDailySummary.objects.all() serializer_class = FindingGroupSerializer filterset_class = FindingGroupSummaryFilter + filter_backends = [ + jsonapi_filters.QueryParameterValidationFilter, + jsonapi_filters.OrderingFilter, + CustomDjangoFilterBackend, + ] http_method_names = ["get"] required_permissions = [] def get_filterset_class(self): - """Return appropriate filter based on action.""" + """Return the filterset class used for schema generation and the list action. + + Note: The resources and latest_resources actions do not use this method + at runtime. They manually instantiate FindingGroupFilter / + LatestFindingGroupFilter against a Finding queryset (see + _get_finding_queryset). The class returned here for those actions only + affects the OpenAPI schema generated by drf-spectacular. + """ if self.action == "latest": return LatestFindingGroupSummaryFilter + if self.action == "resources": + return FindingGroupFilter + if self.action == "latest_resources": + return LatestFindingGroupFilter return FindingGroupSummaryFilter def get_queryset(self): @@ -7146,13 +7227,15 @@ class FindingGroupViewSet(BaseRLSViewSet): raise ValidationError(filterset.errors) filtered_queryset = filterset.qs - # Keep only rows from the latest inserted_at date per check_id - latest_per_check = filtered_queryset.annotate( - latest_inserted_at=Window( - expression=Max("inserted_at"), - partition_by=[F("check_id")], - ) - ).filter(inserted_at=F("latest_inserted_at")) + # Keep only the latest row per (check_id, provider), then aggregate by check_id. + latest_per_check_ids = ( + filtered_queryset.order_by("check_id", "provider_id", "-inserted_at") + .distinct("check_id", "provider_id") + .values("id") + ) + latest_per_check = filtered_queryset.filter( + id__in=Subquery(latest_per_check_ids) + ) # Re-aggregate daily summaries aggregated_queryset = self._aggregate_daily_summaries(latest_per_check) @@ -7188,6 +7271,7 @@ class FindingGroupViewSet(BaseRLSViewSet): and timing information including how long they have been failing. """, tags=["Finding Groups"], + filters=True, ) @action(detail=True, methods=["get"], url_path="resources") def resources(self, request, pk=None): @@ -7262,6 +7346,7 @@ class FindingGroupViewSet(BaseRLSViewSet): and timing information. No date filters required. """, tags=["Finding Groups"], + filters=True, ) @action( detail=False, diff --git a/api/src/backend/config/custom_logging.py b/api/src/backend/config/custom_logging.py index fb04930679..fe2a090ca6 100644 --- a/api/src/backend/config/custom_logging.py +++ b/api/src/backend/config/custom_logging.py @@ -2,6 +2,7 @@ import json import logging from enum import StrEnum + from config.env import env from django_guid.log_filters import CorrelationId @@ -62,6 +63,8 @@ class NDJSONFormatter(logging.Formatter): log_record["duration"] = record.duration if hasattr(record, "status_code"): log_record["status_code"] = record.status_code + if hasattr(record, "metadata"): + log_record["metadata"] = record.metadata if record.exc_info: log_record["exc_info"] = self.formatException(record.exc_info) @@ -107,6 +110,8 @@ class HumanReadableFormatter(logging.Formatter): log_components.append(f"done in {record.duration}s:") if hasattr(record, "status_code"): log_components.append(f"{record.status_code}") + if hasattr(record, "metadata"): + log_components.append(f"metadata={record.metadata}") if record.exc_info: log_components.append(self.formatException(record.exc_info)) 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/conftest.py b/api/src/backend/conftest.py index 209292ffad..6535706cf7 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -543,6 +543,12 @@ def providers_fixture(tenants_fixture): alias="openstack_testing", tenant_id=tenant.id, ) + provider12 = Provider.objects.create( + provider="googleworkspace", + uid="C12345678", + alias="googleworkspace_testing", + tenant_id=tenant.id, + ) return ( provider1, @@ -556,6 +562,7 @@ def providers_fixture(tenants_fixture): provider9, provider10, provider11, + provider12, ) diff --git a/api/src/backend/tasks/jobs/attack_paths/aws.py b/api/src/backend/tasks/jobs/attack_paths/aws.py index 9242946181..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}") @@ -239,8 +264,9 @@ def sync_aws_account( failed_syncs[func_name] = exception_message logger.warning( - f"Caught exception syncing function {func_name} from AWS account {prowler_api_provider.uid}. We " - "are continuing on to the next AWS sync function.", + f"Caught exception syncing function {func_name} from AWS account {prowler_api_provider.uid}: {e}. " + "Continuing to the next AWS sync function.", + exc_info=True, ) continue 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 cd39700dd4..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,22 +268,40 @@ 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: - graph_database.drop_database(tmp_cartography_config.neo4j_database) + 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 drop temporary Neo4j database {tmp_cartography_config.neo4j_database} during cleanup" + 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}", + 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: - logger.warning( - f"Could not mark attack paths scan {attack_paths_scan.id} as FAILED (row may have been deleted)" + 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}", + exc_info=True, ) raise 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/jobs/threatscore_utils.py b/api/src/backend/tasks/jobs/threatscore_utils.py index a17125af46..2ef29484ee 100644 --- a/api/src/backend/tasks/jobs/threatscore_utils.py +++ b/api/src/backend/tasks/jobs/threatscore_utils.py @@ -4,7 +4,7 @@ from django.db.models import Count, Q from api.db_router import READ_REPLICA_ALIAS from api.db_utils import rls_transaction -from api.models import Finding, StatusChoices +from api.models import Finding, Scan, StatusChoices from prowler.lib.outputs.finding import Finding as FindingOutput logger = get_task_logger(__name__) @@ -35,25 +35,26 @@ def _aggregate_requirement_statistics_from_database( } """ requirement_statistics_by_check_id = {} - # TODO: take into account that now the relation is 1 finding == 1 resource, review this when the logic changes + # TODO: review when finding-resource relation changes from 1:1 with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS): + # Pre-check: skip if the scan's provider is deleted (avoids JOINs in the main query) + if Scan.all_objects.filter(id=scan_id, provider__is_deleted=True).exists(): + return requirement_statistics_by_check_id + aggregated_statistics_queryset = ( Finding.all_objects.filter( tenant_id=tenant_id, scan_id=scan_id, muted=False, - resources__provider__is_deleted=False, ) .values("check_id") .annotate( total_findings=Count( "id", - distinct=True, filter=Q(status__in=[StatusChoices.PASS, StatusChoices.FAIL]), ), passed_findings=Count( "id", - distinct=True, filter=Q(status=StatusChoices.PASS), ), ) 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/api/src/backend/tasks/tests/test_reports.py b/api/src/backend/tasks/tests/test_reports.py index a6a80b0891..858f4c06ca 100644 --- a/api/src/backend/tasks/tests/test_reports.py +++ b/api/src/backend/tasks/tests/test_reports.py @@ -169,35 +169,27 @@ class TestAggregateRequirementStatistics: assert result["check_1"]["passed"] == 1 assert result["check_1"]["total"] == 1 - def test_excludes_findings_without_resources(self, tenants_fixture, scans_fixture): - """Verify findings without resources are excluded from aggregation.""" + def test_skips_aggregation_for_deleted_provider( + self, tenants_fixture, scans_fixture + ): + """Verify aggregation returns empty when the scan's provider is soft-deleted.""" tenant = tenants_fixture[0] scan = scans_fixture[0] - # Finding WITH resource → should be counted self._create_finding_with_resource( tenant, scan, "finding-1", "check_1", StatusChoices.PASS ) - # Finding WITHOUT resource → should be EXCLUDED - Finding.objects.create( - tenant_id=tenant.id, - scan=scan, - uid="finding-2", - check_id="check_1", - status=StatusChoices.FAIL, - severity=Severity.high, - impact=Severity.high, - check_metadata={}, - raw_result={}, - ) + # Soft-delete the provider + provider = scan.provider + provider.is_deleted = True + provider.save(update_fields=["is_deleted"]) result = _aggregate_requirement_statistics_from_database( str(tenant.id), str(scan.id) ) - assert result["check_1"]["passed"] == 1 - assert result["check_1"]["total"] == 1 + assert result == {} def test_multiple_resources_no_double_count(self, tenants_fixture, scans_fixture): """Verify a finding with multiple resources is only counted once.""" diff --git a/contrib/k8s/helm/prowler-app/.gitignore b/contrib/k8s/helm/prowler-app/.gitignore new file mode 100644 index 0000000000..ee3892e879 --- /dev/null +++ b/contrib/k8s/helm/prowler-app/.gitignore @@ -0,0 +1 @@ +charts/ diff --git a/contrib/k8s/helm/prowler-app/Chart.yaml b/contrib/k8s/helm/prowler-app/Chart.yaml index 5397d43569..672e807f10 100644 --- a/contrib/k8s/helm/prowler-app/Chart.yaml +++ b/contrib/k8s/helm/prowler-app/Chart.yaml @@ -13,6 +13,8 @@ keywords: - gcp - kubernetes maintainers: + - name: Dani + email: andre.gomes@promptlyhealth.com - name: Mihai email: mihai.legat@gmail.com dependencies: 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/__main__.py b/dashboard/__main__.py index 304e4afdf2..664ce3bfa5 100644 --- a/dashboard/__main__.py +++ b/dashboard/__main__.py @@ -21,7 +21,7 @@ print( f"{Fore.GREEN}Loading all CSV files from the folder {folder_path_overview} ...\n{Style.RESET_ALL}" ) cli.show_server_banner = lambda *x: click.echo( - f"{Fore.YELLOW}NOTE:{Style.RESET_ALL} If you are using {Fore.GREEN}{Style.BRIGHT}Prowler SaaS{Style.RESET_ALL} with the S3 integration or that integration \nfrom {Fore.CYAN}{Style.BRIGHT}Prowler Open Source{Style.RESET_ALL} and you want to use your data from your S3 bucket,\nrun: `{orange_color}aws s3 cp s3:///output/csv ./output --recursive{Style.RESET_ALL}`\nand then run `prowler dashboard` again to load the new files." + f"{Fore.YELLOW}NOTE:{Style.RESET_ALL} If you are using {Fore.GREEN}{Style.BRIGHT}Prowler Cloud{Style.RESET_ALL} with the S3 integration or that integration \nfrom {Fore.CYAN}{Style.BRIGHT}Prowler CLI{Style.RESET_ALL} and you want to use your data from your S3 bucket,\nrun: `{orange_color}aws s3 cp s3:///output/csv ./output --recursive{Style.RESET_ALL}`\nand then run `prowler dashboard` again to load the new files." ) # Initialize the app - incorporate css 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/checks.mdx b/docs/developer-guide/checks.mdx index 0c9bdf154e..da469678fa 100644 --- a/docs/developer-guide/checks.mdx +++ b/docs/developer-guide/checks.mdx @@ -315,6 +315,7 @@ The type of resource being audited. This field helps categorize and organize fin - **Kubernetes**: Use types shown under `KIND` from `kubectl api-resources`. - **Oracle Cloud Infrastructure**: Use types from [Oracle Cloud Infrastructure documentation](https://docs.public.oneportal.content.oci.oraclecloud.com/en-us/iaas/Content/Search/Tasks/queryingresources_topic-Listing_Supported_Resource_Types.htm). - **OpenStack**: Use types from [OpenStack Heat resource types](https://docs.openstack.org/heat/latest/template_guide/openstack.html). +- **Alibaba Cloud**: Use types from [Alibaba Cloud ROS resource types](https://www.alibabacloud.com/help/en/ros/developer-reference/list-of-resource-types-by-service). - **Any other provider**: Use `NotDefined` due to lack of standardized resource types in their SDK or documentation. #### ResourceGroup 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/docs.json b/docs/docs.json index 3ec056989f..a8fd6dedd6 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -225,7 +225,8 @@ "group": "Kubernetes", "pages": [ "user-guide/providers/kubernetes/getting-started-k8s", - "user-guide/providers/kubernetes/misc" + "user-guide/providers/kubernetes/misc", + "user-guide/cookbooks/kubernetes-in-cluster" ] }, { @@ -304,6 +305,13 @@ "pages": [ "user-guide/compliance/tutorials/threatscore" ] + }, + { + "group": "Cookbooks", + "pages": [ + "user-guide/cookbooks/kubernetes-in-cluster", + "user-guide/cookbooks/cicd-pipeline" + ] } ] }, diff --git a/docs/getting-started/basic-usage/prowler-mcp-tools.mdx b/docs/getting-started/basic-usage/prowler-mcp-tools.mdx index bf0092a069..43cbb12027 100644 --- a/docs/getting-started/basic-usage/prowler-mcp-tools.mdx +++ b/docs/getting-started/basic-usage/prowler-mcp-tools.mdx @@ -10,7 +10,7 @@ Complete reference guide for all tools available in the Prowler MCP Server. Tool |----------|------------|------------------------| | Prowler Hub | 10 tools | No | | Prowler Documentation | 2 tools | No | -| Prowler Cloud/App | 24 tools | Yes | +| Prowler Cloud/App | 27 tools | Yes | ## Tool Naming Convention @@ -80,6 +80,14 @@ Tools for managing finding muting, including pattern-based bulk muting (mutelist - **`prowler_app_update_mute_rule`** - Update a mute rule's name, reason, or enabled status - **`prowler_app_delete_mute_rule`** - Delete a mute rule from the system +### Attack Paths Analysis + +Tools for analyzing privilege escalation chains and security misconfigurations using graph-based analysis. Attack Paths maps relationships between cloud resources, permissions, and security findings to detect how privileges can be escalated and how misconfigurations can be exploited. + +- **`prowler_app_list_attack_paths_scans`** - List Attack Paths scans with filtering by provider, provider type, and scan state (available, scheduled, executing, completed, failed, cancelled) +- **`prowler_app_list_attack_paths_queries`** - Discover available Attack Paths queries for a completed scan, including query names, descriptions, and required parameters +- **`prowler_app_run_attack_paths_query`** - Execute an Attack Paths query against a completed scan and retrieve graph results with nodes (cloud resources, findings, virtual nodes) and relationships (access paths, role assumptions, security group memberships) + ### Compliance Management Tools for viewing compliance status and framework details across all cloud providers. 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/docs/getting-started/products/prowler-mcp.mdx b/docs/getting-started/products/prowler-mcp.mdx index df402f605b..ea6fe8fcc0 100644 --- a/docs/getting-started/products/prowler-mcp.mdx +++ b/docs/getting-started/products/prowler-mcp.mdx @@ -24,6 +24,7 @@ Full access to Prowler Cloud platform and self-managed Prowler App for: - **Scan Orchestration**: Trigger on-demand scans and schedule recurring security assessments - **Resource Inventory**: Search and view detailed information about your audited resources - **Muting Management**: Create and manage muting lists/rules to suppress non-relevant findings +- **Attack Paths Analysis**: Analyze privilege escalation chains and security misconfigurations through graph-based analysis of cloud resource relationships ### 2. Prowler Hub @@ -61,6 +62,7 @@ The Prowler MCP Server enables powerful workflows through AI assistants: - "Show me all critical findings from my AWS production accounts" - "Register my new AWS account in Prowler and run a scheduled scan every day" - "List all muted findings and detect what findgings are muted by a not enough good reason in relation to their severity" +- "Run an attack paths query to find EC2 instances exposed to the Internet with access to sensitive S3 buckets" **Security Research** - "Explain what the S3 bucket public access Prowler check does" diff --git a/docs/introduction.mdx b/docs/introduction.mdx index c354f2802d..05de41b157 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -33,12 +33,13 @@ The supported providers right now are: | [Github](/user-guide/providers/github/getting-started-github) | Official | Organizations / Repositories | UI, API, CLI | | [Oracle Cloud](/user-guide/providers/oci/getting-started-oci) | Official | Tenancies / Compartments | UI, API, CLI | | [Alibaba Cloud](/user-guide/providers/alibabacloud/getting-started-alibabacloud) | Official | Accounts | UI, API, CLI | -| [Cloudflare](/user-guide/providers/cloudflare/getting-started-cloudflare) | Official | Accounts | CLI | +| [Cloudflare](/user-guide/providers/cloudflare/getting-started-cloudflare) | Official | Accounts | UI, API, CLI | | [Infra as Code](/user-guide/providers/iac/getting-started-iac) | Official | Repositories | UI, API, CLI | | [MongoDB Atlas](/user-guide/providers/mongodbatlas/getting-started-mongodbatlas) | Official | Organizations | UI, API, CLI | -| [OpenStack](/user-guide/providers/openstack/getting-started-openstack) | Official | Projects | CLI | +| [OpenStack](/user-guide/providers/openstack/getting-started-openstack) | Official | Projects | UI, API, CLI | | [LLM](/user-guide/providers/llm/getting-started-llm) | Official | Models | CLI | -| [Image](/user-guide/providers/image/getting-started-image) | Official | Container Images | CLI | +| [Image](/user-guide/providers/image/getting-started-image) | Official | Container Images | CLI, API | +| [Google Workspace](/user-guide/providers/googleworkspace/getting-started-googleworkspace) | Official | Domains | CLI | | **NHN** | Unofficial | Tenants | CLI | For more information about the checks and compliance of each provider visit [Prowler Hub](https://hub.prowler.com). diff --git a/docs/user-guide/cli/tutorials/configuration_file.mdx b/docs/user-guide/cli/tutorials/configuration_file.mdx index e5039bff21..923bef7ce1 100644 --- a/docs/user-guide/cli/tutorials/configuration_file.mdx +++ b/docs/user-guide/cli/tutorials/configuration_file.mdx @@ -73,6 +73,7 @@ The following list includes all the AWS checks with configurable variables that | `ssm_documents_set_as_public` | `trusted_account_ids` | List of Strings | | `vpc_endpoint_connections_trust_boundaries` | `trusted_account_ids` | List of Strings | | `vpc_endpoint_services_allowed_principals_trust_boundaries` | `trusted_account_ids` | List of Strings | +| `opensearch_service_domains_not_publicly_accessible` | `trusted_ips` | List of Strings | ## Azure diff --git a/docs/user-guide/cli/tutorials/dashboard.mdx b/docs/user-guide/cli/tutorials/dashboard.mdx index 8c819743fe..0fe3c69fed 100644 --- a/docs/user-guide/cli/tutorials/dashboard.mdx +++ b/docs/user-guide/cli/tutorials/dashboard.mdx @@ -99,7 +99,7 @@ def get_table(data): ## S3 Integration -If you are using Prowler SaaS with the S3 integration or that integration from Prowler Open Source and you want to use your data from your S3 bucket, you can run the following command in order to load the dashboard with the new files: +If you are using Prowler Cloud with the S3 integration or that integration from Prowler CLI and you want to use your data from your S3 bucket, you can run the following command in order to load the dashboard with the new files: ```sh aws s3 cp s3:///output/csv ./output --recursive diff --git a/docs/user-guide/cookbooks/cicd-pipeline.mdx b/docs/user-guide/cookbooks/cicd-pipeline.mdx new file mode 100644 index 0000000000..9dffd5049c --- /dev/null +++ b/docs/user-guide/cookbooks/cicd-pipeline.mdx @@ -0,0 +1,243 @@ +--- +title: 'Run Prowler in CI/CD and Send Findings to Prowler Cloud' +--- + +This cookbook demonstrates how to integrate Prowler into CI/CD pipelines so that security scans run automatically and findings are sent to Prowler Cloud via [Import Findings](/user-guide/tutorials/prowler-app-import-findings). Examples cover GitHub Actions and GitLab CI. + +## Prerequisites + +* A **Prowler Cloud** account with an active subscription (see [Prowler Cloud Pricing](https://prowler.com/pricing)) +* A Prowler Cloud **API key** with the **Manage Ingestions** permission (see [API Keys](/user-guide/tutorials/prowler-app-api-keys)) +* Cloud provider credentials configured in the CI/CD environment (e.g., AWS credentials for scanning AWS accounts) +* Access to configure pipeline workflows and secrets in the CI/CD platform + +## Key Concepts + +Prowler CLI provides the `--push-to-cloud` flag, which uploads scan results directly to Prowler Cloud after a scan completes. Combined with the `PROWLER_CLOUD_API_KEY` environment variable, this enables fully automated ingestion without manual file uploads. + +For full details on the flag and API, refer to the [Import Findings](/user-guide/tutorials/prowler-app-import-findings) documentation. + + +The examples in this guide use AWS as the target provider, but the same approach applies to any provider supported by Prowler (Azure, GCP, Kubernetes, and others). Replace `prowler aws` with the desired provider command (e.g., `prowler gcp`, `prowler azure`) and configure the corresponding credentials in the CI/CD environment. + + +## GitHub Actions + +### Store Secrets + +Before creating the workflow, add the following secrets to the repository (under "Settings" > "Secrets and variables" > "Actions"): + +* `PROWLER_CLOUD_API_KEY` — the Prowler Cloud API key +* Cloud provider credentials (e.g., `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, or configure OIDC-based role assumption) + +### Workflow: Scheduled AWS Scan + +This workflow runs Prowler against an AWS account on a daily schedule and on every push to the `main` branch: + +```yaml +name: Prowler Security Scan + +on: + schedule: + - cron: "0 3 * * *" # Daily at 03:00 UTC + push: + branches: [main] + workflow_dispatch: # Allow manual triggers + +permissions: + id-token: write # Required for OIDC + contents: read + +jobs: + prowler-scan: + runs-on: ubuntu-latest + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::123456789012:role/ProwlerScanRole + aws-region: us-east-1 + + - name: Install Prowler + run: pip install prowler + + - name: Run Prowler Scan + env: + PROWLER_CLOUD_API_KEY: ${{ secrets.PROWLER_CLOUD_API_KEY }} + run: | + prowler aws --push-to-cloud +``` + + +Replace `123456789012` with the actual AWS account ID and `ProwlerScanRole` with the IAM role name. For IAM role setup, refer to the [AWS authentication guide](/user-guide/providers/aws/authentication). + + +### Workflow: Scan Specific Services on Pull Request + +To run targeted scans on pull requests without blocking the merge pipeline, use `continue-on-error`: + +```yaml +name: Prowler PR Check + +on: + pull_request: + branches: [main] + +jobs: + prowler-scan: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::123456789012:role/ProwlerScanRole + aws-region: us-east-1 + + - name: Install Prowler + run: pip install prowler + + - name: Run Prowler Scan + env: + PROWLER_CLOUD_API_KEY: ${{ secrets.PROWLER_CLOUD_API_KEY }} + run: | + prowler aws --services s3,iam,ec2 --push-to-cloud +``` + + +Limiting the scan to specific services with `--services` reduces execution time, making it practical for pull request checks. + + +## GitLab CI + +### Store Variables + +Add the following CI/CD variables in the GitLab project (under "Settings" > "CI/CD" > "Variables"): + +* `PROWLER_CLOUD_API_KEY` — mark as **masked** and **protected** +* Cloud provider credentials as needed + +### Pipeline: Scheduled AWS Scan + +Add the following to `.gitlab-ci.yml`: + +```yaml +prowler-scan: + image: python:3.12-slim + stage: test + script: + - pip install prowler + - prowler aws --push-to-cloud + variables: + PROWLER_CLOUD_API_KEY: $PROWLER_CLOUD_API_KEY + AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY + AWS_DEFAULT_REGION: "us-east-1" + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + when: manual +``` + +To run the scan on a schedule, create a **Pipeline Schedule** in GitLab (under "Build" > "Pipeline Schedules") with the desired cron expression. + +### Pipeline: Multi-Provider Scan + +To scan multiple cloud providers in parallel: + +```yaml +stages: + - security + +.prowler-base: + image: python:3.12-slim + stage: security + before_script: + - pip install prowler + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + +prowler-aws: + extends: .prowler-base + script: + - prowler aws --push-to-cloud + variables: + PROWLER_CLOUD_API_KEY: $PROWLER_CLOUD_API_KEY + AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY + +prowler-gcp: + extends: .prowler-base + script: + - prowler gcp --push-to-cloud + variables: + PROWLER_CLOUD_API_KEY: $PROWLER_CLOUD_API_KEY + GOOGLE_APPLICATION_CREDENTIALS: $GCP_SERVICE_ACCOUNT_KEY +``` + +## Tips and Best Practices + +### When to Run Scans + +* **Scheduled scans** (daily or weekly) provide continuous monitoring and are ideal for baseline security assessments +* **On-merge scans** catch configuration changes introduced by new code +* **Pull request scans** provide early feedback but should target specific services to keep execution times reasonable + +### Handling Scan Failures + +By default, Prowler exits with a non-zero code when it finds failing checks. This causes the CI/CD job to fail. To prevent scan results from blocking the pipeline: + +* **GitHub Actions**: Add `continue-on-error: true` to the job +* **GitLab CI**: Add `allow_failure: true` to the job + + +Ingestion failures (e.g., network issues reaching Prowler Cloud) do not affect the Prowler exit code. The scan completes normally and only a warning is emitted. See [Import Findings troubleshooting](/user-guide/tutorials/prowler-app-import-findings#troubleshooting) for details. + + +### Caching Prowler Installation + +For faster pipeline runs, cache the Prowler installation: + +**GitHub Actions:** +```yaml +- name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-prowler + restore-keys: ${{ runner.os }}-pip- + +- name: Install Prowler + run: pip install prowler +``` + +**GitLab CI:** +```yaml +prowler-scan: + cache: + paths: + - .cache/pip + variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" +``` + +### Output Formats + +To generate additional report formats alongside the cloud upload: + +```bash +prowler aws --push-to-cloud -M csv,html -o /tmp/prowler-reports +``` + +This produces CSV and HTML files locally while also pushing OCSF findings to Prowler Cloud. The local files can be stored as CI/CD artifacts for archival purposes. + +### Scanning Multiple AWS Accounts + +To scan multiple accounts sequentially in a single job, use [role assumption](/user-guide/providers/aws/role-assumption): + +```bash +prowler aws -R arn:aws:iam::111111111111:role/ProwlerScanRole --push-to-cloud +prowler aws -R arn:aws:iam::222222222222:role/ProwlerScanRole --push-to-cloud +``` + +Each scan run creates a separate ingestion job in Prowler Cloud. diff --git a/docs/user-guide/cookbooks/kubernetes-in-cluster.mdx b/docs/user-guide/cookbooks/kubernetes-in-cluster.mdx new file mode 100644 index 0000000000..765bcf9313 --- /dev/null +++ b/docs/user-guide/cookbooks/kubernetes-in-cluster.mdx @@ -0,0 +1,207 @@ +--- +title: 'Run Kubernetes In-Cluster and Send Findings to Prowler Cloud' +--- + +This cookbook walks through deploying Prowler inside a Kubernetes cluster on a recurring schedule and automatically sending findings to Prowler Cloud via [Import Findings](/user-guide/tutorials/prowler-app-import-findings). By the end, security scan results from the cluster appear in Prowler Cloud without any manual file uploads. + +## Prerequisites + +* A **Prowler Cloud** account with an active subscription (see [Prowler Cloud Pricing](https://prowler.com/pricing)) +* A Prowler Cloud **API key** with the **Manage Ingestions** permission (see [API Keys](/user-guide/tutorials/prowler-app-api-keys)) +* Access to a Kubernetes cluster with `kubectl` configured +* Permissions to create ServiceAccounts, Roles, RoleBindings, Secrets, and CronJobs in the cluster + +## Step 1: Create the ServiceAccount and RBAC Resources + +Prowler needs a ServiceAccount with read access to cluster resources. Apply the manifests from the [`kubernetes` directory](https://github.com/prowler-cloud/prowler/tree/master/kubernetes) of the Prowler repository: + +```console +kubectl apply -f kubernetes/prowler-sa.yaml +kubectl apply -f kubernetes/prowler-role.yaml +kubectl apply -f kubernetes/prowler-rolebinding.yaml +``` + +This creates: + +* A `prowler-sa` ServiceAccount in the `prowler-ns` namespace +* A ClusterRole with the read permissions Prowler requires +* A ClusterRoleBinding linking the ServiceAccount to the role + +For more details on these resources, refer to [Getting Started with Kubernetes](/user-guide/providers/kubernetes/getting-started-k8s). + +## Step 2: Store the Prowler Cloud API Key as a Secret + +Create a Kubernetes Secret to hold the API key securely: + +```console +kubectl create secret generic prowler-cloud-api-key \ + --from-literal=api-key=pk_your_api_key_here \ + --namespace prowler-ns +``` + +Replace `pk_your_api_key_here` with the actual API key from Prowler Cloud. + + +Avoid embedding the API key directly in the CronJob manifest. Using a Kubernetes Secret keeps credentials out of version control and pod specs. + + +## Step 3: Create the CronJob Manifest + +The CronJob runs Prowler on a schedule, scanning the cluster and pushing findings to Prowler Cloud with the `--push-to-cloud` flag. + +Create a file named `prowler-cronjob.yaml`: + +```yaml +apiVersion: batch/v1 +kind: CronJob +metadata: + name: prowler-k8s-scan + namespace: prowler-ns +spec: + schedule: "0 2 * * *" # Runs daily at 02:00 UTC + concurrencyPolicy: Forbid + jobTemplate: + spec: + backoffLimit: 1 + template: + metadata: + labels: + app: prowler + spec: + serviceAccountName: prowler-sa + containers: + - name: prowler + image: prowlercloud/prowler:stable + args: + - "kubernetes" + - "--push-to-cloud" + env: + - name: PROWLER_CLOUD_API_KEY + valueFrom: + secretKeyRef: + name: prowler-cloud-api-key + key: api-key + - name: CLUSTER_NAME + value: "my-cluster" + imagePullPolicy: Always + volumeMounts: + - name: var-lib-cni + mountPath: /var/lib/cni + readOnly: true + - name: var-lib-etcd + mountPath: /var/lib/etcd + readOnly: true + - name: var-lib-kubelet + mountPath: /var/lib/kubelet + readOnly: true + - name: etc-kubernetes + mountPath: /etc/kubernetes + readOnly: true + hostPID: true + restartPolicy: Never + volumes: + - name: var-lib-cni + hostPath: + path: /var/lib/cni + - name: var-lib-etcd + hostPath: + path: /var/lib/etcd + - name: var-lib-kubelet + hostPath: + path: /var/lib/kubelet + - name: etc-kubernetes + hostPath: + path: /etc/kubernetes +``` + + +Replace `my-cluster` with a meaningful name for the cluster. This value appears in Prowler Cloud reports and helps identify the source of findings. See the `--cluster-name` flag documentation in [Getting Started with Kubernetes](/user-guide/providers/kubernetes/getting-started-k8s) for more details. + + +### Customizing the Schedule + +The `schedule` field uses standard cron syntax. Common examples: + +* `"0 2 * * *"` — daily at 02:00 UTC +* `"0 */6 * * *"` — every 6 hours +* `"0 2 * * 1"` — weekly on Mondays at 02:00 UTC + +### Scanning Specific Namespaces + +To limit the scan to specific namespaces, add the `--namespace` flag to the `args` array: + +```yaml +args: + - "kubernetes" + - "--push-to-cloud" + - "--namespace" + - "production,staging" +``` + +## Step 4: Deploy and Verify + +Apply the CronJob to the cluster: + +```console +kubectl apply -f prowler-cronjob.yaml +``` + +To trigger an immediate test run without waiting for the schedule: + +```console +kubectl create job prowler-test-run --from=cronjob/prowler-k8s-scan -n prowler-ns +``` + +Monitor the job execution: + +```console +kubectl get pods -n prowler-ns -l app=prowler --watch +``` + +Check the logs to confirm findings were pushed successfully: + +```console +kubectl logs -n prowler-ns -l app=prowler --tail=50 +``` + +A successful upload produces output similar to: + +``` +Pushing findings to Prowler Cloud, please wait... + +Findings successfully pushed to Prowler Cloud. Ingestion job: fa8bc8c5-4925-46a0-9fe0-f6575905e094 +See more details here: https://cloud.prowler.com/scans +``` + +## Step 5: View Findings in Prowler Cloud + +Once the job completes and findings are pushed: + +1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) +2. Open the "Scans" section to verify the ingestion job status +3. Browse findings under the Kubernetes provider + +For details on the ingestion workflow and status tracking, refer to the [Import Findings](/user-guide/tutorials/prowler-app-import-findings) documentation. + +## Tips and Troubleshooting + +* **Resource limits**: For large clusters, consider setting `resources.requests` and `resources.limits` on the container to prevent the scan from consuming excessive cluster resources. +* **Network policies**: Ensure the Prowler pod can reach `api.prowler.com` over HTTPS (port 443). Adjust NetworkPolicies or egress rules if needed. +* **Job history**: Kubernetes retains completed and failed jobs by default. Set `successfulJobsHistoryLimit` and `failedJobsHistoryLimit` in the CronJob spec to control cleanup: + + ```yaml + spec: + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + ``` + +* **API key rotation**: When rotating the API key, update the Secret and restart any running jobs: + + ```console + kubectl delete secret prowler-cloud-api-key -n prowler-ns + kubectl create secret generic prowler-cloud-api-key \ + --from-literal=api-key=pk_new_api_key_here \ + --namespace prowler-ns + ``` + +* **Failed uploads**: If the push to Prowler Cloud fails, the scan still completes and findings are saved locally in the container. Check the [Import Findings troubleshooting section](/user-guide/tutorials/prowler-app-import-findings#troubleshooting) for common error messages. diff --git a/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx b/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx index 0ec8215776..c4f4822792 100644 --- a/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx +++ b/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx @@ -164,3 +164,7 @@ env: ``` + + +To set up a production-ready CronJob that runs Prowler on a schedule and sends findings to Prowler Cloud, see the [Run Kubernetes In-Cluster and Send Findings to Prowler Cloud](/user-guide/cookbooks/kubernetes-in-cluster) cookbook. + diff --git a/docs/user-guide/tutorials/prowler-app-attack-paths.mdx b/docs/user-guide/tutorials/prowler-app-attack-paths.mdx index 41047b8c2c..646ee557de 100644 --- a/docs/user-guide/tutorials/prowler-app-attack-paths.mdx +++ b/docs/user-guide/tutorials/prowler-app-attack-paths.mdx @@ -201,3 +201,140 @@ To expand the graph for detailed exploration, click the fullscreen icon in the g alt="Attack Paths fullscreen mode with graph and node detail side panel" width="700" /> + +## Using Attack Paths with the MCP Server and Lighthouse AI + +Attack Paths capabilities are also available through the [Prowler MCP Server](/getting-started/products/prowler-mcp), enabling interaction with Attack Paths data via AI assistants like Claude Desktop, Cursor, and other MCP clients. + +[Prowler Lighthouse AI](/getting-started/products/prowler-lighthouse-ai) also supports Attack Paths queries, allowing you to analyze privilege escalation chains and security misconfigurations directly from the chat interface. + +The following MCP tools are available for Attack Paths: + +- **`prowler_app_list_attack_paths_scans`** - List and filter Attack Paths scans +- **`prowler_app_list_attack_paths_queries`** - Discover available queries for a completed scan +- **`prowler_app_run_attack_paths_query`** - Execute a query and retrieve graph results with nodes and relationships +- **`prowler_app_get_attack_paths_cartography_schema`** - Retrieve the Cartography graph schema for custom openCypher queries + +### Example Questions + +Ask through the MCP Server or Lighthouse AI: + +- "Find EC2 instances exposed to the internet with access to sensitive S3 buckets" +- "Are there any IAM roles that can escalate their own privileges?" +- "Show me all internet-facing resources with open security groups" +- "Which principals can create Lambda functions with privileged roles?" +- "List all RDS instances with storage encryption disabled" +- "Find S3 buckets that allow anonymous access" +- "Are there any CloudFormation stacks that could be hijacked for privilege escalation?" +- "Show me all roles that can be assumed for lateral movement" + +### Supported Queries + +Attack Paths currently supports the following built-in queries for AWS: + +#### Custom Attack Path Queries + +| Query | Description | +|---|---| +| **Internet-Exposed EC2 with Sensitive S3 Access** | Find SSH-exposed EC2 instances that can assume roles to read tagged sensitive S3 buckets | + +#### Basic Resource Queries + +| Query | Description | +|---|---| +| **RDS Instances Inventory** | List all provisioned RDS database instances in the account | +| **Unencrypted RDS Instances** | Find RDS instances with storage encryption disabled | +| **S3 Buckets with Anonymous Access** | Find S3 buckets that allow anonymous access | +| **IAM Statements Allowing All Actions** | Find IAM policy statements that allow all actions via wildcard (\*) | +| **IAM Statements Allowing Policy Deletion** | Find IAM policy statements that allow iam:DeletePolicy | +| **IAM Statements Allowing Create Actions** | Find IAM policy statements that allow any create action | + +#### Network Exposure Queries + +| Query | Description | +|---|---| +| **Internet-Exposed EC2 Instances** | Find EC2 instances flagged as exposed to the internet | +| **Open Security Groups on Internet-Facing Resources** | Find internet-facing resources with security groups allowing inbound from 0.0.0.0/0 | +| **Internet-Exposed Classic Load Balancers** | Find Classic Load Balancers exposed to the internet with their listeners | +| **Internet-Exposed ALB/NLB Load Balancers** | Find ELBv2 (ALB/NLB) load balancers exposed to the internet with their listeners | +| **Resource Lookup by Public IP** | Find the AWS resource associated with a given public IP address | + +#### Privilege Escalation Queries + +These queries are based on research from [pathfinding.cloud](https://pathfinding.cloud) by Datadog. + +| Query | Description | +|---|---| +| **App Runner Service Creation with Privileged Role (APPRUNNER-001)** | Create an App Runner service with a privileged IAM role to gain its permissions | +| **App Runner Service Update for Role Access (APPRUNNER-002)** | Update an existing App Runner service to leverage its already-attached privileged role | +| **Bedrock Code Interpreter with Privileged Role (BEDROCK-001)** | Create a Bedrock AgentCore Code Interpreter with a privileged role attached | +| **Bedrock Code Interpreter Session Hijacking (BEDROCK-002)** | Start a session on an existing Bedrock code interpreter to exfiltrate its privileged role credentials | +| **CloudFormation Stack Creation with Privileged Role (CLOUDFORMATION-001)** | Create a CloudFormation stack with a privileged role to provision arbitrary AWS resources | +| **CloudFormation Stack Update for Role Access (CLOUDFORMATION-002)** | Update an existing CloudFormation stack to leverage its already-attached privileged service role | +| **CloudFormation StackSet Creation with Privileged Role (CLOUDFORMATION-003)** | Create a CloudFormation StackSet with a privileged execution role to provision arbitrary resources across accounts | +| **CloudFormation StackSet Update with Privileged Role (CLOUDFORMATION-004)** | Update an existing CloudFormation StackSet to inject malicious resources using a privileged execution role | +| **CloudFormation Change Set Privilege Escalation (CLOUDFORMATION-005)** | Create and execute a change set on an existing stack to leverage its privileged service role | +| **CodeBuild Project Creation with Privileged Role (CODEBUILD-001)** | Create a CodeBuild project with a privileged role to execute arbitrary code via a malicious buildspec | +| **CodeBuild Buildspec Override for Role Access (CODEBUILD-002)** | Start a build on an existing CodeBuild project with a buildspec override to execute code with its privileged role | +| **CodeBuild Batch Buildspec Override for Role Access (CODEBUILD-003)** | Start a batch build on an existing CodeBuild project with a buildspec override to execute code with its privileged role | +| **CodeBuild Batch Project Creation with Privileged Role (CODEBUILD-004)** | Create a CodeBuild project configured for batch builds with a privileged role to execute arbitrary code via a malicious buildspec | +| **Data Pipeline Creation with Privileged Role (DATAPIPELINE-001)** | Create a Data Pipeline with a privileged role to execute arbitrary commands on provisioned infrastructure | +| **EC2 Instance Launch with Privileged Role (EC2-001)** | Launch EC2 instances with privileged IAM roles to gain their permissions via IMDS | +| **EC2 Role Hijacking via UserData Injection (EC2-002)** | Inject malicious scripts into EC2 instance userData to gain the attached role's permissions | +| **Spot Instance Launch with Privileged Role (EC2-003)** | Launch EC2 Spot Instances with privileged IAM roles to gain their permissions via IMDS | +| **Launch Template Poisoning for Role Access (EC2-004)** | Inject malicious userData into launch templates that reference privileged roles, no PassRole needed | +| **EC2 Instance Connect SSH Access for Role Credentials (EC2INSTANCECONNECT-003)** | Push a temporary SSH key to an EC2 instance via Instance Connect to access its attached role credentials through IMDS | +| **ECS Service Creation with Privileged Role (ECS-001 - New Cluster)** | Create an ECS cluster and service with a privileged Fargate task role to execute arbitrary code | +| **ECS Task Execution with Privileged Role (ECS-002 - New Cluster)** | Create an ECS cluster and run a one-off Fargate task with a privileged role to execute arbitrary code | +| **ECS Service Creation with Privileged Role (ECS-003 - Existing Cluster)** | Deploy a Fargate service with a privileged role on an existing ECS cluster | +| **ECS Task Execution with Privileged Role (ECS-004 - Existing Cluster)** | Run a one-off Fargate task with a privileged role on an existing ECS cluster | +| **ECS Task Start with Privileged Role on EC2 (ECS-005 - Existing Cluster)** | Register a task definition with a privileged role and start it on an EC2 container instance to execute arbitrary code | +| **ECS Exec Container Hijacking for Role Credentials (ECS-006)** | Shell into a running ECS container via ECS Exec to steal the attached task role's credentials | +| **Glue Dev Endpoint with Privileged Role (GLUE-001)** | Create a Glue development endpoint with a privileged role attached to gain its permissions | +| **Glue Dev Endpoint SSH Hijacking via Update (GLUE-002)** | Update an existing Glue development endpoint to inject an SSH public key and access its attached role credentials | +| **Glue Job Creation with Privileged Role (GLUE-003)** | Create a Glue job with a privileged role and start it to execute arbitrary code with that role's permissions | +| **Glue Job Creation with Scheduled Trigger and Privileged Role (GLUE-004)** | Create a Glue job with a privileged role and a scheduled trigger to persistently execute arbitrary code | +| **Glue Job Hijacking via Update with Privileged Role (GLUE-005)** | Update an existing Glue job to attach a privileged role and inject malicious code, then start it to gain that role's permissions | +| **Glue Job Hijacking with Scheduled Trigger and Privileged Role (GLUE-006)** | Update an existing Glue job to attach a privileged role and inject malicious code, then create a scheduled trigger for persistent automated execution | +| **Policy Version Override for Self-Escalation (IAM-001)** | Create a new version of an attached policy with administrative permissions, instantly escalating the principal's own privileges | +| **Access Key Creation for Lateral Movement (IAM-002)** | Create access keys for other IAM users to gain their permissions and move laterally across the account | +| **Access Key Rotation Attack for Lateral Movement (IAM-003)** | Delete and recreate access keys for other IAM users to bypass the two-key limit and gain their permissions | +| **Console Login Profile Creation for Lateral Movement (IAM-004)** | Create console login profiles for other IAM users to access the AWS Console with their permissions | +| **Inline Policy Injection for Self-Escalation (IAM-005)** | Attach an inline policy with administrative permissions to your own role, instantly escalating privileges | +| **Console Password Override for Lateral Movement (IAM-006)** | Change the console password of other IAM users to log in as them and gain their permissions | +| **Inline Policy Injection on User for Self-Escalation (IAM-007)** | Attach an inline policy with administrative permissions to your own IAM user, instantly escalating privileges | +| **Managed Policy Attachment on User for Self-Escalation (IAM-008)** | Attach existing managed policies with administrative permissions to your own IAM user, instantly escalating privileges | +| **Managed Policy Attachment on Role for Self-Escalation (IAM-009)** | Attach existing managed policies with administrative permissions to your own IAM role, instantly escalating privileges | +| **Managed Policy Attachment on Group for Self-Escalation (IAM-010)** | Attach existing managed policies with administrative permissions to a group you belong to, escalating privileges for all group members | +| **Inline Policy Injection on Group for Self-Escalation (IAM-011)** | Attach an inline policy with administrative permissions to a group you belong to, escalating privileges for all group members | +| **Trust Policy Hijacking for Role Assumption (IAM-012)** | Modify a role's trust policy to allow yourself to assume it, gaining the role's permissions | +| **Group Membership Hijacking for Privilege Escalation (IAM-013)** | Add yourself to a privileged IAM group to inherit its permissions, gaining access to all policies attached to the group | +| **Managed Policy Attachment with Role Assumption for Lateral Movement (IAM-014)** | Attach administrative managed policies to another role you can assume, then assume it to gain elevated privileges | +| **Managed Policy Attachment with Access Key Creation for Lateral Movement (IAM-015)** | Attach administrative managed policies to another IAM user and create access keys for them to gain programmatic access with elevated privileges | +| **Policy Version Override with Role Assumption for Lateral Movement (IAM-016)** | Create a new version of a customer-managed policy attached to another role with administrative permissions, then assume that role to gain elevated access | +| **Inline Policy Injection with Role Assumption for Lateral Movement (IAM-017)** | Attach an inline policy with administrative permissions to another role you can assume, then assume it to gain elevated privileges | +| **Inline Policy Injection with Access Key Creation for Lateral Movement (IAM-018)** | Attach an inline policy with administrative permissions to another IAM user and create access keys for them to gain programmatic access with elevated privileges | +| **Managed Policy Attachment with Trust Policy Hijacking for Privilege Escalation (IAM-019)** | Attach administrative managed policies to a role and modify its trust policy to allow yourself to assume it, gaining elevated privileges without prior assume-role access | +| **Policy Version Override with Trust Policy Hijacking for Privilege Escalation (IAM-020)** | Create a new version of a customer-managed policy attached to a role with administrative permissions and modify its trust policy to assume it, without prior assume-role access | +| **Inline Policy Injection with Trust Policy Hijacking for Privilege Escalation (IAM-021)** | Add an inline policy with administrative permissions to a role and modify its trust policy to allow yourself to assume it, gaining elevated privileges without prior assume-role access | +| **Lambda Function Creation with Privileged Role (LAMBDA-001)** | Create a Lambda function with a privileged IAM role and invoke it to execute code with that role's permissions | +| **Lambda Function Creation with Event Source Trigger (LAMBDA-002)** | Create a Lambda function with a privileged IAM role and an event source mapping to trigger it automatically, executing code with the role's permissions | +| **Lambda Function Code Injection (LAMBDA-003)** | Modify the code of an existing Lambda function to execute arbitrary commands with the function's execution role permissions | +| **Lambda Function Code Injection with Direct Invocation (LAMBDA-004)** | Modify the code of an existing Lambda function and invoke it directly to execute arbitrary commands with the function's execution role permissions | +| **Lambda Function Code Injection with Resource Policy Grant (LAMBDA-005)** | Modify the code of an existing Lambda function and grant yourself invocation permission via its resource-based policy to execute code with the function's execution role | +| **Lambda Function Creation with Resource Policy Invocation (LAMBDA-006)** | Create a Lambda function with a privileged IAM role and grant yourself invocation permission via its resource-based policy to execute code with the role's permissions | +| **SageMaker Notebook Creation with Privileged Role (SAGEMAKER-001)** | Create a SageMaker notebook instance with a privileged IAM role to execute arbitrary code with the role's permissions via the Jupyter environment | +| **SageMaker Training Job Creation with Privileged Role (SAGEMAKER-002)** | Create a SageMaker training job with a privileged IAM role to execute arbitrary container code with the role's permissions | +| **SageMaker Processing Job Creation with Privileged Role (SAGEMAKER-003)** | Create a SageMaker processing job with a privileged IAM role to execute arbitrary container code with the role's permissions | +| **SageMaker Presigned Notebook URL for Privilege Escalation (SAGEMAKER-004)** | Generate a presigned URL to access an existing SageMaker notebook instance and execute code with its execution role's permissions | +| **SageMaker Notebook Lifecycle Config Injection (SAGEMAKER-005)** | Inject a malicious lifecycle configuration into an existing SageMaker notebook to execute code with the notebook's execution role during startup | +| **SSM Session Access for EC2 Role Credentials (SSM-001)** | Start an SSM session on an EC2 instance to access its attached role credentials through IMDS | +| **SSM Send Command for EC2 Role Credentials (SSM-002)** | Execute commands on an EC2 instance via SSM Run Command to access its attached role credentials through IMDS | +| **Role Assumption for Privilege Escalation (STS-001)** | Assume IAM roles with elevated permissions by exploiting bidirectional trust between the starting principal and the target role | + +These tools enable workflows such as: +- Asking an AI assistant to identify privilege escalation paths in a specific AWS account +- Automating attack path analysis across multiple scans +- Combining attack path data with findings and compliance information for comprehensive security reports + +For the complete list of MCP tools, see the [Tools Reference](/getting-started/basic-usage/prowler-mcp-tools#attack-paths-analysis). diff --git a/docs/user-guide/tutorials/prowler-app-mute-findings.mdx b/docs/user-guide/tutorials/prowler-app-mute-findings.mdx index 64e9a0c8f4..82623b54cf 100644 --- a/docs/user-guide/tutorials/prowler-app-mute-findings.mdx +++ b/docs/user-guide/tutorials/prowler-app-mute-findings.mdx @@ -24,6 +24,11 @@ Advanced Mutelist enables users to create powerful, pattern-based muting rules u ## Prerequisites + +Advanced Mutelist requires the **Manage Account** permission. See [RBAC Administrative Permissions](/user-guide/tutorials/prowler-app-rbac#rbac-administrative-permissions) for details. + + + Before muting findings, ensure: - Valid access to Prowler App with appropriate permissions @@ -72,10 +77,10 @@ If the YAML configuration is invalid, an error message will be displayed 2. Navigate to the Findings page to verify muted findings ![Check muted findings](/images/mutelist-ui-9.png) - -The Advanced Mutelist configuration takes effect on subsequent scans. Existing findings are not retroactively muted. + +The Advanced Mutelist configuration takes effect on **subsequent scans only**. Existing findings from previous scans are **not** retroactively muted. You must run a new scan after saving your YAML configuration to see its effect. Similarly, removing a pattern from the YAML configuration will only stop muting new findings generated by subsequent scans. - + ## YAML Configuration Examples Below are ready-to-use examples for different cloud providers. For detailed syntax and logic explanation, see [CLI Mutelist documentation](/user-guide/cli/tutorials/mutelist#how-the-mutelist-works). @@ -416,6 +421,10 @@ Mutelist: Description: "Mute findings for dev/test environments in alpha project" ``` +### Priority: Advanced vs. Simple Mutelist + +When both Advanced Mutelist (YAML) and [Simple Mutelist](/user-guide/tutorials/prowler-app-simple-mutelist) rules match the same finding, the **Advanced Mutelist takes higher priority**. The finding will be muted with the reason "Muted by mutelist". If a finding is not matched by the Advanced Mutelist but matches a Simple Mutelist rule, the Simple rule's custom justification is used instead. + ### Best Practices 1. **Start Small**: Begin with specific resources and gradually expand diff --git a/docs/user-guide/tutorials/prowler-app-simple-mutelist.mdx b/docs/user-guide/tutorials/prowler-app-simple-mutelist.mdx index d6226f7c5f..c0bd7db9b4 100644 --- a/docs/user-guide/tutorials/prowler-app-simple-mutelist.mdx +++ b/docs/user-guide/tutorials/prowler-app-simple-mutelist.mdx @@ -23,6 +23,11 @@ Simple Mutelist creates rules based on the finding's unique identifier (UID). Fo + +Simple Mutelist requires the **Manage Scans** permission. See [RBAC Administrative Permissions](/user-guide/tutorials/prowler-app-rbac#rbac-administrative-permissions) for details. + + + ## Accessing the Mutelist Page To access the Mutelist page: @@ -85,10 +90,10 @@ To toggle a mute rule without deleting it: 3. Locate the mute rule 4. Use the toggle switch in the "Enabled" column to enable or disable the rule - -Disabled mute rules remain in the system but do not affect findings. Findings associated with disabled rules will appear as unmuted in subsequent scans. + +Disabling a mute rule does not retroactively unmute existing findings that were already marked as muted. Those findings retain their muted status as point-in-time historical records. Only **new findings** generated by subsequent scans will appear as unmuted. - + ### Editing Mute Rules @@ -112,7 +117,7 @@ To permanently remove a mute rule: 5. Confirm the deletion -Deleting a mute rule is permanent. The finding will appear as unmuted in subsequent scans. To temporarily unmute a finding without losing the rule, disable the rule instead of deleting it. +Deleting a mute rule is permanent and cannot be undone. Existing findings that were already muted retain their muted status as historical records — only **new findings** from subsequent scans will appear as unmuted. To temporarily stop muting new findings without losing the rule, disable the rule instead of deleting it. @@ -124,9 +129,13 @@ Simple Mutelist creates mute rules based on a finding's unique identifier (UID). - **Historical findings** with the same UID are also muted - **Future findings** from subsequent scans are automatically muted if they match the UID +### Bulk Muting and Grouping + +When muting multiple findings at once, a single mute rule is created containing all selected finding UIDs. However, once a rule is created, **additional findings cannot be added to an existing rule**. To mute new findings, create a separate mute rule. + ### Uniqueness Constraint -Each finding UID can only have one mute rule. Attempting to create a duplicate mute rule for the same finding displays an error message indicating the rule already exists. +Each finding UID can only belong to one **enabled** mute rule at a time. Attempting to create a mute rule that includes a finding UID already covered by another enabled rule displays a conflict error. If you need to reorganize mute rules, disable or delete the existing rule first, then create a new one. ## Simple Mutelist vs. Advanced Mutelist @@ -134,8 +143,12 @@ Each finding UID can only have one mute rule. Attempting to create a duplicate m | ------------------------ | ----------------------------------------- | ------------------------------------------------------ | | **Configuration method** | Point-and-click interface | YAML configuration file | | **Muting scope** | Individual finding UIDs | Patterns based on checks, regions, resources, and tags | +| **When muting applies** | Immediately (current + historical findings) | On subsequent scans only (not retroactive) | +| **Unmuting behavior** | Disabling/deleting a rule only affects new findings from subsequent scans | Removing a pattern stops muting on the next scan | +| **Adding findings later** | Not supported — must create a new rule | Automatic — any finding matching the pattern is muted | | **Regular expressions** | Not supported | Fully supported | | **Bulk operations** | Checkbox selection in Findings table | YAML wildcards and patterns | +| **Priority** | Applied after Advanced Mutelist | Highest priority | | **Best for** | Quick, ad-hoc muting of specific findings | Complex, policy-driven muting rules | ### When to Use Simple Mutelist @@ -170,6 +183,14 @@ If an error indicates a mute rule already exists for a finding: 3. Edit the existing rule's justification if needed, or 4. Delete the existing rule and create a new one +### Finding Still Appears Muted After Disabling or Deleting a Rule + +If a finding still appears as muted after disabling or deleting its mute rule: + +1. This is expected behavior — existing findings retain their muted status as historical records +2. Run a new scan to generate new findings that will reflect the updated rule state +3. New findings with the same UID will appear with their actual status (PASS/FAIL) instead of muted + ### Finding Still Appears Unmuted If a muted finding still appears unmuted: 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 bf7809a6e9..2c8b47daf4 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,12 +2,62 @@ All notable changes to the **Prowler SDK** are documented in this file. -## [5.20.0] (Prowler UNRELEASED) +## [5.21.2] (Prowler UNRELEASED) + +### 🐞 Fixed + +- Azure MySQL flexible server checks now compare configuration values case-insensitively to avoid false negatives when Azure returns lowercase values [(#10396)](https://github.com/prowler-cloud/prowler/pull/10396) +- Azure `vm_backup_enabled` and `vm_sufficient_daily_backup_retention_period` checks now compare VM names case-insensitively to avoid false negatives when Azure stores backup item names in a different case [(#10395)](https://github.com/prowler-cloud/prowler/pull/10395) + +--- + +## [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) +- `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 @@ -30,6 +80,10 @@ All notable changes to the **Prowler SDK** are documented in this file. - Update Oracle Cloud Object Storage service metadata to new format [(#9379)](https://github.com/prowler-cloud/prowler/pull/9379) - Update Oracle Cloud Events service metadata to new format [(#9373)](https://github.com/prowler-cloud/prowler/pull/9373) - Update Oracle Cloud Identity service metadata to new format [(#9375)](https://github.com/prowler-cloud/prowler/pull/9375) +- Update Alibaba Cloud services metadata to new format [(#10289)](https://github.com/prowler-cloud/prowler/pull/10289) +- Update M365 Admin Center service metadata to new format [(#9680)](https://github.com/prowler-cloud/prowler/pull/9680) +- Update M365 Defender service metadata to new format [(#9681)](https://github.com/prowler-cloud/prowler/pull/9681) +- Update M365 Purview service metadata to new format [(#9092)](https://github.com/prowler-cloud/prowler/pull/9092) --- @@ -103,6 +157,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🐞 Fixed +- Google Workspace provider `test_connection()` missing `provider_id` parameter for API integration [(#10247)](https://github.com/prowler-cloud/prowler/pull/10247) - Update AWS checks metadata URLs to replace deprecated Trend Micro CloudOne Conformity (EOL July 2026) with Vision One and remove docs.prowler.com references [(#10068)](https://github.com/prowler-cloud/prowler/pull/10068) - Standardize resource_id values across Azure checks to use actual Azure resource IDs and prevent duplicate resource entries [(#9994)](https://github.com/prowler-cloud/prowler/pull/9994) - VPC endpoint service collection filtering third-party services that caused AccessDenied errors on `DescribeVpcEndpointServicePermissions` [(#10152)](https://github.com/prowler-cloud/prowler/pull/10152) 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/config/config.yaml b/prowler/config/config.yaml index 4682afc7af..dcff0a6169 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -72,6 +72,11 @@ aws: # trusted_account_ids : ["123456789012", "098765432109", "678901234567"] trusted_account_ids: [] + # AWS OpenSearch Configuration (opensearch_service_domains_not_publicly_accessible) + # Trusted IP addresses or CIDR ranges that should not be considered as public access, e.g. + # trusted_ips: ["1.2.3.4", "10.0.0.0/8"] + trusted_ips: [] + # AWS Cloudwatch Configuration # aws.cloudwatch_log_group_retention_policy_specific_days_enabled --> by default is 365 days log_group_retention_days: 365 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 cb49461ce0..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 @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "actiontrail_multi_region_enabled", - "CheckTitle": "ActionTrail are configured to export copies of all Log entries", - "CheckType": [ - "Unusual logon", - "Cloud threat detection" - ], + "CheckTitle": "ActionTrail is configured to export copies of all log entries across all regions", + "CheckType": [], "ServiceName": "actiontrail", "SubServiceName": "", - "ResourceIdTemplate": "acs:actiontrail::account-id:trail", + "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "AlibabaCloudActionTrail", + "ResourceType": "ALIYUN::ACTIONTRAIL::Trail", "ResourceGroup": "monitoring", - "Description": "**ActionTrail** is a web service that records API calls for your account and delivers log files to you.\n\nThe recorded information includes the identity of the API caller, the time of the API call, the source IP address of the API caller, the request parameters, and the response elements returned by the Alibaba Cloud service. ActionTrail provides a history of API calls for an account, including API calls made via the Management Console, SDKs, and command line tools.", - "Risk": "The API call history produced by ActionTrail enables **security analysis**, **resource change tracking**, and **compliance auditing**.\n\nEnsuring that a **multi-region trail** exists will detect unexpected activities occurring in otherwise unused regions. Global Service Logging should be enabled by default to capture events generated on Alibaba Cloud global services, ensuring the recording of management operations performed on all resources in an Alibaba Cloud account.", + "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": [ "https://www.alibabacloud.com/help/doc-detail/28829.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ActionTrail/enable-multi-region-trails.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ActionTrail/enable-multi-region-trails.html" ], "Remediation": { "Code": { "CLI": "aliyun actiontrail CreateTrail --Name --OssBucketName --RoleName aliyunactiontraildefaultrole --SlsProjectArn --SlsWriteRoleArn --EventRW ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ActionTrail Console**\n2. Click on **Trails** in the left navigation pane\n3. Click **Add new trail**\n4. Enter a trail name in the `Trail name` box\n5. Set **Yes** for `Apply Trail to All Regions`\n6. Specify an OSS bucket name in the `OSS bucket` box\n7. Specify an SLS project name in the `SLS project` box\n8. Click **Create**", "Terraform": "resource \"alicloud_actiontrail_trail\" \"example\" {\n trail_name = \"multi-region-trail\"\n trail_region = \"All\"\n sls_project_arn = \"acs:log:cn-hangzhou:123456789:project/actiontrail-project\"\n sls_write_role_arn = data.alicloud_ram_roles.actiontrail.roles.0.arn\n}" }, "Recommendation": { - "Text": "1. Log on to the **ActionTrail Console**\n2. Click on **Trails** in the left navigation pane\n3. Click **Add new trail**\n4. Enter a trail name in the `Trail name` box\n5. Set **Yes** for `Apply Trail to All Regions`\n6. Specify an OSS bucket name in the `OSS bucket` box\n7. Specify an SLS project name in the `SLS project` box\n8. Click **Create**", + "Text": "Enable a multi-region trail in ActionTrail to ensure all API calls across all regions are recorded and delivered to a centralized OSS bucket and SLS project for security analysis and compliance auditing.", "Url": "https://hub.prowler.com/check/actiontrail_multi_region_enabled" } }, diff --git a/prowler/providers/alibabacloud/services/actiontrail/actiontrail_oss_bucket_not_publicly_accessible/actiontrail_oss_bucket_not_publicly_accessible.metadata.json b/prowler/providers/alibabacloud/services/actiontrail/actiontrail_oss_bucket_not_publicly_accessible/actiontrail_oss_bucket_not_publicly_accessible.metadata.json index 7505a7404a..55daf22591 100644 --- a/prowler/providers/alibabacloud/services/actiontrail/actiontrail_oss_bucket_not_publicly_accessible/actiontrail_oss_bucket_not_publicly_accessible.metadata.json +++ b/prowler/providers/alibabacloud/services/actiontrail/actiontrail_oss_bucket_not_publicly_accessible/actiontrail_oss_bucket_not_publicly_accessible.metadata.json @@ -1,32 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "actiontrail_oss_bucket_not_publicly_accessible", - "CheckTitle": "The OSS used to store ActionTrail logs is not publicly accessible", - "CheckType": [ - "Sensitive file tampering" - ], + "CheckTitle": "The OSS bucket used to store ActionTrail logs is not publicly accessible", + "CheckType": [], "ServiceName": "actiontrail", "SubServiceName": "", - "ResourceIdTemplate": "acs:oss::account-id:bucket-name", + "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "AlibabaCloudOSSBucket", + "ResourceType": "ALIYUN::ACTIONTRAIL::Trail", "ResourceGroup": "storage", - "Description": "**ActionTrail** logs a record of every API call made in your Alibaba Cloud account. These log files are stored in an **OSS bucket**.\n\nIt is recommended that the **Access Control List (ACL)** of the OSS bucket, which ActionTrail logs to, prevents public access to the ActionTrail logs.", - "Risk": "Allowing **public access** to ActionTrail log content may aid an adversary in identifying weaknesses in the affected account's use or configuration.\n\nExposed audit logs can reveal sensitive information about your infrastructure, API usage patterns, and security configurations.", + "Description": "**Alibaba Cloud ActionTrail** logs a record of every API call made in your account and stores these log files in an **OSS bucket**. It is recommended that the **Access Control List (ACL)** of the OSS bucket used by ActionTrail is set to `private` to prevent unauthorized public access to sensitive audit log data.", + "Risk": "Allowing **public access** to the OSS bucket containing ActionTrail logs may expose sensitive information about your infrastructure, API usage patterns, and security configurations. An adversary could use this information to identify weaknesses in the affected account, leading to potential **data breaches**, **privilege escalation**, and **compliance violations**.", "RelatedUrl": "", "AdditionalURLs": [ - "https://help.aliyun.com/document_detail/31954.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ActionTrail/trail-bucket-publicly-accessible.html" + "https://www.alibabacloud.com/help/doc-detail/31954.htm", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ActionTrail/trail-bucket-publicly-accessible.html" ], "Remediation": { "Code": { "CLI": "ossutil set-acl oss:// private -b", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **OSS Console**\n2. Right-click on the bucket and select **Basic Settings**\n3. In the Access Control List pane, click **Configure**\n4. The Bucket ACL tab shows three types of grants: `Private`, `Public Read`, `Public Read/Write`\n5. Ensure **Private** is set for the bucket\n6. Click **Save** to save the ACL", "Terraform": "resource \"alicloud_oss_bucket_public_access_block\" \"actiontrail\" {\n bucket = alicloud_oss_bucket.actiontrail.bucket\n block_public_access = true\n}" }, "Recommendation": { - "Text": "1. Log on to the **OSS Console**\n2. Right-click on the bucket and select **Basic Settings**\n3. In the Access Control List pane, click **Configure**\n4. The Bucket ACL tab shows three types of grants: `Private`, `Public Read`, `Public Read/Write`\n5. Ensure **Private** is set for the bucket\n6. Click **Save** to save the ACL", + "Text": "Set the ACL of the OSS bucket used to store ActionTrail logs to private to prevent unauthorized public access to sensitive audit log data.", "Url": "https://hub.prowler.com/check/actiontrail_oss_bucket_not_publicly_accessible" } }, 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 7685aa4f2e..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 @@ -1,32 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "cs_kubernetes_cloudmonitor_enabled", - "CheckTitle": "CloudMonitor is set to Enabled on Kubernetes Engine Clusters", - "CheckType": [ - "Threat detection during container runtime" - ], + "CheckTitle": "Kubernetes cluster has CloudMonitor enabled", + "CheckType": [], "ServiceName": "cs", "SubServiceName": "", - "ResourceIdTemplate": "acs:cs:region:account-id:cluster/{cluster-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudKubernetesCluster", + "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "The monitoring service in **Kubernetes Engine clusters** depends on the Alibaba Cloud **CloudMonitor** agent to access additional system resources and application services in virtual machine instances.\n\nThe monitor can access metrics about CPU utilization, disk traffic metrics, network traffic, and disk IO information, which help monitor signals and build operations in your Kubernetes Engine clusters.", - "Risk": "Without **CloudMonitor** enabled, you lack visibility into system metrics and custom metrics. System metrics measure the cluster's infrastructure, such as CPU or memory usage.\n\nWith CloudMonitor, a monitor controller is created that periodically connects to each node and collects metrics about its Pods and containers, then sends the metrics to CloudMonitor server.", + "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://help.aliyun.com/document_detail/125508.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/enable-cloud-monitor.html" + "https://www.alibabacloud.com/help/en/ack/", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/enable-cloud-monitor.html" ], "Remediation": { "Code": { - "CLI": "aliyun cs GET /clusters/[cluster_id]/nodepools to verify nodepools.kubernetes_config.cms_enabled is set to true for all node pools.", + "CLI": "aliyun cs GET /clusters//nodepools --header 'Content-Type=application/json' | jq '.nodepools[].kubernetes_config.cms_enabled'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ACK Console**.\n2. Select the target cluster and click its name to open the cluster detail page.\n3. Select **Nodes** on the left column and click the **Monitor** link on the Actions column of the selected node.\n4. Verify that OS Metrics data exists in the CloudMonitor page.\n5. To enable: Click **Create Kubernetes Cluster** and set `CloudMonitor Agent` to **Enabled** under creation options.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **ACK Console**\n2. Select the target cluster and click its name to open the cluster detail page\n3. Select **Nodes** on the left column and click the **Monitor** link on the Actions column of the selected node\n4. Verify that OS Metrics data exists in the CloudMonitor page\n5. To enable: Click **Create Kubernetes Cluster** and set `CloudMonitor Agent` to **Enabled** under creation options", + "Text": "Enable the **CloudMonitor** agent during cluster creation by setting `CloudMonitor Agent` to **Enabled**. For existing clusters, verify that `cms_enabled` is set to `true` for all node pools.", "Url": "https://hub.prowler.com/check/cs_kubernetes_cloudmonitor_enabled" } }, 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 ed87a8c2fb..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 @@ -1,32 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "cs_kubernetes_cluster_check_recent", - "CheckTitle": "Cluster Check triggered within configured period for Kubernetes Clusters", - "CheckType": [ - "Threat detection during container runtime" - ], + "CheckTitle": "Kubernetes cluster health check has been triggered within the configured period", + "CheckType": [], "ServiceName": "cs", "SubServiceName": "", - "ResourceIdTemplate": "acs:cs:region:account-id:cluster/{cluster-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudKubernetesCluster", + "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Kubernetes Engine's cluster check** feature helps you verify the system nodes and components healthy status.\n\nWhen you trigger the checking, the process validates the health state of each node in your cluster and also the cluster configuration (`kubelet`, `docker daemon`, `kernel`, and network `iptables` configuration). If there are consecutive health check failures, the diagnose reports to admin for further repair.", - "Risk": "Kubernetes Engine uses the node's health status to determine if a node needs to be repaired. A cluster health check includes: cloud resource healthy status including **VPC/VSwitch**, **SLB**, and every **ECS node** status in the cluster; the `kubelet`, `docker daemon`, `kernel`, `iptables` configurations on every node.\n\nWithout regular cluster checks, potential issues may go undetected and could lead to **cluster instability** or **security vulnerabilities**.", + "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://help.aliyun.com/document_detail/114882.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/cluster-check.html" + "https://www.alibabacloud.com/help/en/ack/", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/cluster-check.html" ], "Remediation": { "Code": { - "CLI": "aliyun cs GET /clusters/[cluster_id]/checks to verify cluster checks are being run regularly. Trigger a check if needed.", + "CLI": "aliyun cs GET /clusters//checks --header 'Content-Type=application/json'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ACK Console**.\n2. Select the target cluster and open the **More** pop-menu for advanced options.\n3. Select **Global Check** and click the **Start** button to trigger the checking.\n4. Verify the checking time and details in Global Check.\n5. It is recommended to trigger cluster checks at least once within the configured period.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **ACK Console**\n2. Select the target cluster and open the **More** pop-menu for advanced options\n3. Select **Global Check** and click the **Start** button to trigger the checking\n4. Verify the checking time and details in Global Check\n5. It is recommended to trigger cluster checks at least once within the configured period (default: weekly)", + "Text": "Trigger a cluster health check regularly within the configured period to ensure all nodes and system components are healthy. Use the **Global Check** feature in the ACK Console or the `aliyun cs` CLI to verify and trigger checks.", "Url": "https://hub.prowler.com/check/cs_kubernetes_cluster_check_recent" } }, 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 db61e88fe2..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 @@ -1,32 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "cs_kubernetes_cluster_check_weekly", - "CheckTitle": "Cluster Check triggered at least once per week for Kubernetes Clusters", - "CheckType": [ - "Threat detection during container runtime" - ], + "CheckTitle": "Kubernetes cluster health check has been triggered at least once per week", + "CheckType": [], "ServiceName": "cs", "SubServiceName": "", - "ResourceIdTemplate": "acs:cs:region:account-id:cluster/{cluster-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudKubernetesCluster", + "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Kubernetes Engine's cluster check** feature helps you verify the system nodes and components healthy status.\n\nWhen you trigger the checking, the process validates the health state of each node in your cluster and also the cluster configuration (`kubelet`, `docker daemon`, `kernel`, and network `iptables` configuration). If there are consecutive health check failures, the diagnose reports to admin for further repair.", - "Risk": "Kubernetes Engine uses the node's health status to determine if a node needs to be repaired. A cluster health check includes: cloud resource healthy status including **VPC/VSwitch**, **SLB**, and every **ECS node** status in the cluster; the `kubelet`, `docker daemon`, `kernel`, `iptables` configurations on every node.\n\nWithout regular cluster checks, potential issues may go undetected and could lead to **cluster instability** or **security vulnerabilities**.", + "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://help.aliyun.com/document_detail/114882.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/cluster-check.html" + "https://www.alibabacloud.com/help/en/ack/", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/cluster-check.html" ], "Remediation": { "Code": { - "CLI": "aliyun cs GET /clusters/[cluster_id]/checks to verify cluster checks are being run regularly. Trigger a check if needed.", + "CLI": "aliyun cs GET /clusters//checks --header 'Content-Type=application/json'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ACK Console**.\n2. Select the target cluster and open the **More** pop-menu for advanced options.\n3. Select **Global Check** and click the **Start** button to trigger the checking.\n4. Verify the checking time and details in Global Check.\n5. Trigger cluster checks at least once per week.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **ACK Console**\n2. Select the target cluster and open the **More** pop-menu for advanced options\n3. Select **Global Check** and click the **Start** button to trigger the checking\n4. Verify the checking time and details in Global Check\n5. It is recommended to trigger cluster checks at least once per week", + "Text": "Trigger a cluster health check at least once per week to ensure all nodes and system components are healthy. Use the **Global Check** feature in the ACK Console or the `aliyun cs` CLI to verify and trigger checks.", "Url": "https://hub.prowler.com/check/cs_kubernetes_cluster_check_weekly" } }, 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 4d0b3ed936..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 @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "cs_kubernetes_dashboard_disabled", - "CheckTitle": "Kubernetes web UI / Dashboard is not enabled", - "CheckType": [ - "Threat detection during container runtime", - "Unusual logon" - ], + "CheckTitle": "Kubernetes web UI (Dashboard) is disabled on Kubernetes Engine clusters", + "CheckType": [], "ServiceName": "cs", "SubServiceName": "", - "ResourceIdTemplate": "acs:cs:region:account-id:cluster/{cluster-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudKubernetesCluster", + "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Dashboard** is a web-based Kubernetes user interface that can be used to deploy containerized applications to a Kubernetes cluster, troubleshoot your containerized application, and manage the cluster itself.\n\nYou should disable the **Kubernetes Web UI (Dashboard)** when running on Kubernetes Engine. The Dashboard is backed by a highly privileged Kubernetes Service Account. It is recommended to use the **ACK User Console** instead to avoid privilege escalation via a compromised dashboard.", - "Risk": "The **Kubernetes Dashboard** is backed by a highly privileged Service Account. If the Dashboard is compromised, it could allow an attacker to gain **full control** over the cluster and potentially **escalate privileges**.\n\nAttackers who gain access to the Dashboard can deploy malicious workloads, exfiltrate secrets, and compromise the entire 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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/disable-kubernetes-dashboard.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/disable-kubernetes-dashboard.html" ], "Remediation": { "Code": { - "CLI": "Use kubectl to delete the dashboard deployment: kubectl delete deployment kubernetes-dashboard -n kube-system", + "CLI": "kubectl delete deployment kubernetes-dashboard -n kube-system", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ACK Console**.\n2. Select the target cluster and select the `kube-system` namespace in the Namespace pop-menu.\n3. Input `dashboard` in the deploy filter bar.\n4. Make sure there is no result after the filter.\n5. If dashboard exists, delete the deployment by selecting **Delete** in the More pop-menu.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **ACK Console**\n2. Select the target cluster and select the `kube-system` namespace in the Namespace pop-menu\n3. Input `dashboard` in the deploy filter bar\n4. Make sure there is no result after the filter\n5. If dashboard exists, delete the deployment by selecting **Delete** in the More pop-menu", + "Text": "Delete the Kubernetes Dashboard deployment from the `kube-system` namespace using `kubectl` or the ACK Console. Use the **ACK Console** for cluster management instead of the Kubernetes Dashboard.", "Url": "https://hub.prowler.com/check/cs_kubernetes_dashboard_disabled" } }, 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 29392fa152..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 @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "cs_kubernetes_eni_multiple_ip_enabled", - "CheckTitle": "ENI multiple IP mode support for Kubernetes Cluster", - "CheckType": [ - "Threat detection during container runtime", - "Suspicious network connection" - ], + "CheckTitle": "Kubernetes cluster has ENI multiple IP mode enabled", + "CheckType": [], "ServiceName": "cs", "SubServiceName": "", - "ResourceIdTemplate": "acs:cs:region:account-id:cluster/{cluster-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudKubernetesCluster", + "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "Alibaba Cloud **ENI (Elastic Network Interface)** supports assigning ranges of internal IP addresses as aliases to a single virtual machine's ENI network interfaces.\n\nWith **ENI multiple IP mode**, Kubernetes Engine clusters can allocate IP addresses from a CIDR block known to **Terway** network plugin. This makes your cluster more scalable and allows better interaction with other Alibaba Cloud products.", - "Risk": "Without **ENI multiple IP mode** (provided by Terway), pods share the node's network interface in a less scalable way.\n\nUsing ENI multiple IPs allows pod IPs to be reserved within the network ahead of time, preventing conflict with other compute resources, and allows firewall controls for Pods to be applied separately from their nodes.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/enable-multi-ip-mode.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/enable-multi-ip-mode.html" ], "Remediation": { "Code": { - "CLI": "Terway network plugin must be selected during cluster creation to support ENI multiple IP mode.", + "CLI": "aliyun cs GET /clusters/ --header 'Content-Type=application/json' | jq '.parameters.Network'", "NativeIaC": "", - "Other": "", + "Other": "1. When creating a new cluster in the **ACK Console**, select **Terway** in the `Network Plugin` option to enable ENI multiple IP mode support.\n2. Note that existing clusters using **Flannel** cannot be migrated to **Terway**.", "Terraform": "" }, "Recommendation": { - "Text": "When creating a new cluster, select **Terway** in the `Network Plugin` option to enable ENI multiple IP mode support.\n\n**Note:** Existing clusters using Flannel cannot be migrated to Terway.", + "Text": "Select the **Terway** network plugin during cluster creation to enable ENI multiple IP mode. Existing clusters using **Flannel** cannot be migrated to Terway and must be recreated.", "Url": "https://hub.prowler.com/check/cs_kubernetes_eni_multiple_ip_enabled" } }, 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 22405dd674..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 @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "cs_kubernetes_log_service_enabled", - "CheckTitle": "Log Service is set to Enabled on Kubernetes Engine Clusters", - "CheckType": [ - "Threat detection during container runtime" - ], + "CheckTitle": "Kubernetes cluster has Log Service enabled", + "CheckType": [], "ServiceName": "cs", "SubServiceName": "", - "ResourceIdTemplate": "acs:cs:region:account-id:cluster/{cluster-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudKubernetesCluster", + "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "**Log Service** is a complete real-time data logging service on Alibaba Cloud supporting collection, shipping, search, storage, and analysis for logs.\n\nLog Service can automatically collect, process, and store your container and audit logs in a dedicated, persistent datastore. Container logs are collected from your containers, audit logs from the `kube-apiserver` or deployed ingress, and events about cluster activity such as the deletion of Pods or Secrets.", - "Risk": "Without **Log Service** enabled, you lose visibility into container and system logs. The per-node logging agent collects: `kube-apiserver` audit logs, ingress visiting logs, and standard output/error logs from containerized processes.\n\nLack of logging makes **incident investigation**, **compliance auditing**, and **security monitoring** significantly more difficult.", + "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://help.aliyun.com/document_detail/91406.html", - "https://help.aliyun.com/document_detail/86532.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/enable-log-service.html" + "https://www.alibabacloud.com/help/en/ack/", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/enable-log-service.html" ], "Remediation": { "Code": { - "CLI": "aliyun cs GET /clusters/[cluster_id] to verify AuditProjectName is set. When creating a new cluster, set Enable Log Service to Enabled.", + "CLI": "aliyun cs GET /clusters/ --header 'Content-Type=application/json' | jq '.meta_data' | jq -r 'fromjson | .AuditProjectName'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ACK Console**.\n2. Select the target cluster and click its name to open the cluster detail page.\n3. Select **Cluster Auditing** on the left column and check if the audit page is shown.\n4. To enable: When creating a new cluster, set `Enable Log Service` to **Enabled**.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **ACK Console**\n2. Select the target cluster and click its name to open the cluster detail page\n3. Select **Cluster Auditing** on the left column and check if the audit page is shown\n4. To enable: When creating a new cluster, set `Enable Log Service` to **Enabled**", + "Text": "Enable **Log Service** during cluster creation by setting `Enable Log Service` to **Enabled**. For existing clusters, verify that `AuditProjectName` is configured in the cluster metadata.", "Url": "https://hub.prowler.com/check/cs_kubernetes_log_service_enabled" } }, 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 e7fddd4694..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 @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "cs_kubernetes_network_policy_enabled", - "CheckTitle": "Network policy is enabled on Kubernetes Engine Clusters", - "CheckType": [ - "Threat detection during container runtime", - "Suspicious network connection" - ], + "CheckTitle": "Kubernetes cluster has Network policy enabled", + "CheckType": [], "ServiceName": "cs", "SubServiceName": "", - "ResourceIdTemplate": "acs:cs:region:account-id:cluster/{cluster-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudKubernetesCluster", + "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "A **Network Policy** is a specification of how groups of pods are allowed to communicate with each other and other network endpoints.\n\n`NetworkPolicy` resources use labels to select pods and define rules which specify what traffic is allowed. By default, pods are non-isolated and accept traffic from any source. Pods become isolated by having a NetworkPolicy that selects them.", - "Risk": "Without **Network Policies**, all pods in a Kubernetes cluster can communicate with each other freely. This open communication model allows an attacker who compromises a single pod to potentially move **laterally** within the cluster and access sensitive services or data.\n\nNetwork Policies are essential for implementing **defense in depth** and **least privilege** networking.", + "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://help.aliyun.com/document_detail/97621.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/enable-network-policy-support.html" + "https://www.alibabacloud.com/help/en/ack/", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/enable-network-policy-support.html" ], "Remediation": { "Code": { - "CLI": "Network Policy support (Terway) must be selected during cluster creation.", + "CLI": "aliyun cs GET /clusters/ --header 'Content-Type=application/json' | jq '.parameters.Network'", "NativeIaC": "", - "Other": "", + "Other": "1. When creating a new cluster in the **ACK Console**, select **Terway** in the `Network Plugin` option to enable Network Policy support.\n2. Note that existing clusters using **Flannel** cannot be migrated to **Terway**.", "Terraform": "" }, "Recommendation": { - "Text": "Only the **Terway** network plugin supports the Network Policy feature. When creating a new cluster, select **Terway** in the `Network Plugin` option.\n\n**Note:** Existing clusters using Flannel cannot be migrated to Terway.", + "Text": "Select the **Terway** network plugin during cluster creation to enable Network Policy support. Existing clusters using **Flannel** cannot be migrated to Terway and must be recreated.", "Url": "https://hub.prowler.com/check/cs_kubernetes_network_policy_enabled" } }, 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 4ea2cf3893..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 @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "cs_kubernetes_private_cluster_enabled", - "CheckTitle": "Kubernetes Cluster is created with Private cluster enabled", - "CheckType": [ - "Threat detection during container runtime", - "Unusual logon" - ], + "CheckTitle": "Kubernetes cluster is created with private cluster enabled", + "CheckType": [], "ServiceName": "cs", "SubServiceName": "", - "ResourceIdTemplate": "acs:cs:region:account-id:cluster/{cluster-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudKubernetesCluster", + "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "A **private cluster** is a cluster that makes your master inaccessible from the public internet.\n\nIn a private cluster, nodes do not have public IP addresses, so your workloads run in an environment that is isolated from the internet. Nodes and masters communicate with each other privately using **VPC peering**.", - "Risk": "Exposing the **API server endpoint** to the public internet increases the attack surface of your cluster. Attackers can attempt to probe for vulnerabilities, perform **brute force attacks**, or exploit misconfigurations if the API server is publicly accessible.\n\nUsing a private cluster significantly reduces network security risks.", + "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://help.aliyun.com/document_detail/100380.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/private-cluster.html" + "https://www.alibabacloud.com/help/en/ack/", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/private-cluster.html" ], "Remediation": { "Code": { - "CLI": "Public access settings cannot be easily changed for existing clusters. Ensure Public Access is disabled during creation.", + "CLI": "aliyun cs GET /clusters/ --header 'Content-Type=application/json' | jq '.external_loadbalancer_id'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ACK Console**.\n2. Select the target cluster name and go to the cluster detail page.\n3. Check if there is no `API Server Public Network Endpoint` under Cluster Information.\n4. When creating a new cluster, make sure **Public Access** is not enabled.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **ACK Console**\n2. Select the target cluster name and go to the cluster detail page\n3. Check if there is no `API Server Public Network Endpoint` under Cluster Information\n4. When creating a new cluster, make sure **Public Access** is not enabled", + "Text": "Disable **Public Access** during cluster creation to ensure the API server is not exposed to the public internet. For existing clusters, remove the public endpoint if one was configured.", "Url": "https://hub.prowler.com/check/cs_kubernetes_private_cluster_enabled" } }, 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 bd4fa293ce..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 @@ -1,34 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "cs_kubernetes_rbac_enabled", - "CheckTitle": "Role-based access control (RBAC) authorization is Enabled on Kubernetes Engine Clusters", - "CheckType": [ - "Threat detection during container runtime", - "Abnormal account" - ], + "CheckTitle": "Kubernetes cluster has RBAC authorization enabled", + "CheckType": [], "ServiceName": "cs", "SubServiceName": "", - "ResourceIdTemplate": "acs:cs:region:account-id:cluster/{cluster-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudKubernetesCluster", + "ResourceType": "ALIYUN::CS::ManagedKubernetesCluster", "ResourceGroup": "container", - "Description": "In Kubernetes, authorizers interact by granting a permission if any authorizer grants the permission. The legacy authorizer in Kubernetes Engine grants broad, statically defined permissions.\n\nTo ensure that **RBAC** limits permissions correctly, you must disable the legacy authorizer. RBAC has significant security advantages, helps ensure that users only have access to specific cluster resources within their own namespace, and is now stable in Kubernetes.", - "Risk": "In Kubernetes, **RBAC** is used to grant permissions to resources at the cluster and namespace level. RBAC allows you to define roles with rules containing a set of permissions.\n\nWithout RBAC, legacy authorization mechanisms like **ABAC** grant **overly broad permissions**, increasing the risk of unauthorized access and privilege escalation.", + "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://help.aliyun.com/document_detail/87656.html", - "https://help.aliyun.com/document_detail/119596.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/enable-rbac-authorization.html" + "https://www.alibabacloud.com/help/en/ack/", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ACK/enable-rbac-authorization.html" ], "Remediation": { "Code": { - "CLI": "RBAC is enabled by default on new ACK clusters. Verify cluster authorization configuration.", + "CLI": "aliyun cs GET /clusters/ --header 'Content-Type=application/json' | jq '.parameters.KubernetesVersion'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ACK Console**.\n2. Navigate to **Clusters** -> **Authorizations** page.\n3. Select the target RAM sub-account and configure the RBAC roles on specific clusters or namespaces.\n4. Ensure **RBAC** is enabled and legacy ABAC authorization is disabled.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **ACK Console**\n2. Navigate to **Clusters** -> **Authorizations** page\n3. Select the target RAM sub-account and configure the RBAC roles on specific clusters or namespaces\n4. Ensure **RBAC** is enabled and legacy ABAC authorization is disabled", + "Text": "Ensure **RBAC** is enabled on all Kubernetes Engine clusters and that legacy **ABAC** authorization is disabled. Configure RBAC roles and bindings through the ACK Console Authorizations page to enforce least-privilege access.", "Url": "https://hub.prowler.com/check/cs_kubernetes_rbac_enabled" } }, 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 b6d14c101b..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 @@ -1,32 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ecs_attached_disk_encrypted", - "CheckTitle": "Virtual Machines disk are encrypted", - "CheckType": [ - "Sensitive file tampering" - ], + "CheckTitle": "ECS attached disk is encrypted", + "CheckType": [], "ServiceName": "ecs", "SubServiceName": "", - "ResourceIdTemplate": "acs:ecs:region:account-id:disk/{disk-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudECSDisk", + "ResourceType": "ALIYUN::ECS::Disk", "ResourceGroup": "storage", - "Description": "**ECS cloud disk encryption** protects your data at rest. The cloud disk data encryption feature automatically encrypts data when data is transferred from ECS instances to disks, and decrypts data when read from disks.\n\nEnsure that disks are encrypted when they are created with the creation of VM instances.", - "Risk": "**Unencrypted disks** attached to ECS instances pose a security risk as they may contain sensitive data that could be accessed if the disk is compromised or accessed by unauthorized parties.\n\nData at rest without encryption is vulnerable to **unauthorized access** if storage media is lost, stolen, or improperly decommissioned.", + "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": [ "https://www.alibabacloud.com/help/doc-detail/59643.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ECS/encrypt-vm-instance-disks.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ECS/encrypt-vm-instance-disks.html" ], "Remediation": { "Code": { "CLI": "aliyun ecs CreateDisk --DiskName --Size --Encrypted true --KmsKeyId ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ECS Console** > **Instances & Images** > **Images**.\n2. Select the **Custom Image** tab and select the target image.\n3. Click **Copy Image** and check the **Encrypt** box.\n4. Select a key and click **OK**.\n5. For data disks, go to **Instances** > **Create Instance**, in the Storage section click **Add Disk**, select **Disk Encryption**, and choose a key.\n\n**Note:** You cannot directly convert unencrypted disks to encrypted disks.", "Terraform": "resource \"alicloud_ecs_disk\" \"encrypted\" {\n zone_id = \"cn-hangzhou-a\"\n disk_name = \"encrypted-disk\"\n category = \"cloud_efficiency\"\n size = 20\n encrypted = true\n kms_key_id = alicloud_kms_key.example.id\n}" }, "Recommendation": { - "Text": "**Encrypt a system disk when copying an image:**\n1. Log on to the **ECS Console** > **Instances & Images** > **Images**\n2. Select the **Custom Image** tab and select target image\n3. Click **Copy Image** and check the **Encrypt** box\n4. Select a key and click **OK**\n\n**Encrypt a data disk when creating an instance:**\n1. Log on to the **ECS Console** > **Instances & Images** > **Instances** > **Create Instance**\n2. In the Storage section, click **Add Disk**\n3. Select **Disk Encryption** and choose a key\n\n**Note:** You cannot directly convert unencrypted disks to encrypted disks.", + "Text": "Enable encryption on all ECS disks to protect data at rest. Use KMS-managed keys for encryption. Note that existing unencrypted disks cannot be directly converted; data must be migrated to new encrypted disks.", "Url": "https://hub.prowler.com/check/ecs_attached_disk_encrypted" } }, 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 d2ad113899..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 @@ -1,35 +1,29 @@ { "Provider": "alibabacloud", "CheckID": "ecs_instance_endpoint_protection_installed", - "CheckTitle": "The endpoint protection for all Virtual Machines is installed", - "CheckType": [ - "Suspicious process", - "Webshell", - "Unusual logon", - "Sensitive file tampering", - "Malicious software" - ], + "CheckTitle": "ECS instance has endpoint protection installed", + "CheckType": [], "ServiceName": "ecs", "SubServiceName": "", - "ResourceIdTemplate": "acs:ecs:region:account-id:instance/{instance-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudECSInstance", + "ResourceType": "ALIYUN::ECS::Instance", "ResourceGroup": "compute", - "Description": "Installing **endpoint protection systems** (like **Security Center** for Alibaba Cloud) provides real-time protection capability that helps identify and remove viruses, spyware, and other malicious software.\n\nConfigurable alerts notify when known malicious software attempts to install itself or run on ECS instances.", - "Risk": "ECS instances without **endpoint protection** are vulnerable to **malware**, **viruses**, and other security threats.\n\nEndpoint protection provides real-time monitoring and protection capabilities essential for detecting and preventing security incidents.", + "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": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ECS/enable-endpoint-protection.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ECS/enable-endpoint-protection.html" ], "Remediation": { "Code": { - "CLI": "Logon to Security Center Console > Select Settings > Click Agent > Select virtual machines without Security Center agent > Click Install", + "CLI": "aliyun sas InstallBackupClient --Uuid ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **Security Center Console**.\n2. Select **Settings**.\n3. Click **Agent**.\n4. On the Agent tab, select the virtual machines without Security Center agent installed.\n5. Click **Install**.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **Security Center Console**\n2. Select **Settings**\n3. Click **Agent**\n4. On the Agent tab, select the virtual machines without Security Center agent installed\n5. Click **Install**", + "Text": "Install the Alibaba Cloud **Security Center** agent on all ECS instances to enable real-time endpoint protection, malware detection, and vulnerability scanning.", "Url": "https://hub.prowler.com/check/ecs_instance_endpoint_protection_installed" } }, diff --git a/prowler/providers/alibabacloud/services/ecs/ecs_instance_latest_os_patches_applied/ecs_instance_latest_os_patches_applied.metadata.json b/prowler/providers/alibabacloud/services/ecs/ecs_instance_latest_os_patches_applied/ecs_instance_latest_os_patches_applied.metadata.json index 21222b9058..d667848b18 100644 --- a/prowler/providers/alibabacloud/services/ecs/ecs_instance_latest_os_patches_applied/ecs_instance_latest_os_patches_applied.metadata.json +++ b/prowler/providers/alibabacloud/services/ecs/ecs_instance_latest_os_patches_applied/ecs_instance_latest_os_patches_applied.metadata.json @@ -1,32 +1,29 @@ { "Provider": "alibabacloud", "CheckID": "ecs_instance_latest_os_patches_applied", - "CheckTitle": "The latest OS Patches for all Virtual Machines are applied", - "CheckType": [ - "Malicious software", - "Web application threat detection" - ], + "CheckTitle": "ECS instance has latest OS patches applied", + "CheckType": [], "ServiceName": "ecs", "SubServiceName": "", - "ResourceIdTemplate": "acs:ecs:region:account-id:instance/{instance-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudECSInstance", + "ResourceType": "ALIYUN::ECS::Instance", "ResourceGroup": "compute", - "Description": "Windows and Linux virtual machines should be kept updated to address specific bugs or flaws, improve OS or application's general stability, and fix **security vulnerabilities**.\n\nThe Alibaba Cloud **Security Center** checks for the latest updates in Linux and Windows systems.", - "Risk": "**Unpatched systems** are vulnerable to known security exploits and may be compromised by attackers.\n\nKeeping systems updated with the latest patches is critical for maintaining security and preventing **exploitation of known vulnerabilities**.", + "Description": "**Alibaba Cloud Security Center** checks for the latest updates in Linux and Windows systems running on ECS instances. Keeping virtual machines updated with the latest OS patches addresses specific bugs, improves general stability, and fixes **security vulnerabilities**. This check verifies that all known vulnerabilities detected by Security Center have been patched on each ECS instance.", + "Risk": "**Unpatched systems** are vulnerable to known security exploits and can be compromised by attackers leveraging publicly disclosed vulnerabilities. Failure to apply patches in a timely manner increases the risk of **unauthorized access**, **malware infection**, and **data breaches**, impacting the **confidentiality**, **integrity**, and **availability** of the system.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ECS/apply-latest-os-patches.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ECS/apply-latest-os-patches.html" ], "Remediation": { "Code": { - "CLI": "Logon to Security Center Console > Select Vulnerabilities > Apply all patches for vulnerabilities", + "CLI": "aliyun sas FixCheckWarnings --CheckIds ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **Security Center Console**.\n2. Select **Vulnerabilities** in the left-side navigation pane.\n3. Review all detected vulnerabilities.\n4. Apply all available patches for the reported vulnerabilities.\n5. Verify that vulnerabilities are resolved after patching.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **Security Center Console**\n2. Select **Vulnerabilities**\n3. Ensure all vulnerabilities are fixed\n4. Apply all patches for vulnerabilities", + "Text": "Regularly review and apply OS patches on all ECS instances using the **Alibaba Cloud Security Center** vulnerability management feature to maintain a strong security posture.", "Url": "https://hub.prowler.com/check/ecs_instance_latest_os_patches_applied" } }, diff --git a/prowler/providers/alibabacloud/services/ecs/ecs_instance_no_legacy_network/ecs_instance_no_legacy_network.metadata.json b/prowler/providers/alibabacloud/services/ecs/ecs_instance_no_legacy_network/ecs_instance_no_legacy_network.metadata.json index 1d3eee3847..5da71e02cf 100644 --- a/prowler/providers/alibabacloud/services/ecs/ecs_instance_no_legacy_network/ecs_instance_no_legacy_network.metadata.json +++ b/prowler/providers/alibabacloud/services/ecs/ecs_instance_no_legacy_network/ecs_instance_no_legacy_network.metadata.json @@ -1,32 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ecs_instance_no_legacy_network", - "CheckTitle": "Legacy networks does not exist", - "CheckType": [ - "Suspicious network connection" - ], + "CheckTitle": "ECS instance does not use legacy network", + "CheckType": [], "ServiceName": "ecs", "SubServiceName": "", - "ResourceIdTemplate": "acs:ecs:region:account-id:instance/{instance-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudECSInstance", + "ResourceType": "ALIYUN::ECS::Instance", "ResourceGroup": "compute", - "Description": "In order to prevent use of **legacy networks**, ECS instances should not have a legacy network configured.\n\nLegacy networks have a single network IPv4 prefix range and a single gateway IP address for the whole network. With legacy networks, you cannot create subnetworks or switch from legacy to auto or custom subnet networks.", - "Risk": "**Legacy networks** can have an impact on high network traffic ECS instances and are subject to a **single point of failure**.\n\nThey also lack the security isolation and network segmentation capabilities provided by **VPCs**.", + "Description": "**Alibaba Cloud ECS instances** should use **VPC (Virtual Private Cloud)** networks instead of legacy classic networks. Legacy networks have a single IPv4 prefix range and a single gateway IP address for the whole network, preventing the creation of subnetworks or migration to auto/custom subnet networks. This check verifies that no ECS instances are configured with a legacy network type.", + "Risk": "**Legacy networks** lack the security isolation and network segmentation capabilities provided by **VPCs**, creating a **single point of failure** for high-traffic instances. Without proper network segmentation, lateral movement by attackers becomes easier, impacting **confidentiality** and **integrity** of workloads sharing the same flat network.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/87190.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-VPC/legacy-network-usage.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-VPC/legacy-network-usage.html" ], "Remediation": { "Code": { "CLI": "aliyun ecs CreateInstance --InstanceName --ImageId --InstanceType --VSwitchId ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ECS Console**.\n2. In the left-side navigation pane, choose **Instance & Image** > **Instances**.\n3. Click **Create Instance**.\n4. Specify the basic instance information and click **Next: Networking**.\n5. Select **VPC** as the Network Type and choose an appropriate VSwitch.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **ECS Console**\n2. In the left-side navigation pane, choose **Instance & Image** > **Instances**\n3. Click **Create Instance**\n4. Specify the basic instance information required and click **Next: Networking**\n5. Select the Network Type of **VPC**", + "Text": "Migrate all ECS instances from legacy classic networks to **VPC** networks. Create new instances within a VPC and migrate workloads from legacy network instances.", "Url": "https://hub.prowler.com/check/ecs_instance_no_legacy_network" } }, diff --git a/prowler/providers/alibabacloud/services/ecs/ecs_securitygroup_restrict_rdp_internet/ecs_securitygroup_restrict_rdp_internet.metadata.json b/prowler/providers/alibabacloud/services/ecs/ecs_securitygroup_restrict_rdp_internet/ecs_securitygroup_restrict_rdp_internet.metadata.json index cf61b5255e..855d63279f 100644 --- a/prowler/providers/alibabacloud/services/ecs/ecs_securitygroup_restrict_rdp_internet/ecs_securitygroup_restrict_rdp_internet.metadata.json +++ b/prowler/providers/alibabacloud/services/ecs/ecs_securitygroup_restrict_rdp_internet/ecs_securitygroup_restrict_rdp_internet.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ecs_securitygroup_restrict_rdp_internet", - "CheckTitle": "RDP access is restricted from the internet", - "CheckType": [ - "Unusual logon", - "Suspicious network connection" - ], + "CheckTitle": "Security group restricts RDP access from the internet", + "CheckType": [], "ServiceName": "ecs", "SubServiceName": "", - "ResourceIdTemplate": "acs:ecs:region:account-id:security-group/{security-group-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudECSSecurityGroup", + "ResourceType": "ALIYUN::ECS::SecurityGroup", "ResourceGroup": "network", - "Description": "**Security groups** provide stateful filtering of ingress/egress network traffic to Alibaba Cloud resources.\n\nIt is recommended that no security group allows unrestricted ingress access to port **3389 (RDP)**.", - "Risk": "Removing unfettered connectivity to remote console services, such as **RDP**, reduces a server's exposure to risk.\n\nUnrestricted RDP access from the internet (`0.0.0.0/0`) exposes systems to **brute force attacks**, **credential stuffing**, and **exploitation of RDP vulnerabilities**.", + "Description": "**Alibaba Cloud ECS security groups** provide stateful filtering of ingress and egress network traffic to cloud resources. This check verifies that no security group allows unrestricted ingress access to port **3389** (RDP) from the internet (`0.0.0.0/0` or `::/0`). Restricting RDP access to trusted IP addresses significantly reduces the attack surface of ECS instances.", + "Risk": "Unrestricted **RDP access** from the internet (`0.0.0.0/0`) exposes systems to **brute force attacks**, **credential stuffing**, and **exploitation of RDP vulnerabilities** such as BlueKeep. This can lead to **unauthorized access**, **data exfiltration**, and full system compromise, impacting **confidentiality**, **integrity**, and **availability**.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/25387.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ECS/unrestricted-rdp-access.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ECS/unrestricted-rdp-access.html" ], "Remediation": { "Code": { "CLI": "aliyun ecs RevokeSecurityGroup --SecurityGroupId --IpProtocol tcp --PortRange 3389/3389 --SourceCidrIp 0.0.0.0/0", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ECS Console**.\n2. In the left-side navigation pane, choose **Network & Security** > **Security Groups**.\n3. Find the target security group and click **Add Rules**.\n4. Locate the rule allowing port `3389` from `0.0.0.0/0`.\n5. Modify the Source IP range to a specific trusted IP or CIDR block.\n6. Click **Save**.", "Terraform": "resource \"alicloud_security_group_rule\" \"deny_rdp_internet\" {\n type = \"ingress\"\n ip_protocol = \"tcp\"\n port_range = \"3389/3389\"\n security_group_id = alicloud_security_group.example.id\n cidr_ip = \"10.0.0.0/8\" # Restrict to internal network\n policy = \"accept\"\n}" }, "Recommendation": { - "Text": "1. Log on to the **ECS Console**\n2. In the left-side navigation pane, choose **Network & Security** > **Security Groups**\n3. Find the Security Group you want to modify\n4. Modify Source IP range to specific IP instead of `0.0.0.0/0`\n5. Click **Save**", + "Text": "Restrict RDP (port **3389**) access in security groups to only trusted IP addresses or CIDR blocks. Remove any rules allowing access from `0.0.0.0/0` or `::/0`.", "Url": "https://hub.prowler.com/check/ecs_securitygroup_restrict_rdp_internet" } }, diff --git a/prowler/providers/alibabacloud/services/ecs/ecs_securitygroup_restrict_ssh_internet/ecs_securitygroup_restrict_ssh_internet.metadata.json b/prowler/providers/alibabacloud/services/ecs/ecs_securitygroup_restrict_ssh_internet/ecs_securitygroup_restrict_ssh_internet.metadata.json index 136687ed7f..19c2b5e7f3 100644 --- a/prowler/providers/alibabacloud/services/ecs/ecs_securitygroup_restrict_ssh_internet/ecs_securitygroup_restrict_ssh_internet.metadata.json +++ b/prowler/providers/alibabacloud/services/ecs/ecs_securitygroup_restrict_ssh_internet/ecs_securitygroup_restrict_ssh_internet.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ecs_securitygroup_restrict_ssh_internet", - "CheckTitle": "SSH access is restricted from the internet", - "CheckType": [ - "Unusual logon", - "Suspicious network connection" - ], + "CheckTitle": "Security group restricts SSH access from the internet", + "CheckType": [], "ServiceName": "ecs", "SubServiceName": "", - "ResourceIdTemplate": "acs:ecs:region:account-id:security-group/{security-group-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudECSSecurityGroup", + "ResourceType": "ALIYUN::ECS::SecurityGroup", "ResourceGroup": "network", - "Description": "**Security groups** provide stateful filtering of ingress/egress network traffic to Alibaba Cloud resources.\n\nIt is recommended that no security group allows unrestricted ingress access to port **22 (SSH)**.", - "Risk": "Removing unfettered connectivity to remote console services, such as **SSH**, reduces a server's exposure to risk.\n\nUnrestricted SSH access from the internet (`0.0.0.0/0`) exposes systems to **brute force attacks**, **credential stuffing**, and **exploitation of SSH vulnerabilities**.", + "Description": "**Alibaba Cloud ECS security groups** provide stateful filtering of ingress and egress network traffic to cloud resources. This check verifies that no security group allows unrestricted ingress access to port **22** (SSH) from the internet (`0.0.0.0/0` or `::/0`). Restricting SSH access to trusted IP addresses significantly reduces the attack surface of ECS instances.", + "Risk": "Unrestricted **SSH access** from the internet (`0.0.0.0/0`) exposes systems to **brute force attacks**, **credential stuffing**, and **exploitation of SSH vulnerabilities**. This can lead to **unauthorized access**, **data exfiltration**, and full system compromise, impacting **confidentiality**, **integrity**, and **availability**.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/25387.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ECS/unrestricted-ssh-access.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ECS/unrestricted-ssh-access.html" ], "Remediation": { "Code": { "CLI": "aliyun ecs RevokeSecurityGroup --SecurityGroupId --IpProtocol tcp --PortRange 22/22 --SourceCidrIp 0.0.0.0/0", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ECS Console**.\n2. In the left-side navigation pane, choose **Network & Security** > **Security Groups**.\n3. Find the target security group and click **Add Rules**.\n4. Locate the rule allowing port `22` from `0.0.0.0/0`.\n5. Modify the Source IP range to a specific trusted IP or CIDR block.\n6. Click **Save**.", "Terraform": "resource \"alicloud_security_group_rule\" \"deny_ssh_internet\" {\n type = \"ingress\"\n ip_protocol = \"tcp\"\n port_range = \"22/22\"\n security_group_id = alicloud_security_group.example.id\n cidr_ip = \"10.0.0.0/8\" # Restrict to internal network\n policy = \"accept\"\n}" }, "Recommendation": { - "Text": "1. Log on to the **ECS Console**\n2. In the left-side navigation pane, choose **Network & Security** > **Security Groups**\n3. Find the Security Group you want to modify\n4. Modify Source IP range to specific IP instead of `0.0.0.0/0`\n5. Click **Save**", + "Text": "Restrict SSH (port **22**) access in security groups to only trusted IP addresses or CIDR blocks. Remove any rules allowing access from `0.0.0.0/0` or `::/0`.", "Url": "https://hub.prowler.com/check/ecs_securitygroup_restrict_ssh_internet" } }, diff --git a/prowler/providers/alibabacloud/services/ecs/ecs_unattached_disk_encrypted/ecs_unattached_disk_encrypted.metadata.json b/prowler/providers/alibabacloud/services/ecs/ecs_unattached_disk_encrypted/ecs_unattached_disk_encrypted.metadata.json index ed2daedbde..0808a219f0 100644 --- a/prowler/providers/alibabacloud/services/ecs/ecs_unattached_disk_encrypted/ecs_unattached_disk_encrypted.metadata.json +++ b/prowler/providers/alibabacloud/services/ecs/ecs_unattached_disk_encrypted/ecs_unattached_disk_encrypted.metadata.json @@ -1,32 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ecs_unattached_disk_encrypted", - "CheckTitle": "Unattached disks are encrypted", - "CheckType": [ - "Sensitive file tampering" - ], + "CheckTitle": "ECS unattached disk is encrypted", + "CheckType": [], "ServiceName": "ecs", "SubServiceName": "", - "ResourceIdTemplate": "acs:ecs:region:account-id:disk/{disk-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudECSDisk", + "ResourceType": "ALIYUN::ECS::Disk", "ResourceGroup": "storage", - "Description": "**Cloud disk encryption** protects your data at rest. The cloud disk data encryption feature automatically encrypts data when data is transferred from ECS instances to disks, and decrypts data when read from disks.", - "Risk": "**Unencrypted unattached disks** pose a security risk as they may contain sensitive data that could be accessed if the disk is compromised or accessed by unauthorized parties.\n\nUnattached disks are especially vulnerable as they may be forgotten or not monitored, increasing the risk of **unauthorized access**.", + "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. This check verifies that unattached (detached) disks have encryption enabled, since unattached disks may still contain sensitive data from previous workloads and are especially vulnerable if not properly managed.", + "Risk": "**Unencrypted unattached disks** pose a significant security risk as they may contain sensitive data that could be accessed if the disk is compromised or accessed by unauthorized parties. Unattached disks are especially vulnerable as they may be overlooked in security monitoring, increasing the risk of **unauthorized access** and **data breaches** that impact **confidentiality**.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/59643.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-ECS/encrypt-unattached-disks.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-ECS/encrypt-unattached-disks.html" ], "Remediation": { "Code": { "CLI": "aliyun ecs CreateDisk --DiskName --Size --Encrypted true --KmsKeyId ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **ECS Console**.\n2. In the left-side navigation pane, choose **Storage & Snapshots** > **Disk**.\n3. In the upper-right corner of the Disks page, click **Create Disk**.\n4. In the Disk section, check the **Disk Encryption** box and select a key from the drop-down list.\n\n**Note:** After a data disk is created, you can only encrypt it by manually copying data from the unencrypted disk to a new encrypted disk.", "Terraform": "resource \"alicloud_ecs_disk\" \"encrypted\" {\n zone_id = \"cn-hangzhou-a\"\n disk_name = \"encrypted-disk\"\n category = \"cloud_efficiency\"\n size = 20\n encrypted = true\n kms_key_id = alicloud_kms_key.example.id\n}" }, "Recommendation": { - "Text": "1. Log on to the **ECS Console**\n2. In the left-side navigation pane, choose **Storage & Snapshots** > **Disk**\n3. In the upper-right corner of the Disks page, click **Create Disk**\n4. In the Disk section, check the **Disk Encryption** box and select a key from the drop-down list\n\n**Note:** After a data disk is created, you can only encrypt the data disk by manually copying data from the unencrypted disk to a new encrypted disk.", + "Text": "Ensure all unattached ECS disks are encrypted. Create new encrypted disks and migrate data from unencrypted disks, then delete the unencrypted originals.", "Url": "https://hub.prowler.com/check/ecs_unattached_disk_encrypted" } }, diff --git a/prowler/providers/alibabacloud/services/oss/oss_bucket_logging_enabled/oss_bucket_logging_enabled.metadata.json b/prowler/providers/alibabacloud/services/oss/oss_bucket_logging_enabled/oss_bucket_logging_enabled.metadata.json index 23fce0f862..934eee4dbf 100644 --- a/prowler/providers/alibabacloud/services/oss/oss_bucket_logging_enabled/oss_bucket_logging_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/oss/oss_bucket_logging_enabled/oss_bucket_logging_enabled.metadata.json @@ -2,32 +2,29 @@ "Provider": "alibabacloud", "CheckID": "oss_bucket_logging_enabled", "CheckTitle": "Logging is enabled for OSS buckets", - "CheckType": [ - "Sensitive file tampering", - "Cloud threat detection" - ], + "CheckType": [], "ServiceName": "oss", "SubServiceName": "", - "ResourceIdTemplate": "acs:oss::account-id:bucket-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudOSSBucket", + "ResourceType": "ALIYUN::OSS::Bucket", "ResourceGroup": "storage", - "Description": "**OSS Bucket Access Logging** generates a log that contains access records for each request made to your OSS bucket.\n\nAn access log record contains details about the request, such as the request type, the resources specified in the request, and the time and date the request was processed. It is recommended that bucket access logging be enabled on OSS buckets.", - "Risk": "By enabling **OSS bucket logging** on target OSS buckets, it is possible to capture all events which may affect objects within target buckets.\n\nConfiguring logs to be placed in a separate bucket allows access to log information useful in **security** and **incident response** workflows.", + "Description": "**Alibaba Cloud OSS Bucket Access Logging** generates a log record for each request made to your OSS bucket, containing details such as the request type, the resources specified, and the time and date the request was processed. Enabling bucket access logging on all OSS buckets ensures that access patterns are recorded and available for security analysis and incident response workflows.", + "Risk": "Without **OSS bucket logging** enabled, access events affecting objects within target buckets are not captured. This limits the ability to perform **security analysis**, **incident response**, and **forensic investigations**, as there is no record of who accessed or modified stored data.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/31900.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-OSS/enable-bucket-access-logging.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-OSS/enable-bucket-access-logging.html" ], "Remediation": { "Code": { "CLI": "ossutil logging --method put oss:// --target-bucket --target-prefix ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **OSS Console**\n2. In the bucket-list pane, click on a target OSS bucket\n3. Under **Log**, click **Configure**\n4. Click the **Enabled** checkbox\n5. Select `Target Bucket` from the list\n6. Enter a `Target Prefix`\n7. Click **Save**", "Terraform": "resource \"alicloud_oss_bucket_logging\" \"example\" {\n bucket = alicloud_oss_bucket.example.bucket\n target_bucket = alicloud_oss_bucket.log_bucket.bucket\n target_prefix = \"log/\"\n}" }, "Recommendation": { - "Text": "1. Log on to the **OSS Console**\n2. In the bucket-list pane, click on a target OSS bucket\n3. Under **Log**, click **Configure**\n4. Click the **Enabled** checkbox\n5. Select `Target Bucket` from the list\n6. Enter a `Target Prefix`\n7. Click **Save**", + "Text": "Enable access logging on all OSS buckets and configure logs to be stored in a separate dedicated bucket for security analysis and compliance auditing.", "Url": "https://hub.prowler.com/check/oss_bucket_logging_enabled" } }, diff --git a/prowler/providers/alibabacloud/services/oss/oss_bucket_not_publicly_accessible/oss_bucket_not_publicly_accessible.metadata.json b/prowler/providers/alibabacloud/services/oss/oss_bucket_not_publicly_accessible/oss_bucket_not_publicly_accessible.metadata.json index 8ba7ef19db..748e14b666 100644 --- a/prowler/providers/alibabacloud/services/oss/oss_bucket_not_publicly_accessible/oss_bucket_not_publicly_accessible.metadata.json +++ b/prowler/providers/alibabacloud/services/oss/oss_bucket_not_publicly_accessible/oss_bucket_not_publicly_accessible.metadata.json @@ -2,32 +2,29 @@ "Provider": "alibabacloud", "CheckID": "oss_bucket_not_publicly_accessible", "CheckTitle": "OSS bucket is not anonymously or publicly accessible", - "CheckType": [ - "Sensitive file tampering", - "Cloud threat detection" - ], + "CheckType": [], "ServiceName": "oss", "SubServiceName": "", - "ResourceIdTemplate": "acs:oss::account-id:bucket-name", + "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "AlibabaCloudOSSBucket", + "ResourceType": "ALIYUN::OSS::Bucket", "ResourceGroup": "storage", - "Description": "A bucket is a container used to store objects in **Object Storage Service (OSS)**. All objects in OSS are stored in buckets.\n\nIt is recommended that the access policy on OSS buckets does not allow **anonymous** and/or **public access**.", - "Risk": "Allowing **anonymous** and/or **public access** grants permissions to anyone to access bucket content. Such access might not be desired if you are storing any sensitive data.\n\nPublic buckets can lead to **data breaches**, **unauthorized data access**, and **compliance violations**.", + "Description": "**Alibaba Cloud Object Storage Service (OSS)** buckets store objects that may contain sensitive data. It is recommended that the access policy on OSS buckets does not allow **anonymous** or **public access**, ensuring that only authorized identities can interact with bucket contents. The bucket ACL should be set to `private` to prevent unintended data exposure.", + "Risk": "Allowing **anonymous** or **public access** to OSS buckets grants permissions to anyone on the internet to read or modify bucket content. This can lead to **data breaches**, **unauthorized data exfiltration**, **data tampering**, and **compliance violations**, particularly when buckets contain sensitive or regulated information.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/31896.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-OSS/publicly-accessible-oss-bucket.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-OSS/publicly-accessible-oss-bucket.html" ], "Remediation": { "Code": { "CLI": "aliyun oss PutBucketAcl --bucket --acl private", "NativeIaC": "", - "Other": "", + "Other": "**Set Bucket ACL to Private:**\n1. Log on to the **OSS Console**\n2. In the bucket-list pane, click on a target OSS bucket\n3. Click on **Basic Setting** in the top middle of the console\n4. Under ACL section, click on **Configure**\n5. Click **Private** and click **Save**\n\n**For Bucket Policy:**\n1. Click **Bucket**, and then click the name of the target bucket\n2. Click the **Files** tab and click **Authorize**\n3. In the Authorize dialog, choose `Anonymous Accounts (*)` for Accounts and choose `None` for Authorized Operation\n4. Click **OK**", "Terraform": "resource \"alicloud_oss_bucket_public_access_block\" \"example\" {\n bucket = alicloud_oss_bucket.example.bucket\n block_public_access = true\n}" }, "Recommendation": { - "Text": "**Set Bucket ACL to Private:**\n1. Log on to the **OSS Console**\n2. In the bucket-list pane, click on a target OSS bucket\n3. Click on **Basic Setting** in the top middle of the console\n4. Under ACL section, click on **Configure**\n5. Click **Private** and click **Save**\n\n**For Bucket Policy:**\n1. Click **Bucket**, and then click the name of the target bucket\n2. Click the **Files** tab and click **Authorize**\n3. In the Authorize dialog, choose `Anonymous Accounts (*)` for Accounts and choose `None` for Authorized Operation\n4. Click **OK**", + "Text": "Set the OSS bucket ACL to private and configure bucket policies to deny anonymous or public access, ensuring only authorized identities can access stored objects.", "Url": "https://hub.prowler.com/check/oss_bucket_not_publicly_accessible" } }, diff --git a/prowler/providers/alibabacloud/services/oss/oss_bucket_secure_transport_enabled/oss_bucket_secure_transport_enabled.metadata.json b/prowler/providers/alibabacloud/services/oss/oss_bucket_secure_transport_enabled/oss_bucket_secure_transport_enabled.metadata.json index d5b0a59bf9..b112b84d43 100644 --- a/prowler/providers/alibabacloud/services/oss/oss_bucket_secure_transport_enabled/oss_bucket_secure_transport_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/oss/oss_bucket_secure_transport_enabled/oss_bucket_secure_transport_enabled.metadata.json @@ -1,32 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "oss_bucket_secure_transport_enabled", - "CheckTitle": "Secure transfer required is set to Enabled", - "CheckType": [ - "Sensitive file tampering" - ], + "CheckTitle": "Secure transfer required is enabled for OSS buckets", + "CheckType": [], "ServiceName": "oss", "SubServiceName": "", - "ResourceIdTemplate": "acs:oss::account-id:bucket-name", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudOSSBucket", + "ResourceType": "ALIYUN::OSS::Bucket", "ResourceGroup": "storage", - "Description": "Enable **data encryption in transit**. The secure transfer enhances the security of OSS buckets by only allowing requests to the storage account via a secure connection.\n\nFor example, when calling REST APIs to access storage accounts, the connection must use **HTTPS**. Any requests using HTTP will be rejected.", - "Risk": "Without **secure transfer enforcement**, OSS buckets may accept HTTP requests, which are not encrypted in transit.\n\nThis exposes data to potential **interception** and **man-in-the-middle attacks**, compromising data confidentiality and integrity.", + "Description": "**Alibaba Cloud OSS** buckets should enforce **secure transfer** by requiring all requests to use HTTPS. A bucket policy that denies requests with `acs:SecureTransport` set to `false` ensures that data in transit is encrypted, rejecting any unencrypted HTTP connections to the storage endpoint.", + "Risk": "Without **secure transfer enforcement**, OSS buckets accept HTTP requests that transmit data in plaintext. This exposes stored data to potential **interception**, **man-in-the-middle attacks**, and **eavesdropping**, compromising data **confidentiality** and **integrity** during transit.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/85111.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-OSS/enable-secure-transfer.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-OSS/enable-secure-transfer.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun oss PutBucketPolicy --bucket --policy '{\"Version\":\"1\",\"Statement\":[{\"Effect\":\"Deny\",\"Principal\":[\"*\"],\"Action\":[\"oss:*\"],\"Resource\":[\"acs:oss:*:*:\",\"acs:oss:*:*:/*\"],\"Condition\":{\"Bool\":{\"acs:SecureTransport\":\"false\"}}}]}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **OSS Console**\n2. In the bucket-list pane, click on a target OSS bucket\n3. Click on **Files** in the top middle of the console\n4. Click on **Authorize**\n5. Configure: `Whole Bucket`, `*`, `None` (Authorized Operation) and `http` (Conditions: Access Method) to deny HTTP access\n6. Click **Save**", "Terraform": "resource \"alicloud_oss_bucket\" \"example\" {\n bucket = \"example-bucket\"\n \n policy = jsonencode({\n \"Version\": \"1\",\n \"Statement\": [{\n \"Effect\": \"Deny\",\n \"Principal\": [\"*\"],\n \"Action\": [\"oss:*\"],\n \"Resource\": [\"acs:oss:*:*:example-bucket\", \"acs:oss:*:*:example-bucket/*\"],\n \"Condition\": {\n \"Bool\": {\n \"acs:SecureTransport\": \"false\"\n }\n }\n }]\n })\n}" }, "Recommendation": { - "Text": "1. Log on to the **OSS Console**\n2. In the bucket-list pane, click on a target OSS bucket\n3. Click on **Files** in the top middle of the console\n4. Click on **Authorize**\n5. Configure: `Whole Bucket`, `*`, `None` (Authorized Operation) and `http` (Conditions: Access Method) to deny HTTP access\n6. Click **Save**", + "Text": "Enforce secure transfer on OSS buckets by applying a bucket policy that denies all requests not using HTTPS, ensuring data in transit is always encrypted.", "Url": "https://hub.prowler.com/check/oss_bucket_secure_transport_enabled" } }, 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 be7ec536fd..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 @@ -2,32 +2,29 @@ "Provider": "alibabacloud", "CheckID": "ram_no_root_access_key", "CheckTitle": "No root account access key exists", - "CheckType": [ - "Unusual logon", - "Cloud threat detection" - ], + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:root", + "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "AlibabaCloudRAMAccessKey", + "ResourceType": "ALIYUN::RAM::User", "ResourceGroup": "IAM", - "Description": "Ensure no **root account access key** exists. Access keys provide programmatic access to a given Alibaba Cloud account.\n\nIt is recommended that all access keys associated with the root account be removed.", - "Risk": "The **root account** is the most privileged user in an Alibaba Cloud account. Access Keys provide programmatic access to a given Alibaba Cloud account.\n\nRemoving access keys associated with the root account limits vectors by which the account can be compromised and encourages the creation and use of **role-based accounts** that are least privileged.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/remove-root-access-keys.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/remove-root-access-keys.html" ], "Remediation": { "Code": { "CLI": "aliyun ram DeleteAccessKey --UserAccessKeyId ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RAM Console** by using your Alibaba Cloud account (root account).\n2. Move the pointer over the account icon in the upper-right corner and click **AccessKey**.\n3. Click **Continue to manage AccessKey**.\n4. On the Security Management page, find the target access keys and click **Delete** to delete the target access keys permanently.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **RAM Console** by using your Alibaba Cloud account (root account)\n2. Move the pointer over the account icon in the upper-right corner and click **AccessKey**\n3. Click **Continue to manage AccessKey**\n4. On the Security Management page, find the target access keys and click **Delete** to delete the target access keys permanently", + "Text": "Remove all access keys associated with the root account to reduce the attack surface and encourage the use of role-based accounts with least privilege.", "Url": "https://hub.prowler.com/check/ram_no_root_access_key" } }, diff --git a/prowler/providers/alibabacloud/services/ram/ram_password_policy_lowercase/ram_password_policy_lowercase.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_password_policy_lowercase/ram_password_policy_lowercase.metadata.json index f5189bc2c8..5cfd7d06ae 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_password_policy_lowercase/ram_password_policy_lowercase.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_password_policy_lowercase/ram_password_policy_lowercase.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ram_password_policy_lowercase", - "CheckTitle": "RAM password policy requires at least one lowercase letter", - "CheckType": [ - "Unusual logon", - "Abnormal account" - ], + "CheckTitle": "RAM password policy has lowercase letter requirement", + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:password-policy", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRAMPasswordPolicy", + "ResourceType": "ALIYUN::RAM::SecurityPreference", "ResourceGroup": "IAM", - "Description": "**RAM password policies** can be used to ensure password complexity.\n\nIt is recommended that the password policy require at least one **lowercase letter**.", - "Risk": "Enhancing complexity of a password policy increases account resiliency against **brute force logon attempts**.\n\nWeak passwords without character variety are more susceptible to dictionary attacks and automated password cracking tools.", + "Description": "**Alibaba Cloud RAM** password policies can be used to enforce password complexity requirements. It is recommended that the password policy require at least one **lowercase letter** to increase the character diversity of passwords. This enhances account resiliency against **brute force logon attempts** and dictionary attacks.", + "Risk": "Without requiring **lowercase letters** in the password policy, users may create passwords with limited character diversity. Weak passwords without sufficient character variety are more susceptible to **dictionary attacks** and automated password cracking tools, potentially compromising the **confidentiality** of user accounts and the resources they have access to.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/116413.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/lowercase-letter-password-policy.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/lowercase-letter-password-policy.html" ], "Remediation": { "Code": { "CLI": "aliyun ram SetPasswordPolicy --RequireLowercaseCharacters true", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RAM Console**.\n2. Choose **Settings**.\n3. In the Password section, click **Modify**.\n4. In the Charset section, select **Lower case**.\n5. Click **OK**.", "Terraform": "resource \"alicloud_ram_password_policy\" \"example\" {\n require_lowercase_characters = true\n}" }, "Recommendation": { - "Text": "1. Log on to the **RAM Console**\n2. Choose **Settings**\n3. In the Password section, click **Modify**\n4. In the Charset section, select **Lower case**\n5. Click **OK**", + "Text": "Configure the RAM password policy to require at least one lowercase letter to improve password complexity.", "Url": "https://hub.prowler.com/check/ram_password_policy_lowercase" } }, diff --git a/prowler/providers/alibabacloud/services/ram/ram_password_policy_max_login_attempts/ram_password_policy_max_login_attempts.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_password_policy_max_login_attempts/ram_password_policy_max_login_attempts.metadata.json index f2228326e7..9e4f4298b3 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_password_policy_max_login_attempts/ram_password_policy_max_login_attempts.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_password_policy_max_login_attempts/ram_password_policy_max_login_attempts.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ram_password_policy_max_login_attempts", - "CheckTitle": "RAM password policy temporarily blocks logon after 5 incorrect logon attempts within an hour", - "CheckType": [ - "Unusual logon", - "Abnormal account" - ], + "CheckTitle": "RAM password policy temporarily blocks logon after 5 incorrect attempts within an hour", + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:password-policy", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRAMPasswordPolicy", + "ResourceType": "ALIYUN::RAM::SecurityPreference", "ResourceGroup": "IAM", - "Description": "**RAM password policies** can temporarily block logon after several incorrect logon attempts within an hour.\n\nIt is recommended that the password policy is set to temporarily block logon after **5 incorrect logon attempts** within an hour.", - "Risk": "Temporarily blocking logon for incorrect password input increases account resiliency against **brute force logon attempts**.\n\nThis control helps prevent automated password guessing attacks from succeeding.", + "Description": "**Alibaba Cloud RAM** password policies can temporarily block logon after several incorrect logon attempts within an hour. It is recommended that the password policy is set to temporarily block logon after **5 incorrect logon attempts** within an hour to protect accounts against automated **brute force logon attempts** and credential stuffing attacks.", + "Risk": "Without an account lockout policy, attackers can make unlimited **brute force logon attempts** against RAM user accounts without any throttling. This significantly increases the risk of password compromise, potentially leading to unauthorized access to cloud resources and a breach of **confidentiality** and **integrity** of the account's data and services.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/116413.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/max-login-attempts-password-policy.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/max-login-attempts-password-policy.html" ], "Remediation": { "Code": { "CLI": "aliyun ram SetPasswordPolicy --MaxLoginAttemps 5", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RAM Console**.\n2. Choose **Settings**.\n3. In the Password section, click **Modify**.\n4. In the `Max Attempts` field, check the box next to **Enable** and enter `5`.\n5. Click **OK**.", "Terraform": "resource \"alicloud_ram_password_policy\" \"example\" {\n max_login_attemps = 5\n}" }, "Recommendation": { - "Text": "1. Log on to the **RAM Console**\n2. Choose **Settings**\n3. In the Password section, click **Modify**\n4. In the `Max Attempts` field, check the box next to **Enable** and enter `5`\n5. Click **OK**", + "Text": "Configure the RAM password policy to temporarily block logon after 5 incorrect attempts within an hour.", "Url": "https://hub.prowler.com/check/ram_password_policy_max_login_attempts" } }, 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 a4138c2362..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 @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ram_password_policy_max_password_age", - "CheckTitle": "RAM password policy expires passwords in 365 days or greater", - "CheckType": [ - "Unusual logon", - "Abnormal account" - ], + "CheckTitle": "RAM password policy expires passwords within 365 days or less", + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:password-policy", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRAMPasswordPolicy", + "ResourceType": "ALIYUN::RAM::SecurityPreference", "ResourceGroup": "IAM", - "Description": "**RAM password policies** can require passwords to be expired after a given number of days.\n\nIt is recommended that the password policy expire passwords after **365 days** or greater.", - "Risk": "Too frequent password changes are more harmful than beneficial. They offer no containment benefits and enforce bad habits, since they encourage users to choose variants of older passwords.\n\nThe CIS now recommends an **annual password reset** as a balanced approach.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/require-password-expiration-policy.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/require-password-expiration-policy.html" ], "Remediation": { "Code": { "CLI": "aliyun ram SetPasswordPolicy --MaxPasswordAge 365", "NativeIaC": "", - "Other": "", - "Terraform": "resource \"alicloud_ram_password_policy\" \"example\" {\n max_password_age = 90\n}" + "Other": "1. Log on to the **RAM Console**.\n2. Choose **Settings**.\n3. In the Password section, click **Modify**.\n4. Check the box under `Max Age`, enter `365` or a smaller number.\n5. Click **OK**.", + "Terraform": "resource \"alicloud_ram_password_policy\" \"example\" {\n max_password_age = 365\n}" }, "Recommendation": { - "Text": "1. Log on to the **RAM Console**\n2. Choose **Settings**\n3. In the Password section, click **Modify**\n4. Check the box under `Max Age`, enter `365` or a greater number up to `1095`\n5. Click **OK**", + "Text": "Configure the RAM password policy to expire passwords within 365 days or less.", "Url": "https://hub.prowler.com/check/ram_password_policy_max_password_age" } }, 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 4c0c45c406..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 @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ram_password_policy_minimum_length", - "CheckTitle": "RAM password policy requires minimum length of 14 or greater", - "CheckType": [ - "Unusual logon", - "Abnormal account" - ], + "CheckTitle": "RAM password policy requires a minimum length of 14 or greater", + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:password-policy", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRAMPasswordPolicy", + "ResourceType": "ALIYUN::RAM::SecurityPreference", "ResourceGroup": "IAM", - "Description": "**RAM password policies** can be used to ensure password complexity.\n\nIt is recommended that the password policy require a minimum of **14 or greater characters** for any password.", - "Risk": "Enhancing complexity of a password policy increases account resiliency against **brute force logon attempts**.\n\nLonger passwords provide exponentially more security against automated password cracking.", + "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": "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/require-14-characters-password-policy.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/require-14-characters-password-policy.html" ], "Remediation": { "Code": { "CLI": "aliyun ram SetPasswordPolicy --MinimumPasswordLength 14", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RAM Console**.\n2. Choose **Settings**.\n3. In the Password section, click **Modify**.\n4. In the Length section, enter `14` or a greater number.\n5. Click **OK**.", "Terraform": "resource \"alicloud_ram_password_policy\" \"example\" {\n minimum_password_length = 14\n}" }, "Recommendation": { - "Text": "1. Log on to the **RAM Console**\n2. Choose **Settings**\n3. In the Password section, click **Modify**\n4. In the Length section, enter `14` or a greater number\n5. Click **OK**", + "Text": "Configure the RAM password policy to require a minimum password length of 14 characters or greater.", "Url": "https://hub.prowler.com/check/ram_password_policy_minimum_length" } }, diff --git a/prowler/providers/alibabacloud/services/ram/ram_password_policy_number/ram_password_policy_number.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_password_policy_number/ram_password_policy_number.metadata.json index 29aba59a21..9b107baa91 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_password_policy_number/ram_password_policy_number.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_password_policy_number/ram_password_policy_number.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ram_password_policy_number", - "CheckTitle": "RAM password policy require at least one number", - "CheckType": [ - "Unusual logon", - "Abnormal account" - ], + "CheckTitle": "RAM password policy requires at least one number", + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:password-policy", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRAMPasswordPolicy", + "ResourceType": "ALIYUN::RAM::SecurityPreference", "ResourceGroup": "IAM", - "Description": "**RAM password policies** can be used to ensure password complexity.\n\nIt is recommended that the password policy require at least one **number**.", - "Risk": "Enhancing complexity of a password policy increases account resiliency against **brute force logon attempts**.\n\nWeak passwords without numeric characters are more susceptible to dictionary attacks.", + "Description": "**Alibaba Cloud RAM** password policies can be used to enforce password complexity requirements. It is recommended that the password policy require at least one **numeric character** to increase the character diversity of passwords. This enhances account resiliency against **brute force logon attempts** and dictionary attacks by expanding the keyspace.", + "Risk": "Without requiring **numeric characters** in the password policy, users may create passwords composed only of alphabetic characters. Such passwords are more susceptible to **dictionary attacks** and automated cracking tools, potentially compromising the **confidentiality** of user accounts and the cloud resources they protect.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/116413.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/require-number-password-policy.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/require-number-password-policy.html" ], "Remediation": { "Code": { "CLI": "aliyun ram SetPasswordPolicy --RequireNumbers true", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RAM Console**.\n2. Choose **Settings**.\n3. In the Password section, click **Modify**.\n4. In the Charset section, select **Number**.\n5. Click **OK**.", "Terraform": "resource \"alicloud_ram_password_policy\" \"example\" {\n require_numbers = true\n}" }, "Recommendation": { - "Text": "1. Log on to the **RAM Console**\n2. Choose **Settings**\n3. In the Password section, click **Modify**\n4. In the Charset section, select **Number**\n5. Click **OK**", + "Text": "Configure the RAM password policy to require at least one numeric character to improve password complexity.", "Url": "https://hub.prowler.com/check/ram_password_policy_number" } }, 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 761807fb00..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 @@ -2,32 +2,29 @@ "Provider": "alibabacloud", "CheckID": "ram_password_policy_password_reuse_prevention", "CheckTitle": "RAM password policy prevents password reuse", - "CheckType": [ - "Unusual logon", - "Abnormal account" - ], + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:password-policy", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRAMPasswordPolicy", + "ResourceType": "ALIYUN::RAM::SecurityPreference", "ResourceGroup": "IAM", - "Description": "It is recommended that the **password policy** prevent the reuse of passwords.\n\nThis ensures users cannot cycle back to previously compromised passwords.", - "Risk": "Preventing **password reuse** increases account resiliency against brute force logon attempts.\n\nIf a password is compromised and later reused, attackers with knowledge of old credentials can regain access.", + "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. 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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/prevent-password-reuse-password-policy.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/prevent-password-reuse-password-policy.html" ], "Remediation": { "Code": { "CLI": "aliyun ram SetPasswordPolicy --PasswordReusePrevention 5", "NativeIaC": "", - "Other": "", - "Terraform": "resource \"alicloud_ram_password_policy\" \"example\" {\n password_reuse_prevention = 24\n}" + "Other": "1. Log on to the **RAM Console**.\n2. Choose **Settings**.\n3. In the Password section, click **Modify**.\n4. In the `Do Not repeat History` section field, enter `5`.\n5. Click **OK**.", + "Terraform": "resource \"alicloud_ram_password_policy\" \"example\" {\n password_reuse_prevention = 5\n}" }, "Recommendation": { - "Text": "1. Log on to the **RAM Console**\n2. Choose **Settings**\n3. In the Password section, click **Modify**\n4. In the `Do Not repeat History` section field, enter `5`\n5. Click **OK**", + "Text": "Configure the RAM password policy to prevent the reuse of at least the last 5 passwords.", "Url": "https://hub.prowler.com/check/ram_password_policy_password_reuse_prevention" } }, diff --git a/prowler/providers/alibabacloud/services/ram/ram_password_policy_symbol/ram_password_policy_symbol.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_password_policy_symbol/ram_password_policy_symbol.metadata.json index 7c369a89c2..46fc154d33 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_password_policy_symbol/ram_password_policy_symbol.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_password_policy_symbol/ram_password_policy_symbol.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ram_password_policy_symbol", - "CheckTitle": "RAM password policy require at least one symbol", - "CheckType": [ - "Unusual logon", - "Abnormal account" - ], + "CheckTitle": "RAM password policy requires at least one symbol", + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:password-policy", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRAMPasswordPolicy", + "ResourceType": "ALIYUN::RAM::SecurityPreference", "ResourceGroup": "IAM", - "Description": "**RAM password policies** can be used to ensure password complexity.\n\nIt is recommended that the password policy require at least one **symbol**.", - "Risk": "Enhancing complexity of a password policy increases account resiliency against **brute force logon attempts**.\n\nSpecial characters significantly increase the keyspace that attackers must search.", + "Description": "**Alibaba Cloud RAM** password policies can be used to enforce password complexity requirements. It is recommended that the password policy require at least one **special character (symbol)** to increase the character diversity of passwords. Special characters significantly increase the keyspace that attackers must search, enhancing account resiliency against **brute force logon attempts**.", + "Risk": "Without requiring **symbols** in the password policy, users may create passwords composed only of alphanumeric characters. Such passwords have a reduced keyspace and are more susceptible to **brute force attacks** and automated password cracking tools, potentially compromising the **confidentiality** of user accounts and the cloud resources they protect.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/116413.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/require-symbol-password-policy.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/require-symbol-password-policy.html" ], "Remediation": { "Code": { "CLI": "aliyun ram SetPasswordPolicy --RequireSymbols true", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RAM Console**.\n2. Choose **Settings**.\n3. In the Password section, click **Modify**.\n4. In the Charset section, select **Symbol**.\n5. Click **OK**.", "Terraform": "resource \"alicloud_ram_password_policy\" \"example\" {\n require_symbols = true\n}" }, "Recommendation": { - "Text": "1. Log on to the **RAM Console**\n2. Choose **Settings**\n3. In the Password section, click **Modify**\n4. In the Charset section, select **Symbol**\n5. Click **OK**", + "Text": "Configure the RAM password policy to require at least one symbol to improve password complexity.", "Url": "https://hub.prowler.com/check/ram_password_policy_symbol" } }, diff --git a/prowler/providers/alibabacloud/services/ram/ram_password_policy_uppercase/ram_password_policy_uppercase.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_password_policy_uppercase/ram_password_policy_uppercase.metadata.json index 28f115bd3e..41e5ec4845 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_password_policy_uppercase/ram_password_policy_uppercase.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_password_policy_uppercase/ram_password_policy_uppercase.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ram_password_policy_uppercase", - "CheckTitle": "RAM password policy requires at least one uppercase letter", - "CheckType": [ - "Unusual logon", - "Abnormal account" - ], + "CheckTitle": "RAM password policy has uppercase letter requirement", + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:password-policy", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRAMPasswordPolicy", + "ResourceType": "ALIYUN::RAM::SecurityPreference", "ResourceGroup": "IAM", - "Description": "**RAM password policies** can be used to ensure password complexity.\n\nIt is recommended that the password policy require at least one **uppercase letter**.", - "Risk": "Enhancing complexity of a password policy increases account resiliency against **brute force logon attempts**.\n\nWeak passwords without case variety are more susceptible to dictionary attacks.", + "Description": "**Alibaba Cloud RAM** password policies can be used to enforce password complexity requirements. It is recommended that the password policy require at least one **uppercase letter** to increase the character diversity of passwords. This enhances account resiliency against **brute force logon attempts** and dictionary attacks by requiring mixed-case passwords.", + "Risk": "Without requiring **uppercase letters** in the password policy, users may create passwords with limited case diversity. Weak passwords without case variety are more susceptible to **dictionary attacks** and automated password cracking tools, potentially compromising the **confidentiality** of user accounts and the resources they have access to.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/116413.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/uppercase-letter-password-policy.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/uppercase-letter-password-policy.html" ], "Remediation": { "Code": { "CLI": "aliyun ram SetPasswordPolicy --RequireUppercaseCharacters true", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RAM Console**.\n2. Choose **Settings**.\n3. In the Password section, click **Modify**.\n4. In the Charset section, select **Upper case**.\n5. Click **OK**.", "Terraform": "resource \"alicloud_ram_password_policy\" \"example\" {\n require_uppercase_characters = true\n}" }, "Recommendation": { - "Text": "1. Log on to the **RAM Console**\n2. Choose **Settings**\n3. In the Password section, click **Modify**\n4. In the Charset section, select **Upper case**\n5. Click **OK**", + "Text": "Configure the RAM password policy to require at least one uppercase letter to improve password complexity.", "Url": "https://hub.prowler.com/check/ram_password_policy_uppercase" } }, 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 0dfa1782bd..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 @@ -2,32 +2,29 @@ "Provider": "alibabacloud", "CheckID": "ram_policy_attached_only_to_group_or_roles", "CheckTitle": "RAM policies are attached only to groups or roles", - "CheckType": [ - "Abnormal account", - "Cloud threat detection" - ], + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:user/{user-name}", + "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "AlibabaCloudRAMUser", + "ResourceType": "ALIYUN::RAM::ManagedPolicy", "ResourceGroup": "IAM", - "Description": "By default, **RAM users**, groups, and roles have no access to Alibaba Cloud resources. RAM policies are the means by which privileges are granted to users, groups, or roles.\n\nIt is recommended that RAM policies be applied directly to **groups and roles** but not users.", - "Risk": "Assigning privileges at the **group or role level** reduces the complexity of access management as the number of users grows.\n\nReducing access management complexity may in turn reduce opportunity for a principal to inadvertently receive or retain **excessive privileges**.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/receive-permissions-via-ram-groups-only.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/receive-permissions-via-ram-groups-only.html" ], "Remediation": { "Code": { "CLI": "aliyun ram DetachPolicyFromUser --PolicyName --PolicyType --UserName ", "NativeIaC": "", - "Other": "", + "Other": "1. Create **RAM user groups** and assign policies to those groups.\n2. Add users to the appropriate groups.\n3. Detach any policies directly attached to users using the RAM Console or CLI.", "Terraform": "" }, "Recommendation": { - "Text": "1. Create **RAM user groups** and assign policies to those groups\n2. Add users to the appropriate groups\n3. Detach any policies directly attached to users using the RAM Console or CLI", + "Text": "Detach policies from individual RAM users and attach them to groups or roles instead to simplify access management.", "Url": "https://hub.prowler.com/check/ram_policy_attached_only_to_group_or_roles" } }, 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 3d2e085fc9..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 @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ram_policy_no_administrative_privileges", - "CheckTitle": "RAM policies that allow full \"*:*\" administrative privileges are not created", - "CheckType": [ - "Abnormal account", - "Cloud threat detection" - ], + "CheckTitle": "RAM policies do not allow full administrative privileges", + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:policy/{policy-name}", + "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "AlibabaCloudRAMPolicy", + "ResourceType": "ALIYUN::RAM::ManagedPolicy", "ResourceGroup": "IAM", - "Description": "**RAM policies** represent permissions that can be granted to users, groups, or roles. It is recommended to grant **least privilege**—that is, granting only the permissions required to perform tasks.\n\nDetermine what users need to do and then create policies with permissions that only fit those tasks, instead of allowing full administrative privileges.", - "Risk": "It is more secure to start with a minimum set of permissions and grant additional permissions as necessary. Providing **full administrative privileges** exposes your resources to potentially unwanted actions.\n\nRAM policies with `\"Effect\": \"Allow\"`, `\"Action\": \"*\"`, and `\"Resource\": \"*\"` should be prohibited.", + "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": [ "https://www.alibabacloud.com/help/doc-detail/93733.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/policies-with-full-administrative-privileges.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/policies-with-full-administrative-privileges.html" ], "Remediation": { "Code": { "CLI": "aliyun ram DetachPolicyFromUser --PolicyName --PolicyType Custom --UserName ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RAM Console**.\n2. Choose **Permissions** > **Policies**.\n3. From the Policy Type drop-down list, select **Custom Policy**.\n4. In the Policy Name column, click the name of the target policy.\n5. In the Policy Document section, edit the policy to remove the statement with full administrative privileges, or remove the policy from any RAM users, user groups, or roles that have this policy attached.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **RAM Console**\n2. Choose **Permissions** > **Policies**\n3. From the Policy Type drop-down list, select **Custom Policy**\n4. In the Policy Name column, click the name of the target policy\n5. In the Policy Document section, edit the policy to remove the statement with full administrative privileges, or remove the policy from any RAM users, user groups, or roles that have this policy attached", + "Text": "Remove or modify RAM policies that grant full administrative privileges and replace them with least-privilege policies.", "Url": "https://hub.prowler.com/check/ram_policy_no_administrative_privileges" } }, diff --git a/prowler/providers/alibabacloud/services/ram/ram_rotate_access_key_90_days/ram_rotate_access_key_90_days.metadata.json b/prowler/providers/alibabacloud/services/ram/ram_rotate_access_key_90_days/ram_rotate_access_key_90_days.metadata.json index c5f4bf24ce..da8d81b20f 100644 --- a/prowler/providers/alibabacloud/services/ram/ram_rotate_access_key_90_days/ram_rotate_access_key_90_days.metadata.json +++ b/prowler/providers/alibabacloud/services/ram/ram_rotate_access_key_90_days/ram_rotate_access_key_90_days.metadata.json @@ -2,32 +2,29 @@ "Provider": "alibabacloud", "CheckID": "ram_rotate_access_key_90_days", "CheckTitle": "Access keys are rotated every 90 days or less", - "CheckType": [ - "Unusual logon", - "Cloud threat detection" - ], + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:user/{user-name}/accesskey/{access-key-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRAMAccessKey", + "ResourceType": "ALIYUN::RAM::User", "ResourceGroup": "IAM", - "Description": "An **access key** consists of an access key ID and a secret, which are used to sign programmatic requests that you make to Alibaba Cloud.\n\nRAM users need their own access keys to make programmatic calls from SDKs, CLIs, or direct API calls. It is recommended that all access keys be **regularly rotated**.", - "Risk": "Access keys might be compromised by leaving them in code, configuration files, on-premise and cloud storages, and then stolen by attackers.\n\n**Rotating access keys** reduces the window of opportunity for a compromised access key to be used.", + "Description": "**Alibaba Cloud RAM** access keys consist of an access key ID and a secret, which are used to sign programmatic requests. RAM users need their own access keys to make programmatic calls from SDKs, CLIs, or direct API calls. It is recommended that all access keys be **regularly rotated** every 90 days or less to reduce the window of opportunity for compromised keys to be used.", + "Risk": "Access keys might be compromised by being left in code, configuration files, or cloud storage and then stolen by attackers. Without regular **access key rotation**, a compromised key can remain valid indefinitely, allowing persistent unauthorized access. This threatens the **confidentiality**, **integrity**, and **availability** of all resources accessible via those credentials.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/116401.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/access-keys-rotation.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/access-keys-rotation.html" ], "Remediation": { "Code": { "CLI": "aliyun ram CreateAccessKey --UserName && aliyun ram UpdateAccessKey --UserAccessKeyId --Status Inactive --UserName && aliyun ram DeleteAccessKey --UserAccessKeyId --UserName ", "NativeIaC": "", - "Other": "", + "Other": "1. Create a new **AccessKey pair** for rotation.\n2. Update all applications and systems to use the new AccessKey pair.\n3. **Disable** the original AccessKey pair.\n4. Confirm that your applications and systems are working.\n5. **Delete** the original AccessKey pair.", "Terraform": "" }, "Recommendation": { - "Text": "1. Create a new **AccessKey pair** for rotation\n2. Update all applications and systems to use the new AccessKey pair\n3. **Disable** the original AccessKey pair\n4. Confirm that your applications and systems are working\n5. **Delete** the original AccessKey pair", + "Text": "Rotate all RAM user access keys every 90 days or less to limit the impact of compromised credentials.", "Url": "https://hub.prowler.com/check/ram_rotate_access_key_90_days" } }, 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 1f6b644a46..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 @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ram_user_console_access_unused", - "CheckTitle": "Users not logged on for 90 days or longer are disabled for console logon", - "CheckType": [ - "Unusual logon", - "Abnormal account" - ], + "CheckTitle": "RAM user not logged on for 90 days or longer has console logon disabled", + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:user/{user-name}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRAMUser", + "ResourceType": "ALIYUN::RAM::User", "ResourceGroup": "IAM", - "Description": "Alibaba Cloud **RAM users** can log on to the Alibaba Cloud console by using their username and password.\n\nIf a user has not logged on for **90 days or longer**, it is recommended to disable the console access of the user.", - "Risk": "Disabling users from having unnecessary logon privileges will reduce the opportunity that an **abandoned user** or a user with **compromised password** to be exploited.\n\nInactive accounts are common targets for attackers attempting account takeover.", + "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 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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/inactive-ram-user.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/inactive-ram-user.html" ], "Remediation": { "Code": { "CLI": "aliyun ram DeleteLoginProfile --UserName ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RAM Console**.\n2. Choose **Identities** > **Users**.\n3. In the User Logon Name/Display Name column, click the username of the target RAM user.\n4. In the Console Logon Management section, click **Modify Logon Settings**.\n5. In the Console Password Logon section, select **Disabled**.\n6. Click **OK**.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **RAM Console**\n2. Choose **Identities** > **Users**\n3. In the User Logon Name/Display Name column, click the username of the target RAM user\n4. In the Console Logon Management section, click **Modify Logon Settings**\n5. In the Console Password Logon section, select **Disabled**\n6. Click **OK**", + "Text": "Disable console access for RAM users that have not logged on for 90 days or longer to reduce the attack surface.", "Url": "https://hub.prowler.com/check/ram_user_console_access_unused" } }, 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 11d8177294..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 @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "ram_user_mfa_enabled_console_access", - "CheckTitle": "Multi-factor authentication is enabled for all RAM users that have a console password", - "CheckType": [ - "Unusual logon", - "Abnormal account" - ], + "CheckTitle": "Multi-factor authentication is enabled for all RAM users with console access", + "CheckType": [], "ServiceName": "ram", "SubServiceName": "", - "ResourceIdTemplate": "acs:ram::account-id:user/{user-name}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudRAMUser", + "ResourceType": "ALIYUN::RAM::User", "ResourceGroup": "IAM", - "Description": "**Multi-Factor Authentication (MFA)** adds an extra layer of protection on top of a username and password.\n\nWith MFA enabled, when a user logs on to Alibaba Cloud, they will be 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 users that have a console password.", - "Risk": "**MFA** requires users to verify their identities by entering two authentication factors. When MFA is enabled, an attacker faces at least two different authentication mechanisms.\n\nThe additional security makes it significantly harder for an attacker to gain access even if passwords are compromised.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/ram-user-multi-factor-authentication-enabled.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RAM/ram-user-multi-factor-authentication-enabled.html" ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RAM Console**.\n2. For each user with console access, go to the user's details.\n3. In the **Console Logon Management** section, click **Modify Logon Settings**.\n4. For `Enable MFA`, select **Required**.\n5. Click **OK** to save the settings.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **RAM Console**\n2. For each user with console access, go to the user's details\n3. In the **Console Logon Management** section, click **Modify Logon Settings**\n4. For `Enable MFA`, select **Required**\n5. Click **OK** to save the settings", + "Text": "Enable MFA for all RAM users with console access to add an extra layer of authentication security.", "Url": "https://hub.prowler.com/check/ram_user_mfa_enabled_console_access" } }, diff --git a/prowler/providers/alibabacloud/services/rds/rds_instance_no_public_access_whitelist/rds_instance_no_public_access_whitelist.metadata.json b/prowler/providers/alibabacloud/services/rds/rds_instance_no_public_access_whitelist/rds_instance_no_public_access_whitelist.metadata.json index ac073851a7..02305aa74b 100644 --- a/prowler/providers/alibabacloud/services/rds/rds_instance_no_public_access_whitelist/rds_instance_no_public_access_whitelist.metadata.json +++ b/prowler/providers/alibabacloud/services/rds/rds_instance_no_public_access_whitelist/rds_instance_no_public_access_whitelist.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "rds_instance_no_public_access_whitelist", - "CheckTitle": "RDS Instances are not open to the world", - "CheckType": [ - "Intrusion into applications", - "Suspicious network connection" - ], + "CheckTitle": "RDS instance does not allow public access in the IP whitelist", + "CheckType": [], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "acs:rds:region:account-id:dbinstance/{dbinstance-id}", + "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "AlibabaCloudRDSDBInstance", + "ResourceType": "ALIYUN::RDS::DBInstance", "ResourceGroup": "database", - "Description": "Database Server should accept connections only from trusted **Network(s)/IP(s)** and restrict access from the world.\n\nTo minimize attack surface on a Database server Instance, only trusted/known and required IPs should be whitelisted. Authorized network should not have IPs/networks configured to `0.0.0.0` or `/0` which would allow access from anywhere in the world.", - "Risk": "Allowing **public access** (`0.0.0.0/0`) to the database significantly increases the risk of **brute-force attacks**, **unauthorized access**, and **data exfiltration**.\n\nDatabases exposed to the internet are prime targets for attackers.", + "Description": "**Alibaba Cloud RDS instances** should only accept connections from trusted networks and IP addresses. This check verifies that the IP whitelist does not contain entries such as `0.0.0.0/0` or `0.0.0.0` that would allow access from anywhere on the internet. Only specific, trusted IP addresses should be whitelisted to minimize the attack surface of the database server.", + "Risk": "Allowing **public access** (`0.0.0.0/0`) to the database significantly increases the risk of **brute-force attacks**, **unauthorized access**, and **data exfiltration**. Databases exposed to the internet are prime targets for attackers, and a successful breach can compromise **confidentiality**, **integrity**, and **availability** of all stored data.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/26198.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/disable-network-public-access.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/disable-network-public-access.html" ], "Remediation": { "Code": { - "CLI": "aliyun rds ModifySecurityIps --DBInstanceId --SecurityIps ", + "CLI": "aliyun rds ModifySecurityIps --DBInstanceId --SecurityIps ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RDS Console**.\n2. Select the target RDS instance.\n3. Go to **Data Security** > **Whitelist Settings** tab.\n4. Remove any `0.0.0.0` or `0.0.0.0/0` entries.\n5. Add only the specific IP addresses that need to access the instance.\n6. Click **OK** to save changes.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **RDS Console**\n2. Go to **Data Security** > **Whitelist Settings** tab\n3. Remove any `0.0.0.0` or `/0` entries\n4. Only add the IP addresses that need to access the instance", + "Text": "Restrict the RDS IP whitelist to only trusted IP addresses. Remove any entries that allow unrestricted access such as `0.0.0.0/0`.", "Url": "https://hub.prowler.com/check/rds_instance_no_public_access_whitelist" } }, diff --git a/prowler/providers/alibabacloud/services/rds/rds_instance_postgresql_log_connections_enabled/rds_instance_postgresql_log_connections_enabled.metadata.json b/prowler/providers/alibabacloud/services/rds/rds_instance_postgresql_log_connections_enabled/rds_instance_postgresql_log_connections_enabled.metadata.json index ee59a7cd50..e81fd5a60f 100644 --- a/prowler/providers/alibabacloud/services/rds/rds_instance_postgresql_log_connections_enabled/rds_instance_postgresql_log_connections_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/rds/rds_instance_postgresql_log_connections_enabled/rds_instance_postgresql_log_connections_enabled.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "rds_instance_postgresql_log_connections_enabled", - "CheckTitle": "Parameter log_connections is set to ON for PostgreSQL Database", - "CheckType": [ - "Intrusion into applications", - "Unusual logon" - ], + "CheckTitle": "RDS PostgreSQL instance has log_connections parameter enabled", + "CheckType": [], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "acs:rds:region:account-id:dbinstance/{dbinstance-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRDSDBInstance", + "ResourceType": "ALIYUN::RDS::DBInstance", "ResourceGroup": "database", - "Description": "Enable `log_connections` on **PostgreSQL Servers**. Enabling `log_connections` helps PostgreSQL Database log attempted connections to the server, as well as successful completion of client authentication.\n\nLog data can be used to identify, troubleshoot, and repair configuration errors and suboptimal performance.", - "Risk": "Without **connection logging**, unauthorized access attempts might go unnoticed, and troubleshooting connection issues becomes more difficult.\n\nThis data is essential for **security monitoring** and **incident investigation**.", + "Description": "**Alibaba Cloud RDS PostgreSQL instances** should have the `log_connections` parameter set to `on`. Enabling this parameter logs each attempted connection to the server, including successful client authentication. This log data is essential for identifying, troubleshooting, and repairing configuration errors, detecting unauthorized access attempts, and supporting **security auditing**.", + "Risk": "Without **connection logging** enabled, unauthorized access attempts to the database may go unnoticed, making it difficult to detect **brute-force attacks** or **credential compromise**. This gap in visibility impacts the ability to perform **security monitoring**, **incident investigation**, and **forensic analysis**, reducing overall **confidentiality** assurance.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/96751.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-log-connections-for-postgresql.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-log-connections-for-postgresql.html" ], "Remediation": { "Code": { "CLI": "aliyun rds ModifyParameter --DBInstanceId --Parameters \"{\\\"log_connections\\\":\\\"on\\\"}\"", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RDS Console**.\n2. Select the region and target PostgreSQL instance.\n3. In the left-side navigation pane, select **Parameters**.\n4. Find the `log_connections` parameter and set it to `on`.\n5. Click **Apply Changes**.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **RDS Console**\n2. Select the region and target instance\n3. In the left-side navigation pane, select **Parameters**\n4. Find the `log_connections` parameter and set it to `on`\n5. Click **Apply Changes**", + "Text": "Enable the `log_connections` parameter on all PostgreSQL RDS instances to log connection attempts for security monitoring and troubleshooting.", "Url": "https://hub.prowler.com/check/rds_instance_postgresql_log_connections_enabled" } }, diff --git a/prowler/providers/alibabacloud/services/rds/rds_instance_postgresql_log_disconnections_enabled/rds_instance_postgresql_log_disconnections_enabled.metadata.json b/prowler/providers/alibabacloud/services/rds/rds_instance_postgresql_log_disconnections_enabled/rds_instance_postgresql_log_disconnections_enabled.metadata.json index 2255790916..7c358b77df 100644 --- a/prowler/providers/alibabacloud/services/rds/rds_instance_postgresql_log_disconnections_enabled/rds_instance_postgresql_log_disconnections_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/rds/rds_instance_postgresql_log_disconnections_enabled/rds_instance_postgresql_log_disconnections_enabled.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "rds_instance_postgresql_log_disconnections_enabled", - "CheckTitle": "Server parameter log_disconnections is set to ON for PostgreSQL Database Server", - "CheckType": [ - "Intrusion into applications", - "Unusual logon" - ], + "CheckTitle": "RDS PostgreSQL instance has log_disconnections parameter enabled", + "CheckType": [], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "acs:rds:region:account-id:dbinstance/{dbinstance-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRDSDBInstance", + "ResourceType": "ALIYUN::RDS::DBInstance", "ResourceGroup": "database", - "Description": "Enable `log_disconnections` on **PostgreSQL Servers**. Enabling `log_disconnections` helps PostgreSQL Database log session terminations of the server, as well as duration of the session.\n\nLog data can be used to identify, troubleshoot, and repair configuration errors and suboptimal performance.", - "Risk": "Without **disconnection logging**, it's harder to track session durations and identify abnormal disconnection patterns that might indicate **attacks** or **stability issues**.", + "Description": "**Alibaba Cloud RDS PostgreSQL instances** should have the `log_disconnections` parameter set to `on`. Enabling this parameter logs session terminations and the duration of each session. This data is valuable for identifying abnormal disconnection patterns, troubleshooting performance issues, and supporting **security auditing** and **incident investigation**.", + "Risk": "Without **disconnection logging**, it is harder to track session durations and identify abnormal disconnection patterns that might indicate **attacks**, **session hijacking**, or **stability issues**. This reduces visibility into database activity, impacting **security monitoring** and the ability to perform effective **forensic analysis**.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/96751.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-log-disconnections-for-postgresql.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-log-disconnections-for-postgresql.html" ], "Remediation": { "Code": { "CLI": "aliyun rds ModifyParameter --DBInstanceId --Parameters \"{\\\"log_disconnections\\\":\\\"on\\\"}\"", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RDS Console**.\n2. Select the region and target PostgreSQL instance.\n3. In the left-side navigation pane, select **Parameters**.\n4. Find the `log_disconnections` parameter and set it to `on`.\n5. Click **Apply Changes**.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **RDS Console**\n2. Select the region and target instance\n3. In the left-side navigation pane, select **Parameters**\n4. Find the `log_disconnections` parameter and set it to `on`\n5. Click **Apply Changes**", + "Text": "Enable the `log_disconnections` parameter on all PostgreSQL RDS instances to log session terminations for security monitoring and troubleshooting.", "Url": "https://hub.prowler.com/check/rds_instance_postgresql_log_disconnections_enabled" } }, diff --git a/prowler/providers/alibabacloud/services/rds/rds_instance_postgresql_log_duration_enabled/rds_instance_postgresql_log_duration_enabled.metadata.json b/prowler/providers/alibabacloud/services/rds/rds_instance_postgresql_log_duration_enabled/rds_instance_postgresql_log_duration_enabled.metadata.json index dc5a2d9d3a..a0c04e5228 100644 --- a/prowler/providers/alibabacloud/services/rds/rds_instance_postgresql_log_duration_enabled/rds_instance_postgresql_log_duration_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/rds/rds_instance_postgresql_log_duration_enabled/rds_instance_postgresql_log_duration_enabled.metadata.json @@ -1,32 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "rds_instance_postgresql_log_duration_enabled", - "CheckTitle": "Server parameter log_duration is set to ON for PostgreSQL Database Server", - "CheckType": [ - "Intrusion into applications" - ], + "CheckTitle": "RDS PostgreSQL instance has log_duration parameter enabled", + "CheckType": [], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "acs:rds:region:account-id:dbinstance/{dbinstance-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRDSDBInstance", + "ResourceType": "ALIYUN::RDS::DBInstance", "ResourceGroup": "database", - "Description": "Enable `log_duration` on **PostgreSQL Servers**. Enabling `log_duration` helps PostgreSQL Database log the duration of each completed SQL statement which in turn generates query and error logs.\n\nQuery and error logs can be used to identify, troubleshoot, and repair configuration errors and sub-optimal performance.", - "Risk": "Without **duration logging**, it's difficult to identify **slow queries**, **performance bottlenecks**, and potential **DoS attempts**.\n\nThis information is critical for database performance tuning and security monitoring.", + "Description": "**Alibaba Cloud RDS PostgreSQL instances** should have the `log_duration` parameter set to `on`. Enabling this parameter logs the duration of each completed SQL statement, generating query and error logs that can be used to identify **slow queries**, troubleshoot performance issues, and detect potential **denial-of-service** patterns targeting the database.", + "Risk": "Without **duration logging**, it is difficult to identify **slow queries**, **performance bottlenecks**, and potential **DoS attempts** against the database. This lack of visibility impacts the ability to optimize database performance and detect **malicious activity** such as resource exhaustion attacks, reducing overall **availability** assurance.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/96751.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-log-duration-for-postgresql.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-log-duration-for-postgresql.html" ], "Remediation": { "Code": { "CLI": "aliyun rds ModifyParameter --DBInstanceId --Parameters \"{\\\"log_duration\\\":\\\"on\\\"}\"", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RDS Console**.\n2. Select the region and target PostgreSQL instance.\n3. In the left-side navigation pane, select **Parameters**.\n4. Find the `log_duration` parameter and set it to `on`.\n5. Click **Apply Changes**.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **RDS Console**\n2. Select the region and target instance\n3. In the left-side navigation pane, select **Parameters**\n4. Find the `log_duration` parameter and set it to `on`\n5. Click **Apply Changes**", + "Text": "Enable the `log_duration` parameter on all PostgreSQL RDS instances to log SQL statement durations for performance monitoring and security analysis.", "Url": "https://hub.prowler.com/check/rds_instance_postgresql_log_duration_enabled" } }, 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 2506245412..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 @@ -1,32 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "rds_instance_sql_audit_enabled", - "CheckTitle": "Auditing is set to On for applicable database instances", - "CheckType": [ - "Intrusion into applications" - ], + "CheckTitle": "RDS instance has SQL auditing enabled", + "CheckType": [], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "acs:rds:region:account-id:dbinstance/{dbinstance-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRDSDBInstance", + "ResourceType": "ALIYUN::RDS::DBInstance", "ResourceGroup": "database", - "Description": "Enable **SQL auditing** on all RDS instances (except SQL Server 2012/2016/2017 and MariaDB TX). Auditing tracks database events and writes them to an audit log.\n\nIt helps to maintain **regulatory compliance**, understand database activity, and gain insight into discrepancies and anomalies that could indicate business concerns or suspected security violations.", - "Risk": "Without **SQL auditing**, it's difficult to detect **unauthorized access**, **data breaches**, or **malicious activity** within the database.\n\nIt also hinders **forensic investigations** and compliance reporting.", + "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": [ "https://www.alibabacloud.com/help/doc-detail/96123.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-audit-logs.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-audit-logs.html" ], "Remediation": { "Code": { "CLI": "aliyun rds ModifySQLCollectorPolicy --DBInstanceId --SQLCollectorStatus Enable --StoragePeriod ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RDS Console**.\n2. Select the target RDS instance.\n3. In the left-side navigation pane, select **SQL Explorer**.\n4. Click **Activate Now**.\n5. Specify the SQL log storage duration.\n6. Click **Activate**.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **RDS Console**\n2. In the left-side navigation pane, select **SQL Explorer**\n3. Click **Activate Now**\n4. Specify the SQL log storage duration\n5. Click **Activate**", + "Text": "Enable **SQL auditing** (SQL Explorer) on all applicable RDS instances to track database events and maintain audit logs for security monitoring and compliance.", "Url": "https://hub.prowler.com/check/rds_instance_sql_audit_enabled" } }, diff --git a/prowler/providers/alibabacloud/services/rds/rds_instance_sql_audit_retention/rds_instance_sql_audit_retention.metadata.json b/prowler/providers/alibabacloud/services/rds/rds_instance_sql_audit_retention/rds_instance_sql_audit_retention.metadata.json index 17300c26e5..2402bf7146 100644 --- a/prowler/providers/alibabacloud/services/rds/rds_instance_sql_audit_retention/rds_instance_sql_audit_retention.metadata.json +++ b/prowler/providers/alibabacloud/services/rds/rds_instance_sql_audit_retention/rds_instance_sql_audit_retention.metadata.json @@ -1,32 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "rds_instance_sql_audit_retention", - "CheckTitle": "Auditing Retention is greater than the configured period", - "CheckType": [ - "Intrusion into applications" - ], + "CheckTitle": "RDS instance SQL audit retention period meets the configured minimum", + "CheckType": [], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "acs:rds:region:account-id:dbinstance/{dbinstance-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRDSDBInstance", + "ResourceType": "ALIYUN::RDS::DBInstance", "ResourceGroup": "database", - "Description": "Database **SQL Audit Retention** should be configured to be greater than or equal to the configured period (default: **6 months / 180 days**).\n\nAudit Logs can be used to check for anomalies and give insight into suspected breaches or misuse of information and access.", - "Risk": "**Short retention periods** for audit logs can result in the loss of critical forensic data needed for **incident investigation** and **compliance auditing**.\n\nMany regulations require minimum retention periods for audit data.", + "Description": "**Alibaba Cloud RDS instances** with SQL auditing enabled should have a retention period configured to be greater than or equal to the required minimum (default: **6 months / 180 days**). Audit logs are essential for checking anomalies, understanding database activity, and gaining insight into suspected breaches or misuse of information and access.", + "Risk": "**Short retention periods** for audit logs can result in the loss of critical forensic data needed for **incident investigation**, **compliance auditing**, and **regulatory reporting**. Many regulations and security frameworks require minimum retention periods for audit data, and failing to meet them can result in **non-compliance** penalties.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/96123.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/configure-log-retention-period.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/configure-log-retention-period.html" ], "Remediation": { "Code": { "CLI": "aliyun rds ModifySQLCollectorPolicy --DBInstanceId --SQLCollectorStatus Enable --StoragePeriod 180", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RDS Console**.\n2. Select the target RDS instance.\n3. In the left-side navigation pane, select **SQL Explorer**.\n4. Click **Service Setting**.\n5. Enable `Activate SQL Explorer` if not already active.\n6. Set the storage duration to `6 months` or longer.\n7. Click **OK** to save changes.", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **RDS Console**\n2. Select **SQL Explorer**\n3. Click **Service Setting**\n4. Enable `Activate SQL Explorer`\n5. Set the storage duration to `6 months` or longer", + "Text": "Configure the SQL audit retention period to at least **180 days** (6 months) on all RDS instances to ensure adequate audit log availability for compliance and forensic purposes.", "Url": "https://hub.prowler.com/check/rds_instance_sql_audit_retention" } }, diff --git a/prowler/providers/alibabacloud/services/rds/rds_instance_ssl_enabled/rds_instance_ssl_enabled.metadata.json b/prowler/providers/alibabacloud/services/rds/rds_instance_ssl_enabled/rds_instance_ssl_enabled.metadata.json index c00ac879ec..aa1ff3224f 100644 --- a/prowler/providers/alibabacloud/services/rds/rds_instance_ssl_enabled/rds_instance_ssl_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/rds/rds_instance_ssl_enabled/rds_instance_ssl_enabled.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "rds_instance_ssl_enabled", - "CheckTitle": "RDS instance requires all incoming connections to use SSL", - "CheckType": [ - "Sensitive file tampering", - "Intrusion into applications" - ], + "CheckTitle": "RDS instance has SSL encryption enabled", + "CheckType": [], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "acs:rds:region:account-id:dbinstance/{dbinstance-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRDSDBInstance", + "ResourceType": "ALIYUN::RDS::DBInstance", "ResourceGroup": "database", - "Description": "It is recommended to enforce all incoming connections to SQL database instances to use **SSL**.\n\nSQL database connections if successfully intercepted (MITM) can reveal sensitive data like credentials, database queries, and query outputs. For security, it is recommended to always use SSL encryption when connecting to your instance.", - "Risk": "If **SSL is not enabled**, data in transit (including credentials and query results) can be intercepted by attackers performing **Man-in-the-Middle (MITM) attacks**.\n\nThis compromises data confidentiality and integrity.", + "Description": "**Alibaba Cloud RDS instances** should enforce **SSL encryption** for all incoming connections. SSL protects data in transit between the application and the database, preventing interception of sensitive data such as credentials, database queries, and query outputs. This check verifies that SSL encryption is enabled on the RDS instance.", + "Risk": "If **SSL is not enabled**, data in transit including credentials and query results can be intercepted by attackers performing **Man-in-the-Middle (MITM) attacks**. This compromises data **confidentiality** and **integrity**, potentially leading to **credential theft**, **data exfiltration**, and unauthorized manipulation of database communications.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/32474.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-encryption-in-transit.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-encryption-in-transit.html" ], "Remediation": { "Code": { "CLI": "aliyun rds ModifyDBInstanceSSL --DBInstanceId --SSLEnabled 1", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RDS Console**.\n2. Select the region and target instance.\n3. In the left-side navigation pane, click **Data Security**.\n4. Click the **SSL Encryption** tab.\n5. Click the switch next to **Disabled** to enable SSL encryption.\n6. Download the SSL CA certificate for client configuration.", "Terraform": "resource \"alicloud_db_instance\" \"example\" {\n engine = \"MySQL\"\n engine_version = \"8.0\"\n instance_type = \"rds.mysql.s1.small\"\n instance_storage = 20\n ssl_action = \"Open\"\n}" }, "Recommendation": { - "Text": "1. Log on to the **RDS Console**\n2. Select the region and target instance\n3. In the left-side navigation pane, click **Data Security**\n4. Click the **SSL Encryption** tab\n5. Click the switch next to **Disabled** in the SSL Encryption parameter to enable it", + "Text": "Enable **SSL encryption** on all RDS instances to protect data in transit and prevent Man-in-the-Middle attacks.", "Url": "https://hub.prowler.com/check/rds_instance_ssl_enabled" } }, diff --git a/prowler/providers/alibabacloud/services/rds/rds_instance_tde_enabled/rds_instance_tde_enabled.metadata.json b/prowler/providers/alibabacloud/services/rds/rds_instance_tde_enabled/rds_instance_tde_enabled.metadata.json index da57605bfe..0b033bdbb4 100644 --- a/prowler/providers/alibabacloud/services/rds/rds_instance_tde_enabled/rds_instance_tde_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/rds/rds_instance_tde_enabled/rds_instance_tde_enabled.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "rds_instance_tde_enabled", - "CheckTitle": "TDE is set to Enabled on for applicable database instance", - "CheckType": [ - "Sensitive file tampering", - "Intrusion into applications" - ], + "CheckTitle": "RDS instance has Transparent Data Encryption enabled", + "CheckType": [], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "acs:rds:region:account-id:dbinstance/{dbinstance-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudRDSDBInstance", + "ResourceType": "ALIYUN::RDS::DBInstance", "ResourceGroup": "database", - "Description": "Enable **Transparent Data Encryption (TDE)** on every RDS instance. RDS Database TDE helps protect against the threat of malicious activity by performing real-time encryption and decryption of the database, associated backups, and log files at rest.\n\nNo changes to the application are required.", - "Risk": "**Data at rest** that is not encrypted is vulnerable to unauthorized access if the underlying storage media or backups are compromised.\n\nTDE protects against physical theft and unauthorized access to storage systems.", + "Description": "**Alibaba Cloud RDS instances** should have **Transparent Data Encryption (TDE)** enabled. TDE performs real-time encryption and decryption of the database, associated backups, and log files at rest, without requiring changes to the application. This check verifies that TDE is enabled to protect sensitive data stored in the RDS instance from unauthorized physical access.", + "Risk": "**Data at rest** that is not encrypted is vulnerable to unauthorized access if the underlying storage media or backups are compromised, stolen, or improperly decommissioned. Without TDE, attackers with physical or administrative access to the storage layer can read sensitive data directly, impacting **confidentiality** and potentially leading to **data breaches**.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/33510.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-sql-database-tde.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-sql-database-tde.html" ], "Remediation": { "Code": { "CLI": "aliyun rds ModifyDBInstanceTDE --DBInstanceId --TDEStatus Enabled", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RDS Console**.\n2. Select the target RDS instance.\n3. Go to **Data Security** > **TDE** tab.\n4. Find TDE Status and click the switch next to **Disabled**.\n5. Choose automatically generated key or custom key.\n6. Click **Confirm**.", "Terraform": "resource \"alicloud_db_instance\" \"example\" {\n engine = \"MySQL\"\n engine_version = \"8.0\"\n instance_type = \"rds.mysql.s1.small\"\n instance_storage = 20\n tde_status = \"Enabled\"\n}" }, "Recommendation": { - "Text": "1. Log on to the **RDS Console**\n2. Go to **Data Security** > **TDE** tab\n3. Find TDE Status and click the switch next to **Disabled**\n4. Choose automatically generated key or custom key\n5. Click **Confirm**", + "Text": "Enable **Transparent Data Encryption (TDE)** on all applicable RDS instances to protect data at rest from unauthorized physical access.", "Url": "https://hub.prowler.com/check/rds_instance_tde_enabled" } }, diff --git a/prowler/providers/alibabacloud/services/rds/rds_instance_tde_key_custom/rds_instance_tde_key_custom.metadata.json b/prowler/providers/alibabacloud/services/rds/rds_instance_tde_key_custom/rds_instance_tde_key_custom.metadata.json index 5b911a5ace..24fe497d8d 100644 --- a/prowler/providers/alibabacloud/services/rds/rds_instance_tde_key_custom/rds_instance_tde_key_custom.metadata.json +++ b/prowler/providers/alibabacloud/services/rds/rds_instance_tde_key_custom/rds_instance_tde_key_custom.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "rds_instance_tde_key_custom", - "CheckTitle": "RDS instance TDE protector is encrypted with BYOK (Use your own key)", - "CheckType": [ - "Sensitive file tampering", - "Intrusion into applications" - ], + "CheckTitle": "RDS instance TDE uses a customer-managed key (BYOK)", + "CheckType": [], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "acs:rds:region:account-id:dbinstance/{dbinstance-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudRDSDBInstance", + "ResourceType": "ALIYUN::RDS::DBInstance", "ResourceGroup": "database", - "Description": "**TDE with BYOK** support provides increased transparency and control, increased security with an HSM-backed KMS service, and promotion of separation of duties.\n\nBased on business needs or criticality of data, it is recommended that the TDE protector is encrypted by a key that is managed by the data owner (**BYOK**).", - "Risk": "Using **service-managed keys** means the cloud provider manages the encryption keys. **BYOK (Bring Your Own Key)** gives you full control over the key lifecycle and permissions.\n\nThis ensures that even the cloud provider cannot access your data without your explicit permission.", + "Description": "**Alibaba Cloud RDS instances** with TDE enabled should use a **customer-managed key (BYOK)** rather than a service-managed key. BYOK provides increased transparency and control over the encryption key lifecycle, enhanced security through an HSM-backed **KMS** service, and promotes separation of duties between the data owner and the cloud provider.", + "Risk": "Using **service-managed keys** means the cloud provider manages the encryption keys, limiting the data owner's control over key access and rotation. Without **BYOK (Bring Your Own Key)**, the cloud provider retains the ability to access encrypted data, reducing **confidentiality** assurance and making it harder to enforce **separation of duties** and **key lifecycle management** policies.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/96121.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-tde-with-cmk.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-RDS/enable-tde-with-cmk.html" ], "Remediation": { "Code": { "CLI": "aliyun rds ModifyDBInstanceTDE --DBInstanceId --TDEStatus Enabled --EncryptionKey ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **RDS Console**.\n2. Select the target RDS instance.\n3. Go to **Data Security** > **TDE** tab.\n4. Click the switch next to **Disabled** (or modify existing TDE configuration).\n5. In the displayed dialog box, choose **custom key** and select your KMS key.\n6. Click **Confirm**.", "Terraform": "resource \"alicloud_db_instance\" \"example\" {\n engine = \"MySQL\"\n engine_version = \"8.0\"\n instance_type = \"rds.mysql.s1.small\"\n instance_storage = 20\n tde_status = \"Enabled\"\n encryption_key = alicloud_kms_key.example.id\n}" }, "Recommendation": { - "Text": "1. Log on to the **RDS Console**\n2. Go to **Data Security** > **TDE** tab\n3. Click the switch next to **Disabled**\n4. In the displayed dialog box, choose **custom key**\n5. Click **Confirm**", + "Text": "Configure TDE on RDS instances to use a **customer-managed key (BYOK)** from KMS for full control over the encryption key lifecycle and enhanced security.", "Url": "https://hub.prowler.com/check/rds_instance_tde_key_custom" } }, diff --git a/prowler/providers/alibabacloud/services/securitycenter/securitycenter_advanced_or_enterprise_edition/securitycenter_advanced_or_enterprise_edition.metadata.json b/prowler/providers/alibabacloud/services/securitycenter/securitycenter_advanced_or_enterprise_edition/securitycenter_advanced_or_enterprise_edition.metadata.json index ff90197827..429663bc2a 100644 --- a/prowler/providers/alibabacloud/services/securitycenter/securitycenter_advanced_or_enterprise_edition/securitycenter_advanced_or_enterprise_edition.metadata.json +++ b/prowler/providers/alibabacloud/services/securitycenter/securitycenter_advanced_or_enterprise_edition/securitycenter_advanced_or_enterprise_edition.metadata.json @@ -1,37 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "securitycenter_advanced_or_enterprise_edition", - "CheckTitle": "Security Center is Advanced or Enterprise Edition", - "CheckType": [ - "Suspicious process", - "Webshell", - "Unusual logon", - "Sensitive file tampering", - "Malicious software", - "Precision defense" - ], + "CheckTitle": "Security Center is using Advanced or Enterprise Edition", + "CheckType": [], "ServiceName": "securitycenter", "SubServiceName": "", - "ResourceIdTemplate": "acs:sas::account-id:security-center", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSecurityCenter", + "ResourceType": "ALIYUN::SAS::Instance", "ResourceGroup": "security", - "Description": "The **Advanced or Enterprise Edition** enables threat detection for network and endpoints, providing **malware detection**, **webshell detection**, and **anomaly detection** in Security Center.", - "Risk": "Using **Basic or Free Edition** of Security Center may not provide comprehensive protection against cloud threats.\n\n**Advanced or Enterprise Edition** allows for full protection to defend against cloud threats.", + "Description": "**Alibaba Cloud Security Center** should be running the **Advanced** or **Enterprise Edition** to enable comprehensive threat detection capabilities for network and endpoints. These editions provide **malware detection**, **webshell detection**, **anomaly detection**, and **precision defense** features that are not available in the Basic or Free editions.", + "Risk": "Using the **Basic or Free Edition** of Security Center limits threat detection capabilities to basic vulnerability scanning only. Without the **Advanced or Enterprise Edition**, critical protections such as **malware detection**, **intrusion prevention**, **webshell detection**, and **anomalous behavior analysis** are unavailable, leaving workloads exposed to sophisticated cloud threats.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/product/28498.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SecurityCenter/security-center-plan.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SecurityCenter/security-center-plan.html" ], "Remediation": { "Code": { - "CLI": "Logon to Security Center Console > Select Overview > Click Upgrade > Select Advanced or Enterprise Edition > Finish order placement", + "CLI": "", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **Security Center Console**\n2. Select **Overview**\n3. Click **Upgrade**\n4. Select **Advanced** or **Enterprise Edition**\n5. Finish order placement", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **Security Center Console**\n2. Select **Overview**\n3. Click **Upgrade**\n4. Select **Advanced** or **Enterprise Edition**\n5. Finish order placement", + "Text": "Upgrade Security Center to the Advanced or Enterprise Edition to enable comprehensive threat detection including malware detection, webshell detection, and anomaly detection capabilities.", "Url": "https://hub.prowler.com/check/securitycenter_advanced_or_enterprise_edition" } }, diff --git a/prowler/providers/alibabacloud/services/securitycenter/securitycenter_all_assets_agent_installed/securitycenter_all_assets_agent_installed.metadata.json b/prowler/providers/alibabacloud/services/securitycenter/securitycenter_all_assets_agent_installed/securitycenter_all_assets_agent_installed.metadata.json index 97decc7e0a..bd63eed35f 100644 --- a/prowler/providers/alibabacloud/services/securitycenter/securitycenter_all_assets_agent_installed/securitycenter_all_assets_agent_installed.metadata.json +++ b/prowler/providers/alibabacloud/services/securitycenter/securitycenter_all_assets_agent_installed/securitycenter_all_assets_agent_installed.metadata.json @@ -1,36 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "securitycenter_all_assets_agent_installed", - "CheckTitle": "All assets are installed with security agent", - "CheckType": [ - "Suspicious process", - "Webshell", - "Unusual logon", - "Sensitive file tampering", - "Malicious software" - ], + "CheckTitle": "All assets have the Security Center agent installed", + "CheckType": [], "ServiceName": "securitycenter", "SubServiceName": "", - "ResourceIdTemplate": "acs:sas:region:account-id:machine/{machine-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudSecurityCenterMachine", + "ResourceType": "ALIYUN::SAS::Instance", "ResourceGroup": "security", - "Description": "The endpoint protection of **Security Center** requires an agent to be installed on the endpoint to work. Such an agent-based approach allows the security center to provide comprehensive endpoint intrusion detection and protection capabilities.\n\nThis includes remote logon detection, **webshell detection** and removal, **anomaly detection** (detection of abnormal process behaviors and network connections), and detection of changes in key files and suspicious accounts.", - "Risk": "Assets without **Security Center agent** installed are not protected by endpoint intrusion detection and protection capabilities, leaving them vulnerable to security threats.\n\nUnprotected assets become blind spots in your security monitoring.", + "Description": "**Alibaba Cloud Security Center** requires an agent to be installed on each endpoint to provide comprehensive endpoint intrusion detection and protection capabilities. The agent enables remote logon detection, **webshell detection** and removal, **anomaly detection** of abnormal process behaviors and network connections, and monitoring of changes to key files and suspicious accounts.", + "Risk": "Assets without the **Security Center agent** installed become blind spots in security monitoring, as they are not protected by endpoint intrusion detection capabilities. This leaves them vulnerable to **malware infections**, **unauthorized access**, **webshell attacks**, and **anomalous process execution** without any alerts being generated.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/111650.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SecurityCenter/install-security-agent.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SecurityCenter/install-security-agent.html" ], "Remediation": { "Code": { "CLI": "aliyun sas InstallUninstallAegis --InstanceIds ,", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **Security Center Console**\n2. Select **Settings**\n3. Click **Agent**\n4. On the `Client to be installed` tab, select all items on the list\n5. Click **One-click installation** to install the agent on all assets", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **Security Center Console**\n2. Select **Settings**\n3. Click **Agent**\n4. On the `Client to be installed` tab, select all items on the list\n5. Click **One-click installation** to install the agent on all assets", + "Text": "Install the Security Center agent on all assets to enable comprehensive endpoint intrusion detection and protection, including webshell detection, anomaly detection, and remote logon monitoring.", "Url": "https://hub.prowler.com/check/securitycenter_all_assets_agent_installed" } }, diff --git a/prowler/providers/alibabacloud/services/securitycenter/securitycenter_notification_enabled_high_risk/securitycenter_notification_enabled_high_risk.metadata.json b/prowler/providers/alibabacloud/services/securitycenter/securitycenter_notification_enabled_high_risk/securitycenter_notification_enabled_high_risk.metadata.json index 480e46fb8b..c688baaa48 100644 --- a/prowler/providers/alibabacloud/services/securitycenter/securitycenter_notification_enabled_high_risk/securitycenter_notification_enabled_high_risk.metadata.json +++ b/prowler/providers/alibabacloud/services/securitycenter/securitycenter_notification_enabled_high_risk/securitycenter_notification_enabled_high_risk.metadata.json @@ -1,36 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "securitycenter_notification_enabled_high_risk", - "CheckTitle": "Notification is enabled on all high risk items", - "CheckType": [ - "Suspicious process", - "Webshell", - "Unusual logon", - "Sensitive file tampering", - "Malicious software" - ], + "CheckTitle": "Notifications are enabled for all high-risk items in Security Center", + "CheckType": [], "ServiceName": "securitycenter", "SubServiceName": "", - "ResourceIdTemplate": "acs:sas::account-id:notice-config/{project}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSecurityCenterNoticeConfig", + "ResourceType": "ALIYUN::SAS::Instance", "ResourceGroup": "security", - "Description": "Enable all **risk item notifications** in Vulnerability, Baseline Risks, Alerts, and AccessKey Leak event detection categories.\n\nThis ensures that relevant security operators receive notifications as soon as security events occur.", - "Risk": "Without **notifications enabled** for high-risk items, security operators may not be aware of critical security events in a timely manner, potentially leading to **delayed response** and **increased security exposure**.", + "Description": "**Alibaba Cloud Security Center** should have all **risk item notifications** enabled across Vulnerability, Baseline Risks, Alerts, and AccessKey Leak event detection categories. This ensures that relevant security operators receive notifications as soon as critical security events occur, enabling timely incident response.", + "Risk": "Without **notifications enabled** for high-risk items in Security Center, security operators may not be aware of critical security events such as **vulnerability discoveries**, **baseline violations**, **intrusion alerts**, and **AccessKey leaks** in a timely manner. This leads to **delayed incident response** and **prolonged security exposure**, increasing the potential impact of threats.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/111648.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SecurityCenter/enable-high-risk-item-notifications.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SecurityCenter/enable-high-risk-item-notifications.html" ], "Remediation": { "Code": { "CLI": "aliyun sas ModifyNoticeConfig --Project --Route ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **Security Center Console**\n2. Select **Settings**\n3. Click **Notification**\n4. Enable all high-risk items on Notification setting\n\nRoute values: `1`=text message, `2`=email, `3`=internal message, `4`=text+email, `5`=text+internal, `6`=email+internal, `7`=all methods", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **Security Center Console**\n2. Select **Settings**\n3. Click **Notification**\n4. Enable all high-risk items on Notification setting\n\nRoute values: `1`=text message, `2`=email, `3`=internal message, `4`=text+email, `5`=text+internal, `6`=email+internal, `7`=all methods", + "Text": "Enable notifications for all high-risk items in Security Center including vulnerabilities, baseline risks, alerts, and AccessKey leak detection to ensure timely incident response.", "Url": "https://hub.prowler.com/check/securitycenter_notification_enabled_high_risk" } }, diff --git a/prowler/providers/alibabacloud/services/securitycenter/securitycenter_vulnerability_scan_enabled/securitycenter_vulnerability_scan_enabled.metadata.json b/prowler/providers/alibabacloud/services/securitycenter/securitycenter_vulnerability_scan_enabled/securitycenter_vulnerability_scan_enabled.metadata.json index c302a79842..6d2033f02f 100644 --- a/prowler/providers/alibabacloud/services/securitycenter/securitycenter_vulnerability_scan_enabled/securitycenter_vulnerability_scan_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/securitycenter/securitycenter_vulnerability_scan_enabled/securitycenter_vulnerability_scan_enabled.metadata.json @@ -2,32 +2,29 @@ "Provider": "alibabacloud", "CheckID": "securitycenter_vulnerability_scan_enabled", "CheckTitle": "Scheduled vulnerability scan is enabled on all servers", - "CheckType": [ - "Malicious software", - "Web application threat detection" - ], + "CheckType": [], "ServiceName": "securitycenter", "SubServiceName": "", - "ResourceIdTemplate": "acs:sas::account-id:vulnerability-scan-config", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AlibabaCloudSecurityCenterVulConfig", + "ResourceType": "ALIYUN::SAS::Instance", "ResourceGroup": "security", - "Description": "Ensure that **scheduled vulnerability scan** is enabled on all servers.\n\nBe sure that vulnerability scanning is performed periodically to discover system vulnerabilities in time.", - "Risk": "Without **scheduled vulnerability scans** enabled, system vulnerabilities may not be discovered in a timely manner, leaving systems exposed to **known security threats** and **exploits**.", + "Description": "**Alibaba Cloud Security Center** should have **scheduled vulnerability scanning** enabled on all servers to periodically discover system vulnerabilities. The scan should cover all vulnerability types including `yum`, `cve`, `sys`, `cms`, and `emg` to ensure comprehensive detection of known security weaknesses across the infrastructure.", + "Risk": "Without **scheduled vulnerability scans** enabled, system vulnerabilities may remain undetected for extended periods. This leaves servers exposed to **known security exploits**, **privilege escalation attacks**, and **malware infections** that target unpatched software, increasing the overall attack surface and risk of compromise.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/109076.htm", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SecurityCenter/enable-scheduled-vulnerability-scan.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SecurityCenter/enable-scheduled-vulnerability-scan.html" ], "Remediation": { "Code": { "CLI": "aliyun sas ModifyVulConfig --Type --Config on", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **Security Center Console**\n2. Select **Vulnerabilities**\n3. Click **Settings**\n4. Apply all types of vulnerabilities (`yum`, `cve`, `sys`, `cms`, `emg`)\n5. Enable **High** (asap) and **Medium** (later) vulnerability scan levels", "Terraform": "" }, "Recommendation": { - "Text": "1. Log on to the **Security Center Console**\n2. Select **Vulnerabilities**\n3. Click **Settings**\n4. Apply all types of vulnerabilities (`yum`, `cve`, `sys`, `cms`, `emg`)\n5. Enable **High** (asap) and **Medium** (later) vulnerability scan levels", + "Text": "Enable scheduled vulnerability scanning on all servers in Security Center, covering all vulnerability types to ensure timely discovery and remediation of known security weaknesses.", "Url": "https://hub.prowler.com/check/securitycenter_vulnerability_scan_enabled" } }, 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 94baf55bc5..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 @@ -2,28 +2,25 @@ "Provider": "alibabacloud", "CheckID": "sls_cloud_firewall_changes_alert_enabled", "CheckTitle": "Log monitoring and alerts are set up for Cloud Firewall changes", - "CheckType": [ - "Suspicious network connection", - "Cloud threat detection" - ], + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "It is recommended that a **metric filter and alarm** be established for **Cloud Firewall** rule changes.", - "Risk": "Monitoring for **Create** or **Update** firewall rule events gives insight into network access changes and may reduce the time it takes to detect **suspicious activity**.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/cloudfirewall-control-policy-changes-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/cloudfirewall-control-policy-changes-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name cloud-firewall-changes --alert-displayname 'Cloud Firewall Changes Alert' --condition 'event.serviceName: CloudFirewall and (event.eventName: CreateControlPolicy or event.eventName: ModifyControlPolicy or event.eventName: DeleteControlPolicy)' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for Cloud Firewall changes: `event.serviceName: CloudFirewall and (event.eventName: CreateControlPolicy or event.eventName: ModifyControlPolicy or event.eventName: DeleteControlPolicy)`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { 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 2c564f7bdb..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 @@ -1,29 +1,26 @@ { "Provider": "alibabacloud", "CheckID": "sls_customer_created_cmk_changes_alert_enabled", - "CheckTitle": "A log monitoring and alerts are set up for disabling or deletion of customer created CMKs", - "CheckType": [ - "Sensitive file tampering", - "Cloud threat detection" - ], + "CheckTitle": "A log monitoring and alert is set up for disabling or deletion of customer created CMKs", + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "Real-time monitoring of API calls can be achieved by directing **ActionTrail Logs** to Log Service and establishing corresponding query and alarms.\n\nIt is recommended that a query and alarm be established for customer-created **KMS keys** which have changed state to disabled or deletion.", - "Risk": "Data encrypted with **disabled or deleted keys** will no longer be accessible.\n\nThis could lead to **data loss** or **business disruption** if keys are inadvertently or maliciously disabled.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/kms-cmk-config-changes-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/kms-cmk-config-changes-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name cmk-changes --alert-displayname 'CMK Changes Alert' --condition 'event.serviceName: Kms and (event.eventName: DisableKey or event.eventName: ScheduleKeyDeletion)' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for CMK changes: `event.serviceName: Kms and (event.eventName: DisableKey or event.eventName: ScheduleKeyDeletion)`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { 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 6b5b4e5420..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 @@ -2,28 +2,26 @@ "Provider": "alibabacloud", "CheckID": "sls_logstore_retention_period", "CheckTitle": "Logstore data retention period is set to the recommended period (default 365 days)", - "CheckType": [ - "Cloud threat detection" - ], + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/logstore/logstore-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSLogStore", + "ResourceType": "ALIYUN::SLS::Logstore", "ResourceGroup": "monitoring", - "Description": "Ensure **Activity Log Retention** is set for **365 days** or greater.", - "Risk": "Logstore lifecycle controls how your activity log is exported and retained. It is recommended to retain your activity log for **365 days or more** to have time to respond to any incidents.\n\nShort retention periods may result in loss of **forensic evidence** needed for security investigations.", + "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 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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/sufficient-logstore-data-retention-period.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/sufficient-logstore-data-retention-period.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls update-logstore --project --logstore --ttl 365", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. Log on to the **SLS Console**.\n2. Find the project in the **Projects** section.\n3. Click the **Modify** icon next to the target Logstore.\n4. Set the `Data Retention Period` to `365` days or greater.\n5. Click **Save** to apply the changes.", + "Terraform": "resource \"alicloud_log_store\" \"example\" {\n project = alicloud_log_project.example.name\n name = \"example-logstore\"\n retention_period = 365\n}" }, "Recommendation": { "Text": "1. Log on to the **SLS Console**\n2. Find the project in the Projects section\n3. Click **Modify** icon next to the Logstore\n4. Modify the `Data Retention Period` to `365` or greater", 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 dac8630a84..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 @@ -1,29 +1,26 @@ { "Provider": "alibabacloud", "CheckID": "sls_management_console_authentication_failures_alert_enabled", - "CheckTitle": "A log monitoring and alerts are set up for Management Console authentication failures", - "CheckType": [ - "Unusual logon", - "Abnormal account" - ], + "CheckTitle": "A log monitoring and alert is set up for Management Console authentication failures", + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "Real-time monitoring of API calls can be achieved by directing **ActionTrail Logs** to Log Service and establishing corresponding query and alarms.\n\nIt is recommended that a query and alarm be established for **failed console authentication attempts**.", - "Risk": "Monitoring **failed console logins** may decrease lead time to detect an attempt to **brute force** a credential, which may provide an indicator (such as source IP) that can be used in other event correlation.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/account-continuous-login-failures-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/account-continuous-login-failures-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name console-auth-failures --alert-displayname 'Console Authentication Failures Alert' --condition 'event.eventName: ConsoleSignin and event.errorCode: *' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for failed console authentication: `event.eventName: ConsoleSignin and event.errorCode: *`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { 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 14cb54b236..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 @@ -1,29 +1,26 @@ { "Provider": "alibabacloud", "CheckID": "sls_management_console_signin_without_mfa_alert_enabled", - "CheckTitle": "A log monitoring and alerts are set up for Management Console sign-in without MFA", - "CheckType": [ - "Unusual logon", - "Abnormal account" - ], + "CheckTitle": "A log monitoring and alert is set up for Management Console sign-in without MFA", + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "Real-time monitoring of API calls can be achieved by directing **ActionTrail Logs** to Log Service and establishing corresponding query and alarms.\n\nIt is recommended that a query and alarm be established for console logins that are not protected by **multi-factor authentication (MFA)**.", - "Risk": "Monitoring for **single-factor console logins** will increase visibility into accounts that are not protected by MFA.\n\nThis helps identify potential security gaps in authentication enforcement.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/single-factor-console-logins-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/single-factor-console-logins-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name console-signin-no-mfa --alert-displayname 'Console Sign-in Without MFA Alert' --condition 'event.eventName: ConsoleSignin and event.additionalEventData.MFAUsed: false' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for sign-in without MFA: `event.eventName: ConsoleSignin and event.additionalEventData.MFAUsed: false`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { 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 60c37a3b6f..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 @@ -1,29 +1,26 @@ { "Provider": "alibabacloud", "CheckID": "sls_oss_bucket_policy_changes_alert_enabled", - "CheckTitle": "A log monitoring and alerts are set up for OSS bucket policy changes", - "CheckType": [ - "Sensitive file tampering", - "Cloud threat detection" - ], + "CheckTitle": "A log monitoring and alerts is set up for OSS bucket policy changes", + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "Real-time monitoring of API calls can be achieved by directing **ActionTrail Logs** to Log Service and establishing corresponding query and alarms.\n\nIt is recommended that a query and alarm be established for changes to **OSS bucket policies**.", - "Risk": "Monitoring changes to **OSS bucket policies** may reduce time to detect and correct **permissive policies** on sensitive OSS buckets.\n\nThis helps prevent unintended data exposure.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/oss-bucket-authority-changes-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/oss-bucket-authority-changes-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name oss-bucket-policy-changes --alert-displayname 'OSS Bucket Policy Changes Alert' --condition 'event.serviceName: Oss and (event.eventName: PutBucketPolicy or event.eventName: DeleteBucketPolicy)' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for OSS bucket policy changes: `event.serviceName: Oss and (event.eventName: PutBucketPolicy or event.eventName: DeleteBucketPolicy)`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { 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 2ca4e36872..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 @@ -2,28 +2,25 @@ "Provider": "alibabacloud", "CheckID": "sls_oss_permission_changes_alert_enabled", "CheckTitle": "Log monitoring and alerts are set up for OSS permission changes", - "CheckType": [ - "Sensitive file tampering", - "Cloud threat detection" - ], + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "It is recommended that a **metric filter and alarm** be established for **OSS Bucket RAM** changes.", - "Risk": "Monitoring changes to **OSS permissions** may reduce time to detect and correct permissions on sensitive OSS buckets and objects inside the bucket.\n\nThis helps prevent **unauthorized access** to stored data.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/oss-bucket-permission-changes-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/oss-bucket-permission-changes-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name oss-permission-changes --alert-displayname 'OSS Permission Changes Alert' --condition 'event.serviceName: Oss and (event.eventName: PutBucketAcl or event.eventName: PutObjectAcl)' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for OSS permission changes: `event.serviceName: Oss and (event.eventName: PutBucketAcl or event.eventName: PutObjectAcl)`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { 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 a30d12cd0b..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 @@ -2,28 +2,25 @@ "Provider": "alibabacloud", "CheckID": "sls_ram_role_changes_alert_enabled", "CheckTitle": "Log monitoring and alerts are set up for RAM Role changes", - "CheckType": [ - "Abnormal account", - "Cloud threat detection" - ], + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "It is recommended that a query and alarm be established for **RAM Role** creation, deletion, and updating activities.", - "Risk": "Monitoring **role creation**, **deletion**, and **updating** activities will help in identifying potential **malicious actions** at an early stage.\n\nUnauthorized role changes could lead to privilege escalation.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/ram-policy-changes-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/ram-policy-changes-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name ram-role-changes --alert-displayname 'RAM Role Changes Alert' --condition 'event.serviceName: Ram and (event.eventName: CreateRole or event.eventName: DeleteRole or event.eventName: UpdateRole or event.eventName: AttachPolicyToRole or event.eventName: DetachPolicyFromRole)' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for RAM role changes: `event.serviceName: Ram and (event.eventName: CreateRole or event.eventName: DeleteRole or event.eventName: UpdateRole or event.eventName: AttachPolicyToRole or event.eventName: DetachPolicyFromRole)`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { 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 d196b1802d..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 @@ -2,28 +2,25 @@ "Provider": "alibabacloud", "CheckID": "sls_rds_instance_configuration_changes_alert_enabled", "CheckTitle": "Log monitoring and alerts are set up for RDS instance configuration changes", - "CheckType": [ - "Intrusion into applications", - "Cloud threat detection" - ], + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "It is recommended that a **metric filter and alarm** be established for **RDS Instance** configuration changes.", - "Risk": "Monitoring changes to **RDS Instance configuration** may reduce time to detect and correct **misconfigurations** done on database servers.\n\nThis helps prevent security gaps in database deployments.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/rds-instance-config-changes-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/rds-instance-config-changes-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name rds-config-changes --alert-displayname 'RDS Instance Configuration Changes Alert' --condition 'event.serviceName: Rds and (event.eventName: ModifyDBInstanceSpec or event.eventName: ModifySecurityIps or event.eventName: ModifyDBInstanceNetworkType)' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for RDS configuration changes: `event.serviceName: Rds and (event.eventName: ModifyDBInstanceSpec or event.eventName: ModifySecurityIps or event.eventName: ModifyDBInstanceNetworkType)`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { 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 e6d271b482..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 @@ -1,29 +1,26 @@ { "Provider": "alibabacloud", "CheckID": "sls_root_account_usage_alert_enabled", - "CheckTitle": "A log monitoring and alerts are set up for usage of root account", - "CheckType": [ - "Unusual logon", - "Cloud threat detection" - ], + "CheckTitle": "A log monitoring and alert is set up for usage of root account", + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "Real-time monitoring of API calls can be achieved by directing **ActionTrail Logs** to Log Service and establishing corresponding query and alarms.\n\nIt is recommended that a query and alarm be established for **root account login** attempts.", - "Risk": "Monitoring for **root account logins** will provide visibility into the use of a fully privileged account and an opportunity to reduce its use.\n\nRoot account usage should be minimized and closely monitored.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/root-account-login-frequent-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/root-account-login-frequent-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name root-account-usage --alert-displayname 'Root Account Usage Alert' --condition 'event.userIdentity.type: root-account' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for root account usage: `event.userIdentity.type: root-account`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { 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 eca2fd9b53..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 @@ -1,29 +1,26 @@ { "Provider": "alibabacloud", "CheckID": "sls_security_group_changes_alert_enabled", - "CheckTitle": "A log monitoring and alerts are set up for security group changes", - "CheckType": [ - "Suspicious network connection", - "Cloud threat detection" - ], + "CheckTitle": "A log monitoring and alert is set up for security group changes", + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "Real-time monitoring of API calls can be achieved by directing **ActionTrail Logs** to Log Service and establishing corresponding query and alarms.\n\n**Security Groups** are a stateful packet filter that controls ingress and egress traffic within a VPC. It is recommended that a query and alarm be established for changes to Security Groups.", - "Risk": "Monitoring changes to **security groups** will help ensure that resources and services are not unintentionally exposed.\n\nUnauthorized security group modifications could lead to **network exposure** and **unauthorized access**.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/security-group-config-changes-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/security-group-config-changes-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name security-group-changes --alert-displayname 'Security Group Changes Alert' --condition 'event.serviceName: Ecs and (event.eventName: AuthorizeSecurityGroup or event.eventName: RevokeSecurityGroup or event.eventName: CreateSecurityGroup or event.eventName: DeleteSecurityGroup)' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for security group changes: `event.serviceName: Ecs and (event.eventName: AuthorizeSecurityGroup or event.eventName: RevokeSecurityGroup or event.eventName: CreateSecurityGroup or event.eventName: DeleteSecurityGroup)`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { 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 5115561aba..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 @@ -1,29 +1,26 @@ { "Provider": "alibabacloud", "CheckID": "sls_unauthorized_api_calls_alert_enabled", - "CheckTitle": "A log monitoring and alerts are set up for unauthorized API calls", - "CheckType": [ - "Unusual logon", - "Cloud threat detection" - ], + "CheckTitle": "A log monitoring and alert is set up for unauthorized API calls", + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "Real-time monitoring of API calls can be achieved by directing **ActionTrail Logs** to Log Service and establishing corresponding query and alarms.\n\nIt is recommended that a query and alarm be established for **unauthorized API calls**.", - "Risk": "Monitoring **unauthorized API calls** will help reveal application errors and may reduce time to detect **malicious activity**.\n\nThis is essential for early detection of potential security breaches.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/unauthorized-api-calls-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/unauthorized-api-calls-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name unauthorized-api-calls --alert-displayname 'Unauthorized API Calls Alert' --condition 'event.errorCode: Forbidden or event.errorCode: AccessDenied' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for unauthorized API calls: `event.errorCode: Forbidden or event.errorCode: AccessDenied`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { 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 e922cd50c9..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 @@ -2,28 +2,25 @@ "Provider": "alibabacloud", "CheckID": "sls_vpc_changes_alert_enabled", "CheckTitle": "Log monitoring and alerts are set up for VPC changes", - "CheckType": [ - "Suspicious network connection", - "Cloud threat detection" - ], + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "It is recommended that a **log search/analysis query and alarm** be established for **VPC changes**.", - "Risk": "Monitoring changes to **VPC** will help ensure VPC traffic flow is not getting impacted.\n\nUnauthorized VPC modifications could disrupt network connectivity or create security vulnerabilities.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/vpc-config-changes-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/vpc-config-changes-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name vpc-changes --alert-displayname 'VPC Changes Alert' --condition 'event.serviceName: Vpc and (event.eventName: CreateVpc or event.eventName: DeleteVpc or event.eventName: ModifyVpcAttribute)' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for VPC changes: `event.serviceName: Vpc and (event.eventName: CreateVpc or event.eventName: DeleteVpc or event.eventName: ModifyVpcAttribute)`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { 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 fccd21af53..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 @@ -1,29 +1,26 @@ { "Provider": "alibabacloud", "CheckID": "sls_vpc_network_route_changes_alert_enabled", - "CheckTitle": "Log monitoring and alerts are set up for VPC network route changes", - "CheckType": [ - "Suspicious network connection", - "Cloud threat detection" - ], + "CheckTitle": "A log monitoring and alert is set up for VPC network route changes", + "CheckType": [], "ServiceName": "sls", "SubServiceName": "", - "ResourceIdTemplate": "acs:log:region:account-id:project/project-name/alert/alert-name", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudSLSAlert", + "ResourceType": "ALIYUN::SLS::Alert", "ResourceGroup": "monitoring", - "Description": "It is recommended that a **metric filter and alarm** be established for **VPC network route** changes.", - "Risk": "Monitoring changes to **route tables** will help ensure that all VPC traffic flows through an expected path.\n\nUnauthorized route changes could redirect traffic through malicious intermediaries.", + "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", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/vpc-network-route-changes-alert.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-SLS/vpc-network-route-changes-alert.html" ], "Remediation": { "Code": { - "CLI": "", + "CLI": "aliyun sls create-alert --project --alert-name vpc-route-changes --alert-displayname 'VPC Network Route Changes Alert' --condition 'event.serviceName: Vpc and (event.eventName: CreateRouteEntry or event.eventName: DeleteRouteEntry or event.eventName: ModifyRouteEntry)' --dashboard --schedule '{\"type\":\"FixedRate\",\"interval\":\"1m\"}'", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **SLS Console**.\n2. Ensure **ActionTrail** is enabled and delivering logs to a **Log Service** project.\n3. Navigate to the project receiving ActionTrail logs.\n4. Select **Alerts** and click **Create Alert Rule**.\n5. Configure the query to filter for VPC network route changes: `event.serviceName: Vpc and (event.eventName: CreateRouteEntry or event.eventName: DeleteRouteEntry or event.eventName: ModifyRouteEntry)`.\n6. Set the alert **schedule**, **notification method**, and **severity**.\n7. Save and enable the alert rule.", "Terraform": "" }, "Recommendation": { diff --git a/prowler/providers/alibabacloud/services/vpc/vpc_flow_logs_enabled/vpc_flow_logs_enabled.metadata.json b/prowler/providers/alibabacloud/services/vpc/vpc_flow_logs_enabled/vpc_flow_logs_enabled.metadata.json index c150689c06..414bc8b80a 100644 --- a/prowler/providers/alibabacloud/services/vpc/vpc_flow_logs_enabled/vpc_flow_logs_enabled.metadata.json +++ b/prowler/providers/alibabacloud/services/vpc/vpc_flow_logs_enabled/vpc_flow_logs_enabled.metadata.json @@ -1,33 +1,30 @@ { "Provider": "alibabacloud", "CheckID": "vpc_flow_logs_enabled", - "CheckTitle": "VPC flow logging is enabled in all VPCs", - "CheckType": [ - "Suspicious network connection", - "Cloud threat detection" - ], + "CheckTitle": "VPC flow logging is enabled for all VPCs", + "CheckType": [], "ServiceName": "vpc", "SubServiceName": "", - "ResourceIdTemplate": "acs:vpc:region:account-id:vpc/{vpc-id}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AlibabaCloudVPC", + "ResourceType": "ALIYUN::VPC::FlowLog", "ResourceGroup": "network", - "Description": "You can use the **flow log function** to monitor the IP traffic information for an ENI, a VSwitch, or a VPC.\n\nIf you create a flow log for a VSwitch or a VPC, all the **Elastic Network Interfaces**, including the newly created ones, are monitored. Such flow log data is stored in **Log Service**, where you can view and analyze IP traffic information. It is recommended that VPC Flow Logs be enabled for packet \"Rejects\" for VPCs.", - "Risk": "**VPC Flow Logs** provide visibility into network traffic that traverses the VPC and can be used to detect **anomalous traffic** or provide insight during security workflows.\n\nWithout flow logs, it is difficult to investigate network-based security incidents.", + "Description": "**Alibaba Cloud VPC Flow Logs** capture IP traffic information for Elastic Network Interfaces, VSwitches, or entire VPCs. When a flow log is created for a VPC, all ENIs within it, including newly created ones, are automatically monitored. Flow log data is stored in **Log Service** where it can be viewed and analyzed for security and operational purposes.", + "Risk": "Without **VPC Flow Logs** enabled, there is no visibility into network traffic traversing the VPC. This prevents detection of **anomalous traffic patterns**, **unauthorized network connections**, and **data exfiltration attempts**, and severely limits the ability to investigate network-based security incidents.", "RelatedUrl": "", "AdditionalURLs": [ "https://www.alibabacloud.com/help/doc-detail/90628.html", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/alibaba-cloud/AlibabaCloud-VPC/enable-flow-logs.html" + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/alibaba-cloud/AlibabaCloud-VPC/enable-flow-logs.html" ], "Remediation": { "Code": { "CLI": "aliyun vpc CreateFlowLog --ResourceId --ResourceType VPC --FlowLogName --LogStoreName --ProjectName ", "NativeIaC": "", - "Other": "", + "Other": "1. Log on to the **VPC Console**\n2. In the left-side navigation pane, click **FlowLog**\n3. Click **Create Flow Log**\n4. Select the target VPC as the resource\n5. Configure the Log Service project and logstore for storing flow log data\n6. Click **OK** to enable flow logging", "Terraform": "resource \"alicloud_vpc_flow_log\" \"example\" {\n flow_log_name = \"example-flow-log\"\n resource_type = \"VPC\"\n resource_id = alicloud_vpc.example.id\n traffic_type = \"All\"\n project_name = alicloud_log_project.example.project_name\n log_store_name = alicloud_log_store.example.logstore_name\n}" }, "Recommendation": { - "Text": "1. Log on to the **VPC Console**\n2. In the left-side navigation pane, click **FlowLog**\n3. Follow the instructions to create FlowLog for each of your VPCs", + "Text": "Enable VPC Flow Logs for all VPCs to capture IP traffic information and store it in Log Service for security analysis, anomaly detection, and incident response.", "Url": "https://hub.prowler.com/check/vpc_flow_logs_enabled" } }, 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 6da19a3253..36e6839c8c 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -8398,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" @@ -8634,6 +8636,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -8644,6 +8647,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -9497,6 +9501,7 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", + "ap-southeast-5", "ca-central-1", "eu-central-1", "eu-central-2", @@ -10476,7 +10481,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" @@ -12554,6 +12561,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/__init__.py b/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..6ebfa056ad --- /dev/null +++ b/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions.metadata.json @@ -0,0 +1,44 @@ +{ + "Provider": "aws", + "CheckID": "guardduty_delegated_admin_enabled_all_regions", + "CheckTitle": "GuardDuty has delegated admin configured and is enabled in all regions with organization auto-enable", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], + "ServiceName": "guardduty", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "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 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", + "https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_multi-account.html", + "https://docs.aws.amazon.com/organizations/latest/userguide/services-that-can-integrate-guardduty.html" + ], + "Remediation": { + "Code": { + "CLI": "aws guardduty enable-organization-admin-account --admin-account-id && aws guardduty update-organization-configuration --detector-id --auto-enable-organization-members ALL", + "NativeIaC": "", + "Other": "1. Sign in to the AWS Organizations management account\n2. Open the AWS Organizations console\n3. Navigate to Services > Amazon GuardDuty\n4. Click Register delegated administrator and enter the security account ID\n5. Switch to the delegated admin account\n6. In GuardDuty console, go to Settings > Accounts\n7. Enable auto-enable for all organization members\n8. Repeat detector enablement for all opted-in regions", + "Terraform": "" + }, + "Recommendation": { + "Text": "Configure a **delegated administrator** for GuardDuty via AWS Organizations. Enable GuardDuty detectors in **all opted-in regions** and configure **auto-enable** to automatically enroll new member accounts. This ensures consistent threat detection coverage across the entire organization.", + "Url": "https://hub.prowler.com/check/guardduty_delegated_admin_enabled_all_regions" + } + }, + "Categories": [ + "forensics-ready" + ], + "DependsOn": [], + "RelatedTo": [ + "guardduty_is_enabled", + "guardduty_centrally_managed" + ], + "Notes": "This check requires execution from the organization management account or delegated administrator account to access organization-level APIs." +} diff --git a/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions.py b/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions.py new file mode 100644 index 0000000000..23065abd2b --- /dev/null +++ b/prowler/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions.py @@ -0,0 +1,76 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.guardduty.guardduty_client import guardduty_client + + +class guardduty_delegated_admin_enabled_all_regions(Check): + """Ensure GuardDuty has a delegated admin and is enabled in all regions. + + This check verifies that: + 1. A delegated administrator account is configured for GuardDuty + 2. GuardDuty detectors are enabled in each region + 3. Organization auto-enable is configured for new member accounts + """ + + def execute(self) -> list[Check_Report_AWS]: + """Execute the check logic. + + Returns: + A list of reports containing the result of the check for each region. + """ + findings = [] + + # Build a set of regions that have an organization admin account configured + regions_with_admin = { + admin.region + for admin in guardduty_client.organization_admin_accounts + if admin.admin_status == "ENABLED" + } + + for detector in guardduty_client.detectors: + report = Check_Report_AWS(metadata=self.metadata(), resource=detector) + + # Check if this region has a delegated admin + has_delegated_admin = detector.region in regions_with_admin + + # Check if detector is enabled + detector_enabled = detector.enabled_in_account and detector.status + + # Check if auto-enable is configured for organization members + auto_enable_configured = detector.organization_auto_enable_members in ( + "NEW", + "ALL", + ) + + # Determine overall status + issues = [] + if not has_delegated_admin: + issues.append("no delegated administrator configured") + if not detector_enabled: + issues.append("detector not enabled") + if not auto_enable_configured and detector.organization_config_available: + # Only report auto-enable issue if org config data is available + issues.append("organization auto-enable not configured") + + if issues: + report.status = "FAIL" + report.status_extended = ( + f"GuardDuty in region {detector.region} has issues: " + f"{', '.join(issues)}." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"GuardDuty in region {detector.region} has delegated admin " + f"configured with detector enabled and organization auto-enable active." + ) + + # Support muting non-default regions if configured + if report.status == "FAIL" and ( + guardduty_client.audit_config.get("mute_non_default_regions", False) + and detector.region != guardduty_client.region + ): + report.muted = True + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/guardduty/guardduty_service.py b/prowler/providers/aws/services/guardduty/guardduty_service.py index c267771209..bde8725454 100644 --- a/prowler/providers/aws/services/guardduty/guardduty_service.py +++ b/prowler/providers/aws/services/guardduty/guardduty_service.py @@ -1,5 +1,6 @@ from typing import Optional +from botocore.exceptions import ClientError from pydantic.v1 import BaseModel from prowler.lib.logger import logger @@ -12,12 +13,17 @@ class GuardDuty(AWSService): # Call AWSService's __init__ super().__init__(__class__.__name__, provider) self.detectors = [] + self.organization_admin_accounts = [] self.__threading_call__(self._list_detectors) self.__threading_call__(self._get_detector, self.detectors) self._list_findings() self._list_members() self._get_administrator_account() self._list_tags_for_resource() + self.__threading_call__(self._list_organization_admin_accounts) + self.__threading_call__( + self._describe_organization_configuration, self.detectors + ) def _list_detectors(self, regional_client): logger.info("GuardDuty - listing detectors...") @@ -216,13 +222,97 @@ class GuardDuty(AWSService): f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" ) + def _list_organization_admin_accounts(self, regional_client): + """List GuardDuty delegated administrator accounts for the organization. + + This API is only available to the organization management account or + a delegated administrator account. + """ + logger.info("GuardDuty - listing organization admin accounts...") + try: + paginator = regional_client.get_paginator( + "list_organization_admin_accounts" + ) + for page in paginator.paginate(): + for admin in page.get("AdminAccounts", []): + admin_account = OrganizationAdminAccount( + admin_account_id=admin.get("AdminAccountId"), + admin_status=admin.get("AdminStatus"), + region=regional_client.region, + ) + # Avoid duplicates across regions for the same admin account + if not any( + existing.admin_account_id == admin_account.admin_account_id + and existing.region == admin_account.region + for existing in self.organization_admin_accounts + ): + self.organization_admin_accounts.append(admin_account) + except ClientError as error: + if error.response["Error"]["Code"] in ( + "AccessDeniedException", + "BadRequestException", + ): + logger.warning( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + else: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def _describe_organization_configuration(self, detector): + """Describe the organization configuration for a GuardDuty detector. + + This provides information about auto-enable settings for the organization. + """ + logger.info("GuardDuty - describing organization configuration...") + try: + if detector.id and detector.enabled_in_account: + regional_client = self.regional_clients[detector.region] + org_config = regional_client.describe_organization_configuration( + DetectorId=detector.id + ) + detector.organization_auto_enable_members = org_config.get( + "AutoEnableOrganizationMembers", "NONE" + ) + detector.organization_config_available = True + except ClientError as error: + if error.response["Error"]["Code"] in ( + "AccessDeniedException", + "BadRequestException", + ): + # Expected when not running from management or delegated admin account + logger.warning( + f"{detector.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + else: + logger.error( + f"{detector.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + except Exception as error: + logger.error( + f"{detector.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + +class OrganizationAdminAccount(BaseModel): + """Represents a GuardDuty delegated administrator account.""" + + admin_account_id: str + admin_status: str # ENABLED or DISABLE_IN_PROGRESS + region: str + class Detector(BaseModel): id: str arn: str region: str enabled_in_account: bool - status: bool = None + status: Optional[bool] = None findings: list = [] member_accounts: list = [] administrator_account: str = None @@ -233,3 +323,6 @@ class Detector(BaseModel): eks_runtime_monitoring: bool = False lambda_protection: bool = False ec2_malware_protection: bool = False + # Organization configuration fields + organization_auto_enable_members: str = "NONE" # NEW, ALL, or NONE + organization_config_available: bool = False 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/iam/lib/policy.py b/prowler/providers/aws/services/iam/lib/policy.py index 8442fefa66..d8806f280b 100644 --- a/prowler/providers/aws/services/iam/lib/policy.py +++ b/prowler/providers/aws/services/iam/lib/policy.py @@ -380,6 +380,56 @@ def is_condition_restricting_from_private_ip(condition_statement: dict) -> bool: return is_from_private_ip +def is_condition_restricting_to_trusted_ips( + condition_statement: dict, trusted_ips: list = None +) -> bool: + """Check if the policy condition restricts access to trusted IP addresses. + + Keyword arguments: + condition_statement -- The policy condition to check. For example: + { + "IpAddress": { + "aws:SourceIp": "X.X.X.X" + } + } + trusted_ips -- A list of trusted IP addresses or CIDR ranges. + """ + if not trusted_ips: + return False + + try: + CONDITION_OPERATOR = "IpAddress" + CONDITION_KEY = "aws:sourceip" + + if condition_statement.get(CONDITION_OPERATOR, {}): + condition_statement[CONDITION_OPERATOR] = { + k.lower(): v for k, v in condition_statement[CONDITION_OPERATOR].items() + } + + if condition_statement[CONDITION_OPERATOR].get(CONDITION_KEY, ""): + if not isinstance( + condition_statement[CONDITION_OPERATOR][CONDITION_KEY], list + ): + condition_statement[CONDITION_OPERATOR][CONDITION_KEY] = [ + condition_statement[CONDITION_OPERATOR][CONDITION_KEY] + ] + + trusted_ips_set = {ip.lower() for ip in trusted_ips} + for ip in condition_statement[CONDITION_OPERATOR][CONDITION_KEY]: + if ip == "*" or ip == "0.0.0.0/0": + return False + if ip not in trusted_ips_set: + return False + return True + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return False + + # TODO: Add logic for deny statements def is_policy_public( policy: dict, @@ -388,6 +438,7 @@ def is_policy_public( not_allowed_actions: list = [], check_cross_service_confused_deputy=False, trusted_account_ids: list = None, + trusted_ips: list = None, ) -> bool: """ Check if the policy allows public access to the resource. @@ -399,6 +450,7 @@ def is_policy_public( not_allowed_actions (list): List of actions that are not allowed, default: []. If not_allowed_actions is empty, the function will not consider the actions in the policy. check_cross_service_confused_deputy (bool): If the policy is checked for cross-service confused deputy, default: False trusted_account_ids (list): A list of trusted accound ids to reduce false positives on cross-account checks + trusted_ips (list): A list of trusted IP addresses or CIDR ranges to reduce false positives on IP-based checks Returns: bool: True if the policy allows public access, False otherwise """ @@ -511,6 +563,10 @@ def is_policy_public( and not is_condition_restricting_from_private_ip( statement.get("Condition", {}) ) + and not is_condition_restricting_to_trusted_ips( + statement.get("Condition", {}), + trusted_ips, + ) ) if is_public: break 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/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.py b/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.py index 19a890620b..d02ed12a5a 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.py +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.py @@ -8,6 +8,7 @@ from prowler.providers.aws.services.opensearch.opensearch_client import ( class opensearch_service_domains_not_publicly_accessible(Check): def execute(self): findings = [] + trusted_ips = opensearch_client.audit_config.get("trusted_ips", []) for domain in opensearch_client.opensearch_domains.values(): report = Check_Report_AWS(metadata=self.metadata(), resource=domain) report.status = "PASS" @@ -18,7 +19,9 @@ class opensearch_service_domains_not_publicly_accessible(Check): if domain.vpc_id: report.status_extended = f"Opensearch domain {domain.name} is in a VPC, then it is not publicly accessible." elif domain.access_policy is not None and is_policy_public( - domain.access_policy, opensearch_client.audited_account + domain.access_policy, + opensearch_client.audited_account, + trusted_ips=trusted_ips, ): report.status = "FAIL" report.status_extended = f"Opensearch domain {domain.name} is publicly accessible via access policy." 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/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated.py b/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated.py index 03c94bcfed..5071da4b20 100644 --- a/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated.py +++ b/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated.py @@ -21,9 +21,9 @@ class mysql_flexible_server_audit_log_connection_activated(Check): "audit_log_events" ].resource_id - if "CONNECTION" in server.configurations[ + if "connection" in server.configurations[ "audit_log_events" - ].value.split(","): + ].value.lower().split(","): report.status = "PASS" report.status_extended = f"Audit log is enabled for server {server.name} in subscription {subscription_name}." diff --git a/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled.py b/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled.py index c8ae94fb31..81918f7756 100644 --- a/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled.py +++ b/prowler/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled.py @@ -21,7 +21,7 @@ class mysql_flexible_server_audit_log_enabled(Check): "audit_log_enabled" ].resource_id - if server.configurations["audit_log_enabled"].value == "ON": + if server.configurations["audit_log_enabled"].value.lower() == "on": report.status = "PASS" report.status_extended = f"Audit log is enabled for server {server.name} in subscription {subscription_name}." diff --git a/prowler/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled.py b/prowler/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled.py index a18a1aba5e..79930de947 100644 --- a/prowler/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled.py +++ b/prowler/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled.py @@ -20,7 +20,10 @@ class mysql_flexible_server_ssl_connection_enabled(Check): report.resource_id = server.configurations[ "require_secure_transport" ].resource_id - if server.configurations["require_secure_transport"].value == "ON": + if ( + server.configurations["require_secure_transport"].value.lower() + == "on" + ): report.status = "PASS" report.status_extended = f"SSL connection is enabled for server {server.name} in subscription {subscription_name}." diff --git a/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.py b/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.py index e9da4662ed..d5865937f9 100644 --- a/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.py +++ b/prowler/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled.py @@ -31,7 +31,8 @@ class vm_backup_enabled(Check): for backup_item in vault.backup_protected_items.values(): if ( backup_item.workload_type == DataSourceType.VM - and backup_item.name.split(";")[-1] == vm.resource_name + and backup_item.name.split(";")[-1].lower() + == vm.resource_name.lower() ): found = True found_vault_name = vault.name diff --git a/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.py b/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.py index 221df85351..444cfcfa3b 100644 --- a/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.py +++ b/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.py @@ -27,7 +27,8 @@ class vm_sufficient_daily_backup_retention_period(Check): for backup_item in vault.backup_protected_items.values(): if ( backup_item.workload_type == DataSourceType.VM - and backup_item.name.split(";")[-1] == vm.resource_name + and backup_item.name.split(";")[-1].lower() + == vm.resource_name.lower() ): backup_found = True policy_id = backup_item.backup_policy_id 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/googleworkspace/exceptions/exceptions.py b/prowler/providers/googleworkspace/exceptions/exceptions.py index f38f6481c2..9e4cd2fea7 100644 --- a/prowler/providers/googleworkspace/exceptions/exceptions.py +++ b/prowler/providers/googleworkspace/exceptions/exceptions.py @@ -38,6 +38,10 @@ class GoogleWorkspaceBaseException(ProwlerException): "message": "Service Account does not have required OAuth scopes", "remediation": "Ensure the Service Account has the required scopes configured in Domain-Wide Delegation settings.", }, + (12008, "GoogleWorkspaceInvalidProviderIdError"): { + "message": "The provided provider_id does not match the credentials customer ID", + "remediation": "Check the provider_id (Customer ID) and ensure it matches the Google Workspace organization for the given credentials.", + }, } def __init__(self, code, file=None, original_exception=None, message=None): @@ -117,3 +121,10 @@ class GoogleWorkspaceInsufficientScopesError(GoogleWorkspaceCredentialsError): super().__init__( 12007, file=file, original_exception=original_exception, message=message ) + + +class GoogleWorkspaceInvalidProviderIdError(GoogleWorkspaceBaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 12008, file=file, original_exception=original_exception, message=message + ) diff --git a/prowler/providers/googleworkspace/googleworkspace_provider.py b/prowler/providers/googleworkspace/googleworkspace_provider.py index 61f85f2f24..83beee0d3e 100644 --- a/prowler/providers/googleworkspace/googleworkspace_provider.py +++ b/prowler/providers/googleworkspace/googleworkspace_provider.py @@ -21,6 +21,7 @@ from prowler.providers.googleworkspace.exceptions.exceptions import ( GoogleWorkspaceImpersonationError, GoogleWorkspaceInsufficientScopesError, GoogleWorkspaceInvalidCredentialsError, + GoogleWorkspaceInvalidProviderIdError, GoogleWorkspaceMissingDelegatedUserError, GoogleWorkspaceNoCredentialsError, GoogleWorkspaceSetUpIdentityError, @@ -479,6 +480,7 @@ class GoogleworkspaceProvider(Provider): credentials_content: str = None, delegated_user: str = None, raise_on_exception: bool = True, + provider_id: str = None, ) -> Connection: """Test connection to Google Workspace. @@ -489,6 +491,7 @@ class GoogleworkspaceProvider(Provider): credentials_content (str): Service Account JSON credentials as a string. delegated_user (str): Email of the user to impersonate via Domain-Wide Delegation. raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails. + provider_id (str): The provider ID (Customer ID). Optional, not used in connection test. Returns: Connection: Connection object with success status or error information. @@ -515,7 +518,16 @@ class GoogleworkspaceProvider(Provider): ) # Set up the identity to test the connection - GoogleworkspaceProvider.setup_identity(session, resolved_delegated_user) + identity = GoogleworkspaceProvider.setup_identity( + session, resolved_delegated_user + ) + + # Validate provider_id matches the customer_id from credentials + if provider_id and provider_id != identity.customer_id: + raise GoogleWorkspaceInvalidProviderIdError( + file=os.path.basename(__file__), + message=f"The provider ID {provider_id} does not match the credentials customer ID {identity.customer_id}", + ) return Connection(is_connected=True) except Exception as error: 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/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled.metadata.json b/prowler/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled.metadata.json index 1756b1acd2..c5ff3c2125 100644 --- a/prowler/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled.metadata.json +++ b/prowler/providers/m365/services/admincenter/admincenter_external_calendar_sharing_disabled/admincenter_external_calendar_sharing_disabled.metadata.json @@ -1,30 +1,37 @@ { "Provider": "m365", "CheckID": "admincenter_external_calendar_sharing_disabled", - "CheckTitle": "Ensure external sharing of calendars is disabled", + "CheckTitle": "External calendar sharing is disabled at the organization level", "CheckType": [], "ServiceName": "admincenter", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Sharing Policy", + "ResourceType": "NotDefined", "ResourceGroup": "governance", - "Description": "Restrict the ability for users to share their calendars externally in Microsoft 365. This prevents users from sending calendar sharing links to external recipients, reducing information exposure.", - "Risk": "Allowing calendar sharing outside the organization can help attackers build knowledge of personnel availability, relationships, and activity patterns, aiding social engineering or targeted attacks.", - "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/admin/manage/share-calendars-with-external-users?view=o365-worldwide", + "Description": "**Microsoft 365 calendar sharing** is evaluated at the organization level to determine if sharing with external recipients is disabled, including blocking anonymous access and links sent outside the tenant.", + "Risk": "Allowing **external calendar sharing** exposes meeting metadata (subjects, locations, attendees, patterns), weakening confidentiality. Adversaries can profile staff, craft convincing spear-phish or fake invites, time fraud attempts, and stage meeting hijacks, increasing **BEC** and social engineering success.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/microsoft-365/admin/manage/share-calendars-with-external-users?view=o365-worldwide", + "https://m365scripts.com/microsoft365/how-to-stop-users-from-sharing-their-own-calendars/", + "https://learn.microsoft.com/en-my/answers/questions/1165808/external-calendar-sharing-for-single-user-in-exo" + ], "Remediation": { "Code": { - "CLI": "Set-SharingPolicy -Identity \"Default Sharing Policy\" -Enabled $False", + "CLI": "Set-SharingPolicy -Identity \"Default Sharing Policy\" -Enabled $false", "NativeIaC": "", - "Other": "1. Navigate to https://admin.microsoft.com. 2. Click Settings > Org settings. 3. Select Calendar in the Services section. 4. Uncheck 'Let your users share their calendars with people outside of your organization who have Office 365 or Exchange'. 5. Click Save.", + "Other": "1. Go to https://admin.microsoft.com and sign in\n2. Navigate to Settings > Org settings > Services > Calendar\n3. Under External sharing, uncheck \"Let your users share their calendars with people outside of your organization who have Microsoft 365 or Exchange\"\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Disable external calendar sharing by setting the Default Sharing Policy to disabled.", - "Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/manage/share-calendars-with-external-users?view=o365-worldwide" + "Text": "Apply **least privilege**: disable **external calendar sharing** tenant-wide. If business-needed, allow only approved domains, require authenticated recipients (no anonymous), and limit details to `Free/Busy (time only)`. Review sharing policies regularly under **zero trust** and **defense in depth**.", + "Url": "https://hub.prowler.com/check/admincenter_external_calendar_sharing_disabled" } }, "Categories": [ + "internet-exposed", + "trust-boundaries", "e5" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json b/prowler/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json index c46e21a037..821ddb4273 100644 --- a/prowler/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json +++ b/prowler/providers/m365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "admincenter_groups_not_public_visibility", - "CheckTitle": "Ensure that only organizationally managed/approved public groups exist", + "CheckTitle": "Microsoft 365 group has Private visibility", "CheckType": [], "ServiceName": "admincenter", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Active teams & groups", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Ensure that only organizationally managed and approved public groups exist to prevent unauthorized access to sensitive group resources like SharePoint, Teams, or other shared assets.", - "Risk": "Unmanaged public groups can allow unauthorized access to organizational resources, posing a risk of data leakage or misuse through easily guessable SharePoint URLs or self-adding to groups via the Azure portal.", - "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/admin/create-groups/manage-groups?view=o365-worldwide", + "Description": "**Microsoft 365 groups** are assessed for their visibility setting.\n\nThe finding highlights groups configured as `Public`; groups set to `Private` align with the intended privacy posture.", + "Risk": "**Public visibility** lets any authenticated tenant user discover and self-join, accessing group files, conversations, SharePoint sites, and calendars.\n\nThis weakens **confidentiality** and can enable unauthorized changes that affect **integrity**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/microsoft-365/admin/create-groups/manage-groups?view=o365-worldwide", + "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/microsoft-365-groups-governance" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "Set-UnifiedGroup -Identity -AccessType Private", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Teams & groups select Active teams & groups. 3. On the Active teams and groups page, select the group's name that is public. 4. On the popup groups name page, select Settings. 5. Under Privacy, select Private.", - "Terraform": "" + "Other": "1. Sign in to https://admin.microsoft.com\n2. Go to Teams & groups > Active teams & groups\n3. Select the group with visibility Public\n4. Open Settings\n5. Under Privacy, select Private\n6. Click Save", + "Terraform": "```hcl\nresource \"azuread_group\" \"\" {\n display_name = \"\"\n mail_enabled = true\n security_enabled = false\n types = [\"Unified\"]\n visibility = \"Private\" # Critical: sets the group visibility to Private to pass the check\n}\n```" }, "Recommendation": { - "Text": "Review and adjust the privacy settings of Microsoft 365 Groups to ensure only organizationally managed and approved public groups exist.", - "Url": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/microsoft-365-groups-governance" + "Text": "Set groups to `Private` by default and allow `Public` only for clearly non-sensitive communities.\n\nApply **least privilege**: restrict who can create or change visibility, require owner approval for membership, review access regularly, and use **sensitivity labels** to reinforce classification.", + "Url": "https://hub.prowler.com/check/admincenter_groups_not_public_visibility" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled.metadata.json b/prowler/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled.metadata.json index 78a9102daf..04370ddc66 100644 --- a/prowler/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled.metadata.json +++ b/prowler/providers/m365/services/admincenter/admincenter_organization_customer_lockbox_enabled/admincenter_organization_customer_lockbox_enabled.metadata.json @@ -1,30 +1,37 @@ { "Provider": "m365", "CheckID": "admincenter_organization_customer_lockbox_enabled", - "CheckTitle": "Ensure that customer lockbox is enabled for the organization", + "CheckTitle": "Customer Lockbox is enabled at the organization level", "CheckType": [], "ServiceName": "admincenter", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Exchange Organization Configuration", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "Customer Lockbox ensures that Microsoft support engineers cannot access content in your tenant to perform a service operation without explicit approval. This feature provides an additional layer of control and transparency over data access requests.", - "Risk": "If Customer Lockbox is not enabled, Microsoft support personnel can access your organization's data for troubleshooting without explicit approval, potentially increasing the risk of unauthorized access or data exfiltration.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/security/fundamentals/customer-lockbox-overview", + "Description": "**Microsoft 365 organization** setting indicates whether **Customer Lockbox** is enabled to require explicit approval for Microsoft support access to customer content", + "Risk": "Without **Customer Lockbox**, Microsoft engineers may access tenant content during support without your approval, undermining **confidentiality** and **accountability**. This increases risks of targeted data exposure, insider misuse, and weakens auditability of access during troubleshooting.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.gitbit.org/course/ms-500/learn/locking-down-your-microsoft-365-tenant-from-microsoft-engineers-fldnualgc", + "https://learn.microsoft.com/en-us/azure/security/fundamentals/customer-lockbox-overview", + "https://learn.microsoft.com/en-us/microsoft-365/compliance/customer-lockbox-requests?redirectSourcePath=%2flv-lv%2farticle%2fOffice-365-klientu-lockbox-piepras%25C4%25ABjumu-36f9cdd1-e64c-421b-a7e4-4a54d16440a2&view=o365-worldwide", + "https://learnthecontent.com/exam/microsoft-365/ms-700-managing-microsoft-teams/s/enable-customer-lockbox-for-data-security" + ], "Remediation": { "Code": { - "CLI": "Set-OrganizationConfig -CustomerLockBoxEnabled $true", + "CLI": "Set-OrganizationConfig -CustomerLockboxEnabled $true", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click Settings > Org settings. 3. Select the Security & privacy tab. 4. Click Customer lockbox. 5. Check the box 'Require approval for all data access requests'. 6. Click Save.", + "Other": "1. Go to https://admin.microsoft.com and sign in\n2. Navigate to Settings > Org settings\n3. Select Security & privacy\n4. Click Customer Lockbox\n5. Check \"Require approval for all data access requests\"\n6. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Enable the Customer Lockbox feature to ensure explicit approval is required before Microsoft engineers can access your data during support operations.", - "Url": "https://learn.microsoft.com/en-us/azure/security/fundamentals/customer-lockbox-overview" + "Text": "Enable **Customer Lockbox** to enforce tenant approval for data access (`Require approval for all data access requests`).\n- Limit approvers per **least privilege**\n- Establish an on-call review workflow\n- Audit requests and engineer actions\n- Layer with **defense in depth** (MFA, RBAC, logging) to strengthen control", + "Url": "https://hub.prowler.com/check/admincenter_organization_customer_lockbox_enabled" } }, "Categories": [ + "identity-access", "e5" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json b/prowler/providers/m365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json index a844c80b00..e79fdee041 100644 --- a/prowler/providers/m365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json +++ b/prowler/providers/m365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json @@ -1,30 +1,34 @@ { "Provider": "m365", "CheckID": "admincenter_settings_password_never_expire", - "CheckTitle": "Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)'", + "CheckTitle": "Tenant password policy is set to never expire", "CheckType": [], "ServiceName": "admincenter", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Security & privacy settings", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "This control ensures that the password expiration policy is set to 'Set passwords to never expire (recommended)'. This aligns with modern recommendations to enhance security by avoiding arbitrary password changes and focusing on supplementary controls like MFA.", - "Risk": "Arbitrary password expiration policies can lead to weaker passwords due to frequent changes. Users may adopt insecure habits such as using simple, memorable passwords.", - "RelatedUrl": "https://www.cisecurity.org/insights/white-papers/cis-password-policy-guide", + "Description": "**Microsoft 365 tenant password policy** is configured so user passwords **do not expire** (`never expire`), meaning no time-based rotation is enforced across accounts.", + "Risk": "Forced password expiration degrades security: users adopt predictable patterns and reuse credentials, reducing **confidentiality** and **integrity**. It boosts success of **password spraying** and **credential stuffing**, encourages insecure storage and helpdesk resets, and can disrupt service accounts, impacting **availability**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/microsoft-365/admin/misc/password-policy-recommendations?view=o365-worldwide" + ], "Remediation": { "Code": { - "CLI": "Set-MsolUser -UserPrincipalName -PasswordNeverExpires $true", + "CLI": "Set-MsolPasswordPolicy -DomainName -ValidityPeriod 2147483647", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings select Org Settings. 3. Click on Security & privacy. 4. Check the Set passwords to never expire (recommended) box. 5. Click Save.", + "Other": "1. Sign in to the Microsoft 365 admin center: https://admin.microsoft.com\n2. Go to Settings > Org settings > Security & privacy\n3. Open Password expiration policy and check Set passwords to never expire\n4. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Enable the 'Never Expire Passwords' option in Microsoft 365 Admin Center.", - "Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/misc/password-policy-recommendations?view=o365-worldwide" + "Text": "Set passwords to `never expire` and enforce layered controls: **MFA** with risk-based challenges, banned-password checks, and strong length. Apply **least privilege** and monitor sign-ins. Rotate credentials only after compromise or risk, and favor managed non-human identities for automation instead of time-based password cycling.", + "Url": "https://hub.prowler.com/check/admincenter_settings_password_never_expire" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json b/prowler/providers/m365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json index 44654f0fb2..7d1e782451 100644 --- a/prowler/providers/m365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json +++ b/prowler/providers/m365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "admincenter_users_admins_reduced_license_footprint", - "CheckTitle": "Ensure administrative accounts use licenses with a reduced application footprint", + "CheckTitle": "Administrative user has no license or an allowed license (AAD_PREMIUM or AAD_PREMIUM_P2)", "CheckType": [], "ServiceName": "admincenter", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "Active users", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Administrative accounts must use licenses with a reduced application footprint, such as Microsoft Entra ID P1 or P2, or avoid licenses entirely when possible. This minimizes the attack surface associated with privileged identities.", - "Risk": "Licensing administrative accounts with applications like email or collaborative tools increases their exposure to social engineering attacks and malicious content, putting privileged accounts at risk.", - "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/enterprise/protect-your-global-administrator-accounts?view=o365-worldwide", + "Description": "Privileged users in Microsoft 365 are evaluated for a **reduced license footprint**: only `AAD_PREMIUM` (P1) or `AAD_PREMIUM_P2` (P2), or no license. Assignments that include productivity app suites indicate excess application entitlements on administrative accounts.", + "Risk": "Productivity licenses on **privileged identities** create mailboxes and collaboration surfaces that widen attack paths. Phishing, malicious content, or rogue OAuth apps can steal credentials/tokens, enabling tenant-wide role misuse, data exfiltration, and configuration tampering-compromising **confidentiality** and **integrity**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/microsoft-365/enterprise/protect-your-global-administrator-accounts?view=o365-worldwide", + "https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/add-users?view=o365-worldwide" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "Set-MgUserLicense -UserId -AddLicenses @() -RemoveLicenses @(Get-MgUserLicenseDetail -UserId | Select-Object -ExpandProperty SkuId)", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Users select Active users. 3. Click Add a user. 4. Fill out the appropriate fields for Name, user, etc. 5. When prompted to assign licenses select as needed Microsoft Entra ID P1 or Microsoft Entra ID P2, then click Next. 6. Under the Option settings screen you may choose from several types of privileged roles. Choose Admin center access followed by the appropriate role then click Next. 7. Select Finish adding.", + "Other": "1. Sign in to https://admin.microsoft.com\n2. Go to Users > Active users\n3. Select the affected administrative user\n4. Open Licenses and apps\n5. To pass the check, do ONE of the following:\n - Unassign all licenses (make the user unlicensed); or\n - Assign only Microsoft Entra ID P1 or P2 and remove all other licenses\n6. Click Save changes", "Terraform": "" }, "Recommendation": { - "Text": "Assign Microsoft Entra ID P1 or P2 licenses to administrative accounts to participate in essential security services without enabling access to vulnerable applications.", - "Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/add-users?view=o365-worldwide" + "Text": "Maintain **dedicated admin accounts** with a **least-privilege, reduced-license** model: assign only Microsoft Entra P1/P2 features needed for identity security, or keep them unlicensed for apps. Separate admin from daily-use accounts, enforce **MFA** and just-in-time elevation, and use **privileged access workstations** for administration.", + "Url": "https://hub.prowler.com/check/admincenter_users_admins_reduced_license_footprint" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json b/prowler/providers/m365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json index b429d3a66b..617ae2a35c 100644 --- a/prowler/providers/m365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json +++ b/prowler/providers/m365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "admincenter_users_between_two_and_four_global_admins", - "CheckTitle": "Ensure that between two and four global admins are designated", + "CheckTitle": "Tenant has between two and four Global Administrators", "CheckType": [], "ServiceName": "admincenter", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Active users", + "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Ensure that there are between two and four global administrators designated in your tenant. This ensures monitoring, redundancy, and reduces the risk associated with having too many privileged accounts.", - "Risk": "Having only one global administrator increases the risk of unmonitored actions and operational disruptions if that administrator is unavailable. Having more than four increases the likelihood of a breach through one of these highly privileged accounts.", - "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#5-limit-the-number-of-global-administrators-to-less-than-5", + "Description": "Count of users assigned the **Global Administrator** role in the **Microsoft 365 tenant** is between `2` and `4` (inclusive).", + "Risk": "Having 1 **Global Administrator** jeopardizes availability if that account is unavailable or locked out. Having 5 expands the attack surface; one compromised admin can enable tenant-wide changes, data exfiltration, and privilege escalation, impacting confidentiality and integrity.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://admin.microsoft.com", + "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#5-limit-the-number-of-global-administrators-to-less-than-5", + "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/manage-roles-portal" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "1. Navigate to the Microsoft 365 admin center https://admin.microsoft.com 2. Select Users > Active Users. 3. In the Search field enter the name of the user to be made a Global Administrator. 4. To create a new Global Admin: 1. Select the user's name. 2. A window will appear to the right. 3. Select Manage roles. 4. Select Admin center access. 5. Check Global Administrator. 6. Click Save changes. 5. To remove Global Admins: 1. Select User. 2. Under Roles select Manage roles. 3. De-Select the appropriate role. 4. Click Save changes.", - "Terraform": "" + "Other": "1. Go to https://entra.microsoft.com and sign in as Privileged Role Administrator or Global Administrator\n2. Navigate to Entra ID > Roles & admins > Global Administrator\n3. Select Assignments\n4. To add: click Add assignments, select user(s), click Add; repeat until total Global Administrators is between 2 and 4\n5. To remove: select extra assignment(s), click Remove, confirm\n6. Verify the Assignments count shows between 2 and 4", + "Terraform": "```hcl\n# Assign the Global Administrator role to a specific principal (user or group)\n# Critical: Looks up the built-in Global Administrator role\ndata \"azuread_directory_role\" \"global_admin\" {\n display_name = \"Global Administrator\"\n}\n\n# Critical: Creates the role assignment so the principal becomes a Global Administrator\n# This helps reach the recommended 2-4 Global Administrators\nresource \"azuread_directory_role_assignment\" \"\" {\n role_id = data.azuread_directory_role.global_admin.object_id # Critical: GA role ID\n principal_object_id = \"\" # Critical: target user/group object ID\n}\n```" }, "Recommendation": { - "Text": "Review the number of global administrators in your tenant. Add or remove global admins as necessary to ensure compliance with the recommended range of two to four.", - "Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/manage-roles-portal" + "Text": "Maintain `2-4` standing **Global Administrators** (include two break-glass accounts). Apply **least privilege** using scoped roles and **just-in-time** elevation. Enforce **MFA**, run periodic **access reviews**, and implement **separation of duties** to limit standing privileged exposure.", + "Url": "https://hub.prowler.com/check/admincenter_users_between_two_and_four_global_admins" } }, "Categories": [ + "identity-access", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/defender/defender_antiphishing_policy_configured/defender_antiphishing_policy_configured.metadata.json b/prowler/providers/m365/services/defender/defender_antiphishing_policy_configured/defender_antiphishing_policy_configured.metadata.json index e312a637e9..1572e0010b 100644 --- a/prowler/providers/m365/services/defender/defender_antiphishing_policy_configured/defender_antiphishing_policy_configured.metadata.json +++ b/prowler/providers/m365/services/defender/defender_antiphishing_policy_configured/defender_antiphishing_policy_configured.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "defender_antiphishing_policy_configured", - "CheckTitle": "Ensure anti-phishing policies are properly configured and active.", + "CheckTitle": "Defender anti-phishing policy active, quarantines spoofed senders and DMARC reject/quarantine failures, honors DMARC policy, safety tips enabled", "CheckType": [], "ServiceName": "defender", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "Defender Anti-Phishing Policy", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Ensure that anti-phishing policies are created and configured for specific users, groups, or domains, taking precedence over the default policy. This check verifies the existence of rules within policies and validates specific policy settings such as spoof intelligence, DMARC actions, safety tips, and unauthenticated sender actions.", - "Risk": "Without anti-phishing policies, organizations may rely solely on default settings, which might not adequately protect against phishing attacks targeted at specific users, groups, or domains. This increases the risk of successful phishing attempts and potential data breaches.", - "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/set-up-anti-phishing-policies?view=o365-worldwide", + "Description": "**Microsoft Defender for Office 365 anti-phishing policies** are evaluated for custom scoping to users, groups, or domains and precedence over the default, plus key settings: **spoof intelligence**, DMARC honoring, `quarantine` actions for spoof/DMARC, **safety tips**, unauthenticated sender indicators, and policy enablement.", + "Risk": "Missing or lax configuration lets **spoofed** and **impersonated** emails reach inboxes. Ignoring DMARC or not using `quarantine` enables delivery of fraudulent messages, driving **credential theft**, **BEC**, and **account takeover**, compromising data **confidentiality** and **integrity** and enabling lateral movement via mailbox rule abuse.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/set-up-anti-phishing-policies?view=o365-worldwide", + "https://learn.microsoft.com/en-us/defender-office-365/anti-phishing-policies-mdo-configure", + "https://learn.microsoft.com/en-us/defender-office-365/anti-phishing-policies-about" + ], "Remediation": { "Code": { - "CLI": "$params = @{Name='';PhishThresholdLevel=3;EnableTargetedUserProtection=$true;EnableOrganizationDomainsProtection=$true;EnableMailboxIntelligence=$true;EnableMailboxIntelligenceProtection=$true;EnableSpoofIntelligence=$true;TargetedUserProtectionAction='Quarantine';TargetedDomainProtectionAction='Quarantine';MailboxIntelligenceProtectionAction='Quarantine';TargetedUserQuarantineTag='DefaultFullAccessWithNotificationPolicy';MailboxIntelligenceQuarantineTag='DefaultFullAccessWithNotificationPolicy';TargetedDomainQuarantineTag='DefaultFullAccessWithNotificationPolicy';EnableFirstContactSafetyTips=$true;EnableSimilarUsersSafetyTips=$true;EnableSimilarDomainsSafetyTips=$true;EnableUnusualCharactersSafetyTips=$true;HonorDmarcPolicy=$true}; New-AntiPhishPolicy @params; New-AntiPhishRule -Name $params.Name -AntiPhishPolicy $params.Name -RecipientDomainIs (Get-AcceptedDomain).Name -Priority 0", + "CLI": "", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration and select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies, select Anti-phishing 5. Ensure policies have rules with the state set to 'on' and validate settings: spoof intelligence enabled, spoof intelligence action set to 'Quarantine', DMARC reject and quarantine actions, safety tips enabled, unauthenticated sender action enabled, show tag enabled, and honor DMARC policy enabled. If not, modify them to be as recommended.", + "Other": "1. Go to Microsoft 365 Defender: https://security.microsoft.com > Email & collaboration > Policies & rules > Threat policies > Anti-phishing\n2. Open the Default anti-phishing policy and click Edit\n3. Spoof settings: ensure Enable spoof intelligence is On and set If the message is detected as spoof by spoof intelligence to Quarantine\n4. DMARC: turn On Honor DMARC record policy and set both actions to Quarantine:\n - If DMARC policy is p=quarantine: Quarantine\n - If DMARC policy is p=reject: Quarantine\n5. Safety tips & indicators: turn On Show first contact safety tip, Show (?) for unauthenticated senders for spoof, and Show \"via\" tag\n6. Save changes\n7. If using custom anti-phishing policies, ensure their rule Status is On", "Terraform": "" }, "Recommendation": { - "Text": "Create and configure anti-phishing policies for specific users, groups, or domains to enhance protection against phishing attacks.", - "Url": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/set-up-anti-phishing-policies?view=o365-worldwide" + "Text": "Apply **defense in depth** for email:\n- Create high-priority custom policies for sensitive users/groups/domains\n- Enable **spoof intelligence**; honor DMARC (`p=quarantine`, `p=reject`) with `quarantine` actions\n- Turn on **safety tips** and unauthenticated sender tags\n- Review policy precedence, scope, and thresholds regularly to minimize false positives", + "Url": "https://hub.prowler.com/check/defender_antiphishing_policy_configured" } }, "Categories": [ + "email-security", "e5" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist.metadata.json b/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist.metadata.json index 17907a0bc7..fc7d6676cb 100644 --- a/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist.metadata.json +++ b/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_empty_ip_allowlist/defender_antispam_connection_filter_policy_empty_ip_allowlist.metadata.json @@ -1,30 +1,34 @@ { "Provider": "m365", "CheckID": "defender_antispam_connection_filter_policy_empty_ip_allowlist", - "CheckTitle": "Ensure the Anti-Spam Connection Filter Policy IP Allowlist is empty or undefined.", + "CheckTitle": "Defender Antispam Connection Filter Policy IP Allowlist is empty or undefined", "CheckType": [], "ServiceName": "defender", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Defender Anti-Spam Policy", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "This check focuses on Microsoft 365 organizations with Exchange Online mailboxes or standalone Exchange Online Protection (EOP) organizations. It ensures that the connection filter policy's IP Allowlist is empty or undefined to prevent bypassing spam filtering and sender authentication checks, which could lead to successful delivery of malicious emails.", - "Risk": "Using the IP Allowlist without additional verification like mail flow rules poses a risk, as emails from these sources skip essential security checks (SPF, DKIM, DMARC). This could allow attackers to deliver harmful emails directly to the Inbox.", + "Description": "**Microsoft Defender connection filter policy** is evaluated to determine whether the **IP Allowlist** (`IPAllowList`) is configured. The finding indicates if any IP addresses are present in the policy's allow list for Exchange Online or standalone EOP environments.", + "Risk": "Allowlisted IPs bypass **SPF**, **DKIM**, **DMARC** and antispam, enabling **phishing** and **spoofing** that steal credentials (confidentiality), deliver **malware** or fraudulent mail (integrity), and cause **inbox flooding** (availability). Attackers can abuse compromised or shared relays on those IPs to deliver malicious mail.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/exchange/set-hostedconnectionfilterpolicy?view=exchange-ps" + ], "Remediation": { "Code": { - "CLI": "Set-HostedConnectionFilterPolicy -Identity Default -IPAllowList @{}", + "CLI": "Set-HostedConnectionFilterPolicy -Identity Default -IPAllowList $null", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration and select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies, select Anti-spam and click on the Connection filter policy (Default). 5. Remove IP entries from the allow list. 6. Click Save.", + "Other": "1. Go to https://security.microsoft.com and sign in\n2. Email & collaboration > Policies & rules > Threat policies\n3. Open Anti-spam > Connection filter policy (Default)\n4. Edit the IP allow list and remove all entries\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Ensure that the IP Allowlist in your connection filter policy is empty or undefined to prevent bypassing essential security checks.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-hostedconnectionfilterpolicy?view=exchange-ps" + "Text": "Keep the **IP Allowlist** empty. Rely on layered controls: **SPF**, **DKIM**, **DMARC**, connection filtering, and content scanning. *If an exception is unavoidable*, apply **defense in depth** (sender auth, TLS, reputation checks, and tight scope/time limits) and prefer domain- or certificate-based trust over static IPs. Monitor delivery logs for abuse.", + "Url": "https://hub.prowler.com/check/defender_antispam_connection_filter_policy_empty_ip_allowlist" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_safe_list_off/defender_antispam_connection_filter_policy_safe_list_off.metadata.json b/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_safe_list_off/defender_antispam_connection_filter_policy_safe_list_off.metadata.json index 202b67c678..fc99632352 100644 --- a/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_safe_list_off/defender_antispam_connection_filter_policy_safe_list_off.metadata.json +++ b/prowler/providers/m365/services/defender/defender_antispam_connection_filter_policy_safe_list_off/defender_antispam_connection_filter_policy_safe_list_off.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "defender_antispam_connection_filter_policy_safe_list_off", - "CheckTitle": "Ensure the default connection filter policy has the SafeList setting disabled", + "CheckTitle": "Defender Antispam Connection Filter Policy has Safe List disabled", "CheckType": [], "ServiceName": "defender", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Defender Anti-Spam Policy", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "This check ensures that the EnableSafeList setting in the default connection filter policy is set to False. The safe list, managed dynamically by Microsoft, allows emails from listed IPs to bypass spam filtering and sender authentication checks, posing a security risk.", - "Risk": "If the safe list is enabled, emails from IPs on this list can bypass essential security checks (SPF, DKIM, DMARC), potentially allowing malicious emails to be delivered directly to users' inboxes.", - "RelatedUrl": "https://learn.microsoft.com/en-us/defender-office-365/connection-filter-policies-configure", + "Description": "**Microsoft Defender for Office 365 connection filter policy** safe list setting is evaluated. When enabled, mail from Microsoft-managed IPs skips spam filtering and some sender authentication. The finding indicates whether this implicit bypass is turned off.", + "Risk": "With the safe list on, inbound mail can bypass SPF/DKIM/DMARC and spam heuristics, allowing spoofed or phishing messages to reach inboxes. This risks credential theft (confidentiality), enables account takeover and tampering (integrity), and may lead to malware-driven outages (availability).", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/defender-office-365/connection-filter-policies-configure", + "https://learn.microsoft.com/en-us/defender-office-365/create-safe-sender-lists-in-office-365#use-the-ip-allow-list" + ], "Remediation": { "Code": { "CLI": "Set-HostedConnectionFilterPolicy -Identity Default -EnableSafeList $false", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration and select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies, select Anti-spam and click on the Connection filter policy (Default). 5. Disable the safe list option. 6. Click Save.", + "Other": "1. Go to https://security.microsoft.com/antispam\n2. Select Connection filter policy (Default)\n3. Click Edit connection filter policy\n4. Uncheck Turn on safe list\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Ensure that the EnableSafeList setting in your connection filter policy is set to False to prevent bypassing essential security checks.", - "Url": "https://learn.microsoft.com/en-us/defender-office-365/create-safe-sender-lists-in-office-365#use-the-ip-allow-list" + "Text": "Disable the **safe list** (`EnableSafeList=false`). Favor **allow-by-exception**: use the **Tenant Allow/Block List** or tightly scoped IPs only when necessary and validated by strong **email authentication**. Apply **least privilege**, review exceptions regularly, and layer **defense in depth** with anti-phishing and monitoring.", + "Url": "https://hub.prowler.com/check/defender_antispam_connection_filter_policy_safe_list_off" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured.metadata.json b/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured.metadata.json index a0b33c0449..1844220471 100644 --- a/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured.metadata.json +++ b/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "defender_antispam_outbound_policy_configured", - "CheckTitle": "Ensure Defender Outbound Spam Policies are set to notify administrators.", + "CheckTitle": "Defender outbound spam policy is configured to notify recipients when senders are blocked or exceed sending limits", "CheckType": [], "ServiceName": "defender", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "Defender Anti-Spam Outbound Policy", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Ensure that outbound anti-spam policies are configured to notify administrators and copy suspicious outbound messages to designated recipients when a sender is blocked for sending spam emails.", - "Risk": "Without outbound spam notifications and message copies, compromised accounts may go undetected, increasing the risk of reputation damage or data leakage through unauthorized email activity.", - "RelatedUrl": "https://learn.microsoft.com/en-us/defender-office-365/outbound-spam-protection-about", + "Description": "**Microsoft Defender for Office 365 outbound spam policies** must send **administrator alerts** and Bcc **suspicious outbound messages** when a sender exceeds limits or is blocked. The assessment checks for `notify limit exceeded` and `notify sender blocked` with recipient addresses in the default policy and any applicable custom policies.", + "Risk": "Absent alerts and copies, **compromised mailboxes** can exfiltrate data and send phishing undetected. This harms **email deliverability** through blocklisting and throttling (**availability**), undermines domain **integrity**, and impedes **forensics** by removing evidence needed to triage abusive outbound traffic.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/defender-office-365/outbound-spam-protection-about", + "https://learn.microsoft.com/is-is/defender-office-365/outbound-spam-policies-configure" + ], "Remediation": { "Code": { - "CLI": "$BccEmailAddress = @(\"\")\n$NotifyEmailAddress = @(\"\")\nSet-HostedOutboundSpamFilterPolicy -Identity Default -BccSuspiciousOutboundAdditionalRecipients $BccEmailAddress -BccSuspiciousOutboundMail $true -NotifyOutboundSpam $true -NotifyOutboundSpamRecipients $NotifyEmailAddress", + "CLI": "Set-HostedOutboundSpamFilterPolicy -Identity Default -BccSuspiciousOutboundMail $true -BccSuspiciousOutboundAdditionalRecipients \"\" -NotifyOutboundSpam $true -NotifyOutboundSpamRecipients \"\"", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration and select Policies & rules > Threat policies. 3. Under Policies, select Anti-spam. 4. Click on the Anti-spam outbound policy (default). 5. Select Edit protection settings then under Notifications: 6. Check 'Send a copy of suspicious outbound messages or message that exceed these limits to these users and groups' and enter the email addresses. 7. Check 'Notify these users and groups if a sender is blocked due to sending outbound spam' and enter the desired email addresses. 8. Click Save.", + "Other": "1. Sign in to Microsoft 365 Defender: https://security.microsoft.com\n2. Go to Email & collaboration > Policies & rules > Threat policies > Anti-spam\n3. Open Anti-spam outbound policy (Default) and select Edit protection settings\n4. Under Notifications:\n - Check \"Send a copy of suspicious outbound messages or messages that exceed these limits to these users and groups\" and add \n - Check \"Notify these users and groups if a sender is blocked due to sending outbound spam\" and add \n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Configure Defender outbound spam filter policies to notify administrators and copy suspicious outbound messages when users are blocked for sending spam.", - "Url": "https://learn.microsoft.com/en-us/defender-office-365/outbound-spam-protection-about" + "Text": "Enable outbound spam notifications and Bcc suspicious messages to a monitored mailbox, applying them consistently to default and scoped policies. Set prudent sending limits and block actions, disable unnecessary external forwarding, and monitor alerts-aligning with **least privilege** and **defense in depth**.", + "Url": "https://hub.prowler.com/check/defender_antispam_outbound_policy_configured" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled.metadata.json b/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled.metadata.json index bcb9f406cf..98158f8af3 100644 --- a/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled.metadata.json +++ b/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled.metadata.json @@ -1,30 +1,34 @@ { "Provider": "m365", "CheckID": "defender_antispam_outbound_policy_forwarding_disabled", - "CheckTitle": "Ensure Defender Outbound Spam Policies are set to disable mail forwarding.", + "CheckTitle": "Defender Outbound Spam policy disables mail forwarding", "CheckType": [], "ServiceName": "defender", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Defender Anti-Spam Outbound Policy", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Ensure Defender Outbound Spam Policies 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/defender-office-365/outbound-spam-protection-about", + "Description": "**Microsoft Defender for Office 365 outbound spam policies** are evaluated to confirm that automatic mail forwarding is disabled in the default policy and in any custom policies applied to users, groups, or domains.", + "Risk": "Allowing **automatic forwarding** enables covert **data exfiltration**, eroding **confidentiality**. Attackers or insiders can auto-route mail to external inboxes, persist access, evade monitoring, and harvest sensitive content (tickets, approvals, MFA codes), enabling **lateral movement** and fraud while reducing auditability.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/defender-office-365/outbound-spam-protection-about" + ], "Remediation": { "Code": { - "CLI": "Set-HostedOutboundSpamFilterPolicy -Identity {policyName} -AutoForwardingMode Off", + "CLI": "Set-HostedOutboundSpamFilterPolicy -Identity -AutoForwardingMode Off", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 Defender https://security.microsoft.com/. 2. Expand E-mail & collaboration then select Policies & rules. 3. Select Threat policies > Anti-spam. 4. Select Anti-spam outbound policy (default). 5. Click Edit protection settings. 6. Set Automatic forwarding rules dropdown to Off - Forwarding is disabled and click Save. 7. Repeat steps 4-6 for any additional higher priority, custom policies.", + "Other": "1. Sign in to https://security.microsoft.com\n2. Go to Email & collaboration > Policies & rules > Threat policies > Anti-spam\n3. Open Anti-spam outbound policy (Default) or the target custom policy\n4. Click Edit protection settings and set Automatic forwarding rules to Off - Forwarding is disabled, then Save\n5. For custom policies, ensure the policy Status is On (enabled); repeat for any additional policies", "Terraform": "" }, "Recommendation": { - "Text": "Block all forms of mail forwarding using Anti-spam outbound policies in Exchange Online. Apply exclusions only where justified by organizational policy.", - "Url": "https://learn.microsoft.com/en-us/defender-office-365/outbound-spam-protection-about" + "Text": "Disable **automatic forwarding** globally in outbound spam policies to enforce **least privilege** on data flows. *If exceptions are required*, restrict to named senders or domains, document approvals, and review regularly. Add **DLP**, alerts on new forwarding rules, and mailbox auditing for **defense in depth**.", + "Url": "https://hub.prowler.com/check/defender_antispam_outbound_policy_forwarding_disabled" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.metadata.json b/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.metadata.json index 8413a8fc82..ed178c87f1 100644 --- a/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.metadata.json +++ b/prowler/providers/m365/services/defender/defender_antispam_policy_inbound_no_allowed_domains/defender_antispam_policy_inbound_no_allowed_domains.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "defender_antispam_policy_inbound_no_allowed_domains", - "CheckTitle": "Ensure inbound anti-spam policies do not contain allowed domains", + "CheckTitle": "Inbound anti-spam policy does not contain allowed domains", "CheckType": [], "ServiceName": "defender", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "Defender Anti-Spam Policy", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Ensure that inbound anti-spam policies do not have any domains listed in the AllowedSenderDomains. Messages from these domains bypass most email protections, increasing the risk of successful phishing attacks.", - "Risk": "Having domains in the AllowedSenderDomains list allows emails from these domains to bypass essential security checks, increasing the risk of phishing attacks and other malicious activities.", - "RelatedUrl": "https://learn.microsoft.com/en-us/defender-office-365/anti-spam-protection-about#allow-and-block-lists-in-anti-spam-policies", + "Description": "**Microsoft Defender for Office 365 inbound anti-spam policies** are evaluated for domains listed in `AllowedSenderDomains`.\n\nThe finding identifies any policy where this list is populated rather than empty.", + "Risk": "Populating `AllowedSenderDomains` makes messages from those domains skip **spam filtering** and **email authentication** (SPF, DKIM, DMARC), often delivered with SCL `-1`. Attackers can spoof such domains to phish credentials, enable BEC, and alter mailboxes, undermining **confidentiality** and **integrity**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/exchangepowershell/set-hostedcontentfilterpolicy?view=exchange-ps", + "https://learn.microsoft.com/en-us/defender-office-365/anti-spam-policies-configure", + "https://learn.microsoft.com/en-us/defender-office-365/anti-spam-protection-about#allow-and-block-lists-in-anti-spam-policies" + ], "Remediation": { "Code": { - "CLI": "Set-HostedContentFilterPolicy -Identity -AllowedSenderDomains @{}", + "CLI": "Set-HostedContentFilterPolicy -Identity -AllowedSenderDomains $null", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 Defender (https://security.microsoft.com). 2. Click to expand Email & collaboration and select Policies & rules > Threat policies. 3. Under Policies, select Anti-spam. 4. Open each out-of-compliance inbound anti-spam policy by clicking on it. 5. Click Edit allowed and blocked senders and domains. 6. Select Allow domains. 7. Delete each domain from the domains list. 8. Click Done > Save. 9. Repeat as needed.", + "Other": "1. Open Microsoft 365 Defender: https://security.microsoft.com/antispam\n2. Open each inbound anti-spam policy (Default and any custom).\n3. Click Edit allowed and blocked senders and domains.\n4. Select Allow domains.\n5. Remove all domains, then click Done and Save.\n6. Repeat for any remaining inbound anti-spam policies.", "Terraform": "" }, "Recommendation": { - "Text": "Ensure that the AllowedSenderDomains list in your inbound anti-spam policies is empty to prevent bypassing essential security checks.", - "Url": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/configure-the-allowed-sender-domains?view=o365-worldwide" + "Text": "- Keep `AllowedSenderDomains` empty.\n- Use narrowly scoped allow logic that requires authentication alignment and additional conditions (sender, IP, headers).\n- Make any exceptions temporary and reviewed.\n\nApply **least privilege** and **defense in depth** to email trust decisions.", + "Url": "https://hub.prowler.com/check/defender_antispam_policy_inbound_no_allowed_domains" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured.metadata.json b/prowler/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured.metadata.json index 5b69d64b5f..f485b342f3 100644 --- a/prowler/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured.metadata.json +++ b/prowler/providers/m365/services/defender/defender_atp_safe_attachments_and_docs_configured/defender_atp_safe_attachments_and_docs_configured.metadata.json @@ -7,9 +7,9 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Defender ATP Policy", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "**Safe Attachments** for SharePoint, OneDrive, and Microsoft Teams protects organizations from inadvertently sharing malicious files. When enabled, files identified as malicious are blocked and cannot be opened, copied, moved, or shared until further actions are taken by the organization's security team.", + "Description": "**Microsoft Defender for Office 365 Safe Attachments** for SharePoint, OneDrive, and Microsoft Teams protects organizations from inadvertently sharing malicious files. When enabled, files identified as malicious are blocked and cannot be opened, copied, moved, or shared until further actions are taken by the organization's security team.", "Risk": "Without **Safe Attachments** enabled, users may download, sync, or access **malicious files** from SharePoint, OneDrive, or Teams, potentially leading to **malware infections** across the organization. If **Safe Documents** is disabled or users can bypass **Protected View**, malicious content in Office documents may execute and **compromise endpoints**.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/m365/services/defender/defender_chat_report_policy_configured/defender_chat_report_policy_configured.metadata.json b/prowler/providers/m365/services/defender/defender_chat_report_policy_configured/defender_chat_report_policy_configured.metadata.json index feb96141a4..20428b2ae5 100644 --- a/prowler/providers/m365/services/defender/defender_chat_report_policy_configured/defender_chat_report_policy_configured.metadata.json +++ b/prowler/providers/m365/services/defender/defender_chat_report_policy_configured/defender_chat_report_policy_configured.metadata.json @@ -1,30 +1,34 @@ { "Provider": "m365", "CheckID": "defender_chat_report_policy_configured", - "CheckTitle": "Ensure chat report submission policy is properly configured in Defender", + "CheckTitle": "Defender report submission policy uses customized addresses for junk, not junk and phish, and chat reports are sent only to a customized address", "CheckType": [], "ServiceName": "defender", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Defender Report Submission Policy", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Ensure Defender report submission policy is properly configured to use customized addresses and enable chat message reporting to customized addresses, while disabling report chat message to Microsoft.", - "Risk": "If Defender report submission policy is not properly configured, reported messages from Teams may not be handled or routed correctly, reducing the organization's ability to respond to threats.", - "RelatedUrl": "https://learn.microsoft.com/en-us/defender-office-365/submissions-teams?view=o365-worldwide", + "Description": "**Microsoft Defender for Office 365** user-reported settings ensure `junk`, `not-junk`, and `phish` reports are sent to **customized addresses** with valid destinations, and that **Teams chat reports** route to customized addresses while direct chat reporting to Microsoft is disabled.", + "Risk": "Misrouted or disabled user reports reduce **visibility** into Teams threats, delaying containment. Attackers can keep distributing **phishing links** or **malicious files**, causing credential theft (**confidentiality**), message manipulation (**integrity**), and channel disruption from ongoing spam (**availability**).", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/defender-office-365/submissions-teams?view=o365-worldwide" + ], "Remediation": { "Code": { - "CLI": "Set-ReportSubmissionPolicy -Identity DefaultReportSubmissionPolicy -EnableReportToMicrosoft $false -ReportChatMessageEnabled $false -ReportChatMessageToCustomizedAddressEnabled $true -ReportJunkToCustomizedAddress $true -ReportNotJunkToCustomizedAddress $true -ReportPhishToCustomizedAddress $true -ReportJunkAddresses $usersub -ReportNotJunkAddresses $usersub -ReportPhishAddresses $usersub", + "CLI": "Set-ReportSubmissionPolicy -Identity DefaultReportSubmissionPolicy -ReportJunkToCustomizedAddress $true -ReportNotJunkToCustomizedAddress $true -ReportPhishToCustomizedAddress $true -ReportJunkAddresses -ReportNotJunkAddresses -ReportPhishAddresses -ReportChatMessageEnabled $false -ReportChatMessageToCustomizedAddressEnabled $true", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 Defender (https://security.microsoft.com/). 2. Click on Settings > Email & collaboration > User reported settings. 3. Scroll to Microsoft Teams section. 4. Ensure Monitor reported messages in Microsoft Teams is checked. 5. Ensure Send reported messages to: is set to My reporting mailbox only with report email addresses defined for authorized staff.", + "Other": "1. Go to Microsoft 365 Defender: https://security.microsoft.com\n2. Navigate to Settings > Email & collaboration > User reported settings\n3. In Reported message destinations (Outlook):\n - Turn on Send Junk to a customized address and enter \n - Turn on Send Not junk to a customized address and enter \n - Turn on Send Phish to a customized address and enter \n4. In Microsoft Teams section:\n - Turn off Monitor reported messages in Microsoft Teams\n - Turn on Send reported Teams messages to a customized address", "Terraform": "" }, "Recommendation": { - "Text": "Configure Defender report submission policy to use customized addresses and enable chat message reporting to customized addresses, while disabling report chat message to Microsoft.", - "Url": "https://learn.microsoft.com/en-us/defender-office-365/submissions-teams?view=o365-worldwide" + "Text": "Send all user-reported `junk`, `not-junk`, and `phish` to monitored **custom mailboxes** and enable **Teams chat reporting** to those addresses, keeping direct chat submissions to Microsoft disabled. Apply **least privilege** to reviewer access, establish a **triage workflow**, and integrate alerts for **defense in depth**.", + "Url": "https://hub.prowler.com/check/defender_chat_report_policy_configured" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.metadata.json b/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.metadata.json index 93921a4e0c..a57bce662a 100644 --- a/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.metadata.json +++ b/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "defender_domain_dkim_enabled", - "CheckTitle": "Ensure that DKIM is enabled for all Exchange Online Domains", + "CheckTitle": "Exchange Online domain has DKIM enabled", "CheckType": [], "ServiceName": "defender", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Exchange Online Domain", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "This check ensures that DomainKeys Identified Mail (DKIM) is enabled for all Exchange Online domains. DKIM is a crucial authentication method that, along with SPF and DMARC, helps prevent attackers from sending spoofed emails that appear to originate from your domain. By adding a digital signature to outbound emails, DKIM allows receiving email systems to verify the legitimacy of incoming messages.", - "Risk": "If DKIM is not enabled, attackers may send spoofed emails that appear to originate from your domain, potentially leading to phishing attacks and damage to your domain's reputation.", - "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/use-dkim-to-validate-outbound-email?view=o365-worldwide", + "Description": "**Microsoft 365 Exchange Online domains** use **DKIM signing** for outbound mail. This evaluates each domain to confirm an active DKIM configuration so messages include a verifiable signature via the domain's DKIM selectors.", + "Risk": "Without **DKIM**, recipients can't verify sender authenticity, enabling **domain spoofing** and **BEC phishing**.\n\nAttackers can impersonate trusted mail to steal credentials, deliver malware, and pivot internally, impacting **confidentiality** and **integrity**. Messages may also fail **DMARC** alignment, reducing deliverability and trust.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/exchangepowershell/set-dkimsigningconfig?view=exchange-ps", + "https://learn.microsoft.com/en-us/powershell/module/exchange/set-dkimsigningconfig?view=exchange-ps", + "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/use-dkim-to-validate-outbound-email?view=o365-worldwide" + ], "Remediation": { "Code": { - "CLI": "Set-DkimSigningConfig -Identity -Enabled $True", + "CLI": "Set-DkimSigningConfig -Identity -Enabled $true", "NativeIaC": "", - "Other": "1. After DNS records are created, enable DKIM signing in Microsoft 365 Defender. 2. Navigate to Microsoft 365 Defender at https://security.microsoft.com/. 3. Go to Email & collaboration > Policies & rules > Threat policies. 4. Under Rules, select Email authentication settings. 5. Choose DKIM, click on each domain, and enable 'Sign messages for this domain with DKIM signature'.", + "Other": "1. Sign in to Microsoft 365 Defender: https://security.microsoft.com\n2. Go to Email & collaboration > Policies & rules > Threat policies > Email authentication settings > DKIM\n3. Select the domain and enable \"Sign messages for this domain with DKIM signatures\"\n4. If prompted for CNAMEs, publish the two records shown at your DNS provider, wait for DNS to update, then return and enable in step 3", "Terraform": "" }, "Recommendation": { - "Text": "Enable DKIM for all your Exchange Online domains to ensure emails are cryptographically signed and to protect against email spoofing.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-dkimsigningconfig?view=exchange-ps" + "Text": "- Enable **DKIM signing** for all sending domains and subdomains\n- Combine with **SPF** and **DMARC** to enforce alignment (defense in depth)\n- Apply **least privilege** to mail auth settings\n- Rotate DKIM keys regularly and monitor authentication results to detect anomalies", + "Url": "https://hub.prowler.com/check/defender_domain_dkim_enabled" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/defender/defender_malware_policy_common_attachments_filter_enabled/defender_malware_policy_common_attachments_filter_enabled.metadata.json b/prowler/providers/m365/services/defender/defender_malware_policy_common_attachments_filter_enabled/defender_malware_policy_common_attachments_filter_enabled.metadata.json index 7a2a731f82..d581e9fc0c 100644 --- a/prowler/providers/m365/services/defender/defender_malware_policy_common_attachments_filter_enabled/defender_malware_policy_common_attachments_filter_enabled.metadata.json +++ b/prowler/providers/m365/services/defender/defender_malware_policy_common_attachments_filter_enabled/defender_malware_policy_common_attachments_filter_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "defender_malware_policy_common_attachments_filter_enabled", - "CheckTitle": "Ensure the Common Attachment Types Filter is enabled.", + "CheckTitle": "Defender malware policy has Common Attachment Types Filter enabled", "CheckType": [], "ServiceName": "defender", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "Defender Malware Policy", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Ensure that the Common Attachment Types Filter is enabled in anti-malware policies to block known and custom malicious file types from being attached to emails.", - "Risk": "If this setting is not enabled, users may receive emails with malicious attachments that could contain malware, increasing the risk of endpoint infection or data compromise.", - "RelatedUrl": "https://learn.microsoft.com/en-us/defender-office-365/anti-malware-policies-configure?view=o365-worldwide", + "Description": "**Microsoft Defender for Office 365 anti-malware policies** use the **Common Attachment Types Filter** to block risky file formats regardless of extension. The evaluation checks whether this filter is enabled across default and custom policies and considers policy precedence that could override or bypass the protection.", + "Risk": "Without consistent **attachment type blocking**, malicious `exe`, `js`, `iso`, or `zip` payloads can reach users, enabling code execution and phishing kits.\n- Confidentiality: data exfiltration\n- Integrity: credential theft/tampering\n- Availability: ransomware\n\nPolicy scope/priority gaps can leave specific users unprotected.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/common-attachment-blocking-scenarios", + "https://learn.microsoft.com/en-us/powershell/module/exchange/set-malwarefilterpolicy?view=exchange-ps", + "https://learn.microsoft.com/en-us/defender-office-365/anti-malware-policies-configure?view=o365-worldwide" + ], "Remediation": { "Code": { - "CLI": "Set-MalwareFilterPolicy -Identity Default -EnableFileFilter $true", + "CLI": "Set-MalwareFilterPolicy -Identity -EnableFileFilter $true", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration and select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies, select Anti-malware and click on the Default (Default) policy. 5. On the policy page, scroll to the bottom and click Edit protection settings. 6. Check the option Enable the common attachments filter. 7. Click Save.", + "Other": "1. Sign in to Microsoft 365 Defender: https://security.microsoft.com\n2. Go to Email & collaboration > Policies & rules > Threat policies > Anti-malware\n3. Open the failing anti-malware policy (e.g., Default or the named custom policy)\n4. Click Edit protection settings\n5. Enable \"Enable the common attachments filter\"\n6. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Enable the common attachment types filter in your default or custom anti-malware policy to prevent the delivery of emails with potentially dangerous attachments.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-malwarefilterpolicy?view=exchange-ps" + "Text": "Enable the **Common Attachment Types Filter** in all applicable anti-malware policies and choose a strict action (e.g., `Quarantine` or `Reject`).\n- Block high-risk formats; review the list regularly\n- Align policy precedence to cover every recipient\n- Use defense-in-depth: **Safe Attachments**, **Safe Links**, **ZAP**; apply **least privilege** to file types", + "Url": "https://hub.prowler.com/check/defender_malware_policy_common_attachments_filter_enabled" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/defender/defender_malware_policy_comprehensive_attachments_filter_applied/defender_malware_policy_comprehensive_attachments_filter_applied.metadata.json b/prowler/providers/m365/services/defender/defender_malware_policy_comprehensive_attachments_filter_applied/defender_malware_policy_comprehensive_attachments_filter_applied.metadata.json index 3b3616d27f..0b0f2f8d67 100644 --- a/prowler/providers/m365/services/defender/defender_malware_policy_comprehensive_attachments_filter_applied/defender_malware_policy_comprehensive_attachments_filter_applied.metadata.json +++ b/prowler/providers/m365/services/defender/defender_malware_policy_comprehensive_attachments_filter_applied/defender_malware_policy_comprehensive_attachments_filter_applied.metadata.json @@ -1,30 +1,36 @@ { "Provider": "m365", "CheckID": "defender_malware_policy_comprehensive_attachments_filter_applied", - "CheckTitle": "Ensure the Common Attachment Types Filter is enabled and applied in a comprehensive way", + "CheckTitle": "Defender anti-malware policy has Common Attachment Types Filter enabled and blocks all recommended file types", "CheckType": [], "ServiceName": "defender", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "Defender Malware Policy", + "Severity": "high", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Ensure that the Common Attachment Types Filter is enabled in all enabled anti-malware policies in a Comprehensive way to block known and custom malicious file types from being attached to emails. This means that the file types that the filter blocks are checked by the organization, by default all the default file types from M365 defender should be blocked but you can change that with the config file.", - "Risk": "If this setting or the policy is not enabled, users may receive emails with malicious attachments that could contain malware, increasing the risk of endpoint infection or data compromise.", - "RelatedUrl": "https://learn.microsoft.com/en-us/defender-office-365/anti-malware-protection-about?view=o365-worldwide#common-attachments-filter-in-anti-malware-policies", + "Description": "**Microsoft Defender anti-malware policies** use the **Common Attachment Types Filter** to block a comprehensive set of risky file extensions. It evaluates whether the filter is enabled and all recommended types are blocked across the default policy and any enabled custom policies, considering scope and precedence.", + "Risk": "Missing or partial blocking of dangerous extensions lets **malicious attachments** reach users, enabling code execution, malware staging, and credential theft. Mis-scoped custom policies can override safer defaults, risking **confidentiality** via data exfiltration and **availability** through ransomware and lateral movement.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/defender-office-365/anti-malware-policies-configure", + "https://learn.microsoft.com/en-us/defender-office-365/anti-malware-protection-about?view=o365-worldwide#common-attachments-filter-in-anti-malware-policies", + "https://learn.microsoft.com/en-us/powershell/module/exchange/set-malwarefilterpolicy?view=exchange-ps" + ], "Remediation": { "Code": { - "CLI": "$Policy = @{Name = 'CIS L2 Attachment Policy'; EnableFileFilter = $true; }; $L2Extensions = @('ace','ani','apk','app','appx','arj','bat','cab','cmd','com','deb','dex','dll','docm','elf','exe','hta','img','iso','jar','jnlp','kext','lha','lib','library','lnk','lzh','macho','msc','msi','msix','msp','mst','pif','ppa','ppam','reg','rev','scf','scr','sct','sys','uif','vb','vbe','vbs','vxd','wsc','wsf','wsh','xll','xz','z'); New-MalwareFilterPolicy @Policy -FileTypes $L2Extensions; $Rule = @{Name = $Policy.Name; Enabled = $false; MalwareFilterPolicy = $Policy.Name; Priority = 0}; New-MalwareFilterRule @Rule", + "CLI": "Set-MalwareFilterPolicy -Identity Default -EnableFileFilter $true -FileTypes ace,ani,apk,app,appx,arj,bat,cab,cmd,com,deb,dex,dll,docm,elf,exe,hta,img,iso,jar,jnlp,kext,lha,lib,library,lnk,lzh,macho,msc,msi,msix,msp,mst,pif,ppa,ppam,reg,rev,scf,scr,sct,sys,uif,vb,vbe,vbs,vxd,wsc,wsf,wsh,xll,xz,z", "NativeIaC": "", - "Other": "1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration and select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies, select Anti-malware and click on the Default (Default) policy. 5. On the policy page, scroll to the bottom and click Edit protection settings. 6. Check the option Enable the common attachments filter. 7. Click on select file types and select the file types you want to block. 8. Click Save. 9. Ensure the status of the policy is On", + "Other": "1. Go to Microsoft 365 Defender: https://security.microsoft.com\n2. Navigate to Email & collaboration > Policies & rules > Threat policies > Anti-malware\n3. Open Default (Default) policy and select Edit protection settings\n4. Enable \"Enable the common attachments filter\"\n5. Select file types and ensure ALL of these are selected: ace, ani, apk, app, appx, arj, bat, cab, cmd, com, deb, dex, dll, docm, elf, exe, hta, img, iso, jar, jnlp, kext, lha, lib, library, lnk, lzh, macho, msc, msi, msix, msp, mst, pif, ppa, ppam, reg, rev, scf, scr, sct, sys, uif, vb, vbe, vbs, vxd, wsc, wsf, wsh, xll, xz, z\n6. Save", "Terraform": "" }, "Recommendation": { - "Text": "Enable the common attachment types filter in your default or custom anti-malware policy to prevent the delivery of emails with potentially dangerous attachments.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-malwarefilterpolicy?view=exchange-ps" + "Text": "Enable and enforce the **Common Attachment Types Filter** in all anti-malware policies and block the full recommended set. Align custom policy scope and priority to avoid weakening coverage. Apply **least privilege** to exceptions, prefer quarantine, and regularly review/expand blocked types. Use ZAP and monitoring for **defense-in-depth**.", + "Url": "https://hub.prowler.com/check/defender_malware_policy_comprehensive_attachments_filter_applied" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/defender/defender_malware_policy_notifications_internal_users_malware_enabled/defender_malware_policy_notifications_internal_users_malware_enabled.metadata.json b/prowler/providers/m365/services/defender/defender_malware_policy_notifications_internal_users_malware_enabled/defender_malware_policy_notifications_internal_users_malware_enabled.metadata.json index 1fc47b03c6..4380f8eed4 100644 --- a/prowler/providers/m365/services/defender/defender_malware_policy_notifications_internal_users_malware_enabled/defender_malware_policy_notifications_internal_users_malware_enabled.metadata.json +++ b/prowler/providers/m365/services/defender/defender_malware_policy_notifications_internal_users_malware_enabled/defender_malware_policy_notifications_internal_users_malware_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "defender_malware_policy_notifications_internal_users_malware_enabled", - "CheckTitle": "Ensure notifications for internal users sending malware is Enabled", + "CheckTitle": "Defender anti-malware policy has admin notifications enabled for internal users sending malware", "CheckType": [], "ServiceName": "defender", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Defender Malware Policy", + "Severity": "medium", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Verify that Exchange Online Protection (EOP) is configured to notify admins of malicious activity from internal users.", - "Risk": "If notifications for internal users sending malware are not enabled, administrators may not be aware of potential threats originating from within the organization, increasing the risk of undetected malicious activities.", - "RelatedUrl": "https://learn.microsoft.com/en-us/defender-office-365/anti-malware-protection-about", + "Description": "**Microsoft Defender for Office 365 anti-malware policies** are checked for **admin notifications** on malware detected from **internal senders**, ensuring a valid notification address is defined (`EnableInternalSenderAdminNotifications` and `InternalSenderAdminAddress`).\n\n*Effective settings across default and custom policies are considered.*", + "Risk": "Without these notifications, malware sent from internal accounts can persist unnoticed, delaying response and containment. This undermines **integrity** of email, enables **lateral movement** and **outbound propagation**, and can cause **domain reputation** damage and blocklisting, affecting **availability**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/powershell/module/exchange/set-malwarefilterpolicy?view=exchange-ps", + "https://learn.microsoft.com/en-us/defender-office-365/anti-malware-protection-about" + ], "Remediation": { "Code": { - "CLI": "Set-MalwareFilterPolicy -Identity Default -EnableInternalSenderAdminNotifications $true -InternalSenderAdminAddress 'admin@example.com'", + "CLI": "Set-MalwareFilterPolicy -Identity Default -EnableInternalSenderAdminNotifications $true -InternalSenderAdminAddress \"\"", "NativeIaC": "", - "Other": "1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Execute the command: Get-MalwareFilterPolicy | fl Identity, EnableInternalSenderAdminNotifications, InternalSenderAdminAddress. 3. Ensure 'Notify an admin about undelivered messages from internal senders' is set to On and that at least one email address is listed under Administrator email address.", + "Other": "1. In the Microsoft Defender portal (security.microsoft.com), go to Email & collaboration > Policies & rules > Threat policies > Anti-malware\n2. Select the affected policy (e.g., Default) and click Edit policy\n3. Open Notifications\n4. Turn on \"Notify an admin about undelivered messages from internal senders\"\n5. Add at least one Administrator email address\n6. Save", "Terraform": "" }, "Recommendation": { - "Text": "Enable notifications for internal users sending malware in your Defender Malware Policy to ensure admins are alerted of potential threats.", - "Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-malwarefilterpolicy?view=exchange-ps" + "Text": "Enable and maintain admin alerts for internal-sender malware and route to a monitored mailbox or SOC list (`EnableInternalSenderAdminNotifications` and `InternalSenderAdminAddress`).\n\nEnsure coverage via policy precedence, integrate with SIEM, and apply **least privilege** and **defense in depth** to limit impact.", + "Url": "https://hub.prowler.com/check/defender_malware_policy_notifications_internal_users_malware_enabled" } }, "Categories": [ + "email-security", "e3" ], "DependsOn": [], diff --git a/prowler/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled.metadata.json b/prowler/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled.metadata.json index 0a4176a0df..da2d92d35d 100644 --- a/prowler/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled.metadata.json +++ b/prowler/providers/m365/services/defender/defender_safe_attachments_policy_enabled/defender_safe_attachments_policy_enabled.metadata.json @@ -7,9 +7,9 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Defender Safe Attachments Policy", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Safe Attachments provides an additional layer of protection by scanning email attachments in a secure environment before delivering them to recipients.\n\nThe Built-In Protection Policy should have **Enable=True**, **Action=Block**, and **QuarantineTag=AdminOnlyAccessPolicy** to ensure malicious attachments are blocked and quarantined with admin-only access.", + "Description": "**Microsoft Defender for Office 365 Safe Attachments** provides an additional layer of protection by scanning email attachments in a secure environment before delivering them to recipients.\n\nThe Built-In Protection Policy should have **Enable=True**, **Action=Block**, and **QuarantineTag=AdminOnlyAccessPolicy** to ensure malicious attachments are blocked and quarantined with admin-only access.", "Risk": "Without properly configured Safe Attachments policies, malicious email attachments could reach users' mailboxes and potentially compromise the organization through:\n\n- **Malware delivery** via infected documents\n- **Ransomware attacks** through weaponized attachments\n- **Data exfiltration** using malicious scripts", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled.metadata.json b/prowler/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled.metadata.json index cccebcfc7f..0c2c716822 100644 --- a/prowler/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled.metadata.json +++ b/prowler/providers/m365/services/defender/defender_safelinks_policy_enabled/defender_safelinks_policy_enabled.metadata.json @@ -7,9 +7,9 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Defender Safe Links Policy", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Safe Links is a Microsoft Defender for Office 365 feature that provides URL scanning and rewriting of inbound email messages, as well as time-of-click verification of URLs and links in email messages, Teams, and Office apps.\n\nThis check verifies that the Safe Links policy is properly configured with all recommended settings enabled.", + "Description": "**Microsoft Defender for Office 365 Safe Links** is a feature that provides URL scanning and rewriting of inbound email messages, as well as time-of-click verification of URLs and links in email messages, Teams, and Office apps.\n\nThis check verifies that the Safe Links policy is properly configured with all recommended settings enabled.", "Risk": "Without properly configured Safe Links protection, users may be vulnerable to **phishing attacks** and **malicious URLs** in emails, Teams messages, and Office documents.\n\nAttackers could bypass security by using URLs that redirect to malicious content after initial scanning.", "RelatedUrl": "", "AdditionalURLs": [ 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 d2df3f8892..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 @@ -7,10 +7,10 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Teams Protection Policy", + "ResourceType": "NotDefined", "ResourceGroup": "collaboration", - "Description": "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", + "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 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/defenderidentity/defenderidentity_health_issues_no_open/defenderidentity_health_issues_no_open.metadata.json b/prowler/providers/m365/services/defenderidentity/defenderidentity_health_issues_no_open/defenderidentity_health_issues_no_open.metadata.json index deaf9078f8..52b09e830f 100644 --- a/prowler/providers/m365/services/defenderidentity/defenderidentity_health_issues_no_open/defenderidentity_health_issues_no_open.metadata.json +++ b/prowler/providers/m365/services/defenderidentity/defenderidentity_health_issues_no_open/defenderidentity_health_issues_no_open.metadata.json @@ -7,9 +7,9 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Defender for Identity Health Issue", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Microsoft Defender for Identity (MDI) monitors your hybrid identity infrastructure and detects advanced threats targeting Active Directory. This check verifies that MDI sensors are deployed and that there are no unresolved health issues that may affect the ability to detect identity-based attacks.", + "Description": "**Microsoft Defender for Identity (MDI)** monitors your hybrid identity infrastructure and detects advanced threats targeting Active Directory. This check verifies that MDI sensors are deployed and that there are no unresolved health issues that may affect the ability to detect identity-based attacks.", "Risk": "Without deployed MDI sensors or with unresolved health issues, organizations face critical gaps in threat detection. Misconfigured or missing sensors fail to monitor domain controllers, allowing identity-based attacks like Pass-the-Hash, Golden Ticket, or lateral movement to go undetected. Attackers commonly exploit these blind spots to compromise hybrid environments while evading detection.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/m365/services/defenderxdr/defenderxdr_critical_asset_management_pending_approvals/defenderxdr_critical_asset_management_pending_approvals.metadata.json b/prowler/providers/m365/services/defenderxdr/defenderxdr_critical_asset_management_pending_approvals/defenderxdr_critical_asset_management_pending_approvals.metadata.json index 1d2c58ba2b..4bd6c1a5a3 100644 --- a/prowler/providers/m365/services/defenderxdr/defenderxdr_critical_asset_management_pending_approvals/defenderxdr_critical_asset_management_pending_approvals.metadata.json +++ b/prowler/providers/m365/services/defenderxdr/defenderxdr_critical_asset_management_pending_approvals/defenderxdr_critical_asset_management_pending_approvals.metadata.json @@ -1,16 +1,16 @@ { "Provider": "m365", "CheckID": "defenderxdr_critical_asset_management_pending_approvals", - "CheckTitle": "Ensure all Critical Asset Management classifications are reviewed and approved in Microsoft Defender XDR", + "CheckTitle": "Critical asset management classifications are reviewed and approved", "CheckType": [], "ServiceName": "defenderxdr", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Defender XDR Critical Asset Management", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Assets with a lower classification confidence score in Microsoft Defender XDR must be approved by a security administrator.\n\nAsset classifications that have not yet been reviewed and approved may result in incomplete **critical asset** visibility.", - "Risk": "Stale pending approvals lead to limited visibility in Microsoft Defender XDR. **Critical assets** that are not properly identified and classified may not receive appropriate security monitoring and protections, creating gaps in the organization's security posture.", + "Description": "**Microsoft Defender XDR critical asset management classifications** with a lower classification confidence score must be approved by a security administrator.\n\nAsset classifications that have not yet been reviewed and approved may result in incomplete **critical asset** visibility.", + "Risk": "Stale pending approvals lead to limited visibility in **Microsoft Defender XDR**. **Critical assets** that are not properly identified and classified may not receive appropriate security monitoring and protections, creating gaps in the organization's security posture.", "RelatedUrl": "", "AdditionalURLs": [ "https://learn.microsoft.com/en-us/security-exposure-management/classify-critical-assets", diff --git a/prowler/providers/m365/services/defenderxdr/defenderxdr_endpoint_privileged_user_exposed_credentials/defenderxdr_endpoint_privileged_user_exposed_credentials.metadata.json b/prowler/providers/m365/services/defenderxdr/defenderxdr_endpoint_privileged_user_exposed_credentials/defenderxdr_endpoint_privileged_user_exposed_credentials.metadata.json index 3f4efe68c8..4142484eec 100644 --- a/prowler/providers/m365/services/defenderxdr/defenderxdr_endpoint_privileged_user_exposed_credentials/defenderxdr_endpoint_privileged_user_exposed_credentials.metadata.json +++ b/prowler/providers/m365/services/defenderxdr/defenderxdr_endpoint_privileged_user_exposed_credentials/defenderxdr_endpoint_privileged_user_exposed_credentials.metadata.json @@ -7,9 +7,9 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "Exposure Management", + "ResourceType": "NotDefined", "ResourceGroup": "security", - "Description": "Privileged users may have authentication artifacts (CLI secrets, cookies, tokens) exposed on endpoints with high risk scores. Microsoft Defender XDR's Security Exposure Management detects when credentials from users with Entra ID privileged roles are present on vulnerable devices.", + "Description": "Microsoft Defender XDR's **Security Exposure Management** detects when credentials from users with Entra ID privileged roles are present on vulnerable devices. Privileged users may have authentication artifacts (CLI secrets, cookies, tokens) exposed on endpoints with high risk scores.", "Risk": "Exposed credentials on vulnerable endpoints enable account takeover through stolen tokens or cookies, Conditional Access bypass via primary refresh tokens, lateral movement to sensitive resources, and persistence until tokens are explicitly revoked.", "RelatedUrl": "", "AdditionalURLs": [ 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/purview/purview_audit_log_search_enabled/purview_audit_log_search_enabled.metadata.json b/prowler/providers/m365/services/purview/purview_audit_log_search_enabled/purview_audit_log_search_enabled.metadata.json index 6a7f056fe5..68e458ee49 100644 --- a/prowler/providers/m365/services/purview/purview_audit_log_search_enabled/purview_audit_log_search_enabled.metadata.json +++ b/prowler/providers/m365/services/purview/purview_audit_log_search_enabled/purview_audit_log_search_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "m365", "CheckID": "purview_audit_log_search_enabled", - "CheckTitle": "Ensure Purview audit log search is enabled", + "CheckTitle": "Purview audit log search is enabled", "CheckType": [], "ServiceName": "purview", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "Purview Settings", + "ResourceType": "NotDefined", "ResourceGroup": "governance", - "Description": "Ensure Purview audit log search is enabled.", - "Risk": "Disabling Microsoft 365 audit log search can hinder the ability to track and monitor user and admin activities, making it harder to detect suspicious behavior, security incidents, or compliance violations. This can result in undetected breaches and inability to respond to incidents effectively.", - "RelatedUrl": "https://learn.microsoft.com/en-us/purview/audit-search?tabs=microsoft-purview-portal", + "Description": "Microsoft Purview tenant setting for **audit log search** is assessed to confirm unified audit log ingestion (`UnifiedAuditLogIngestionEnabled`), which records user and admin activities and makes them searchable.", + "Risk": "Without **audit log ingestion/search**, activity trails are missing or delayed, reducing visibility and accountability.\n- Data exfiltration and privilege abuse go undetected (confidentiality/integrity)\n- Incident response and forensics fail due to absent evidence, increasing dwell time", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/purview/audit-search?tabs=microsoft-purview-portal", + "https://learn.microsoft.com/en-us/purview/audit-log-enable-disable" + ], "Remediation": { "Code": { "CLI": "Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $true", "NativeIaC": "", - "Other": "1. Navigate to Microsoft Purview https://compliance.microsoft.com. 2. Select Audit to open the audit search. 3. Click Start recording user and admin activity next to the information warning at the top. 4. Click Yes on the dialog box to confirm.", + "Other": "1. Go to https://compliance.microsoft.com and sign in with an admin account\n2. Open Solutions > Audit\n3. Click Start recording user and admin activity\n4. Click Yes to confirm", "Terraform": "" }, "Recommendation": { - "Text": "Ensure that Microsoft 365 audit log search is enabled to maintain a comprehensive record of user and admin activities. This will help improve security monitoring, support compliance needs, and provide critical insights for responding to incidents.", - "Url": "https://learn.microsoft.com/en-us/purview/audit-search?tabs=microsoft-purview-portal" + "Text": "Enable and keep **audit log search** on (`UnifiedAuditLogIngestionEnabled=true`). Apply **least privilege** to audit roles, set retention aligned to sensitivity, forward logs to a SIEM for **defense in depth**, and routinely review and alert on audit events. *Avoid disabling auditing even when using third-party tools.*", + "Url": "https://hub.prowler.com/check/purview_audit_log_search_enabled" } }, "Categories": [ + "logging", "e3" ], "DependsOn": [], 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/openstack/services/objectstorage/__init__.py b/prowler/providers/openstack/services/objectstorage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_client.py b/prowler/providers/openstack/services/objectstorage/objectstorage_client.py new file mode 100644 index 0000000000..b858f90176 --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.openstack.services.objectstorage.objectstorage_service import ( + ObjectStorage, +) + +objectstorage_client = ObjectStorage(Provider.get_global_provider()) diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_acl_not_globally_shared/__init__.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_acl_not_globally_shared/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_acl_not_globally_shared/objectstorage_container_acl_not_globally_shared.metadata.json b/prowler/providers/openstack/services/objectstorage/objectstorage_container_acl_not_globally_shared/objectstorage_container_acl_not_globally_shared.metadata.json new file mode 100644 index 0000000000..02d66edfa5 --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_acl_not_globally_shared/objectstorage_container_acl_not_globally_shared.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "objectstorage_container_acl_not_globally_shared", + "CheckTitle": "Object storage container read ACL is not globally shared", + "CheckType": [], + "ServiceName": "objectstorage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "OS::Swift::Container", + "ResourceGroup": "storage", + "Description": "**OpenStack Swift containers** are evaluated to verify that the **read ACL** does not use `*:*` (global sharing). The `*:*` ACL grants read access to all authenticated users from any project in the OpenStack deployment, violating project isolation and the principle of least privilege.", + "Risk": "Containers with `*:*` read ACL are accessible to **every authenticated user** in the OpenStack deployment, regardless of their project. This breaks **multi-tenant isolation**, exposing data to users in other projects. **Compromised credentials** from any project can access the container's contents.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/swift/latest/overview_acl.html", + "https://docs.openstack.org/security-guide/object-storage.html" + ], + "Remediation": { + "Code": { + "CLI": "swift post --read-acl ':*'", + "NativeIaC": "", + "Other": "1. Navigate to **Object Store > Containers**\n2. Select the container with global read ACL\n3. Edit the container metadata\n4. Replace `*:*` with specific project-scoped ACLs\n5. Save changes", + "Terraform": "" + }, + "Recommendation": { + "Text": "Replace `*:*` read ACLs with **project-scoped ACLs** (e.g., `project-id:*` or `project-id:user-id`). Use the **most restrictive ACL** that meets access requirements. Implement regular ACL audits to detect overly permissive configurations. Consider using **Keystone role-based access control** for fine-grained permissions.", + "Url": "https://hub.prowler.com/check/objectstorage_container_acl_not_globally_shared" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "The `*:*` ACL means 'any user in any project'. This is different from `.r:*` which grants anonymous (unauthenticated) access. Both are overly permissive but target different audiences: `*:*` targets all authenticated users while `.r:*` targets everyone including unauthenticated users." +} diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_acl_not_globally_shared/objectstorage_container_acl_not_globally_shared.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_acl_not_globally_shared/objectstorage_container_acl_not_globally_shared.py new file mode 100644 index 0000000000..e5c21616d8 --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_acl_not_globally_shared/objectstorage_container_acl_not_globally_shared.py @@ -0,0 +1,29 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.objectstorage.objectstorage_client import ( + objectstorage_client, +) + + +class objectstorage_container_acl_not_globally_shared(Check): + """Ensure object storage container read ACL does not use global sharing.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for container in objectstorage_client.containers: + report = CheckReportOpenStack(metadata=self.metadata(), resource=container) + acl_entries = [entry.strip() for entry in container.read_ACL.split(",")] + if "*:*" in acl_entries or "*" in acl_entries: + report.status = "FAIL" + report.status_extended = f"Container {container.name} has globally shared read ACL (*:*) allowing all authenticated users from any project." + else: + report.status = "PASS" + report.status_extended = ( + f"Container {container.name} read ACL is not globally shared." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_listing_disabled/__init__.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_listing_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_listing_disabled/objectstorage_container_listing_disabled.metadata.json b/prowler/providers/openstack/services/objectstorage/objectstorage_container_listing_disabled/objectstorage_container_listing_disabled.metadata.json new file mode 100644 index 0000000000..446d01a74b --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_listing_disabled/objectstorage_container_listing_disabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "objectstorage_container_listing_disabled", + "CheckTitle": "Object storage container listings are not publicly accessible", + "CheckType": [], + "ServiceName": "objectstorage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OS::Swift::Container", + "ResourceGroup": "storage", + "Description": "**OpenStack Swift containers** are evaluated to verify that **public container listings** are disabled. The `.rlistings` ACL allows users with read access to list all objects within the container. When combined with `.r:*`, this enables unauthenticated users to enumerate all objects, facilitating targeted data exfiltration.", + "Risk": "Containers with `.rlistings` enabled allow attackers to enumerate all object names, sizes, and modification dates. This **metadata exposure** aids **targeted attacks**, reveals application structure, and can expose sensitive file names. Combined with public read access, it enables complete **data exfiltration**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/swift/latest/overview_acl.html", + "https://docs.openstack.org/security-guide/object-storage.html" + ], + "Remediation": { + "Code": { + "CLI": "swift post --read-acl ''", + "NativeIaC": "", + "Other": "1. Navigate to **Object Store > Containers**\n2. Select the container with public listing\n3. Edit the container metadata\n4. Remove `.rlistings` from the Read ACL\n5. Save changes", + "Terraform": "" + }, + "Recommendation": { + "Text": "Remove `.rlistings` from container read ACLs to prevent **public object enumeration**. Listings should only be available to **authenticated project members**. If external access is needed, implement application-level access control with authenticated APIs.", + "Url": "https://hub.prowler.com/check/objectstorage_container_listing_disabled" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "The `.rlistings` ACL controls whether container listings (GET on container) are allowed. Even without `.rlistings`, individual objects may still be readable if `.r:*` is set. Both ACLs should be removed for full protection." +} diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_listing_disabled/objectstorage_container_listing_disabled.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_listing_disabled/objectstorage_container_listing_disabled.py new file mode 100644 index 0000000000..a541b9866e --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_listing_disabled/objectstorage_container_listing_disabled.py @@ -0,0 +1,32 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.objectstorage.objectstorage_client import ( + objectstorage_client, +) + + +class objectstorage_container_listing_disabled(Check): + """Ensure object storage container object listings are not publicly accessible.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for container in objectstorage_client.containers: + report = CheckReportOpenStack(metadata=self.metadata(), resource=container) + acl_entries = [entry.strip() for entry in container.read_ACL.split(",")] + if ".rlistings" in acl_entries: + report.status = "FAIL" + report.status_extended = f"Container {container.name} has public listing enabled (.rlistings) allowing anonymous object enumeration." + elif "*:*" in acl_entries or "*" in acl_entries: + report.status = "FAIL" + report.status_extended = f"Container {container.name} has listing enabled via global read ACL (*:*) allowing all authenticated users to list objects." + else: + report.status = "PASS" + report.status_extended = ( + f"Container {container.name} does not have public listing enabled." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_metadata_sensitive_data/__init__.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_metadata_sensitive_data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_metadata_sensitive_data/objectstorage_container_metadata_sensitive_data.metadata.json b/prowler/providers/openstack/services/objectstorage/objectstorage_container_metadata_sensitive_data/objectstorage_container_metadata_sensitive_data.metadata.json new file mode 100644 index 0000000000..b3a39bd3f8 --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_metadata_sensitive_data/objectstorage_container_metadata_sensitive_data.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "openstack", + "CheckID": "objectstorage_container_metadata_sensitive_data", + "CheckTitle": "Object storage container metadata does not contain sensitive data", + "CheckType": [], + "ServiceName": "objectstorage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "OS::Swift::Container", + "ResourceGroup": "storage", + "Description": "**OpenStack Swift container metadata** is evaluated to detect **sensitive data** such as passwords, API keys, secrets, and private keys. Container metadata is accessible to any user with read access to the container. Storing secrets in metadata exposes them to unauthorized access and credential theft.", + "Risk": "Container metadata containing **sensitive data** exposes credentials to any user with container read access. Attackers with read permissions can extract **passwords**, **API keys**, and **private keys**. Stolen credentials enable unauthorized access to other systems, **data exfiltration**, and **privilege escalation**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/swift/latest/overview_acl.html", + "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html" + ], + "Remediation": { + "Code": { + "CLI": "swift post --meta ':'", + "NativeIaC": "", + "Other": "1. Identify containers with sensitive metadata\n2. Remove sensitive metadata keys using CLI\n3. Rotate exposed credentials immediately\n4. Store secrets in Barbican or external secrets manager instead", + "Terraform": "" + }, + "Recommendation": { + "Text": "Never store secrets in container metadata. Use **Barbican** (OpenStack Key Manager), **Vault**, or external secrets management instead. Remove existing sensitive metadata and **rotate any exposed credentials**. Implement metadata policies to prevent future secret storage.", + "Url": "https://hub.prowler.com/check/objectstorage_container_metadata_sensitive_data" + } + }, + "Categories": [ + "threat-detection", + "secrets", + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check uses the detect-secrets library to scan for credentials. May produce false positives on metadata keys containing secret-like keywords. Findings should be reviewed manually. The audit_config allows configuring secrets_ignore_patterns to exclude specific patterns and detect_secrets_plugins to customize detection." +} diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_metadata_sensitive_data/objectstorage_container_metadata_sensitive_data.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_metadata_sensitive_data/objectstorage_container_metadata_sensitive_data.py new file mode 100644 index 0000000000..94d281bf32 --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_metadata_sensitive_data/objectstorage_container_metadata_sensitive_data.py @@ -0,0 +1,64 @@ +import json +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.lib.utils.utils import detect_secrets_scan +from prowler.providers.openstack.services.objectstorage.objectstorage_client import ( + objectstorage_client, +) + + +class objectstorage_container_metadata_sensitive_data(Check): + """Ensure object storage container metadata does not contain sensitive data like passwords or API keys.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + secrets_ignore_patterns = objectstorage_client.audit_config.get( + "secrets_ignore_patterns", [] + ) + + for container in objectstorage_client.containers: + report = CheckReportOpenStack(metadata=self.metadata(), resource=container) + report.status = "PASS" + report.status_extended = ( + f"Container {container.name} metadata does not contain sensitive data." + ) + + if container.metadata: + # Build metadata dict and parallel list of keys + dump_metadata = {} + original_metadata_keys = [] + for key, value in container.metadata.items(): + dump_metadata[key] = value + original_metadata_keys.append(key) + + # Convert metadata dict to JSON string for detect-secrets scanning + metadata_json = json.dumps(dump_metadata, indent=2) + detect_secrets_output = detect_secrets_scan( + data=metadata_json, + excluded_secrets=secrets_ignore_patterns, + detect_secrets_plugins=objectstorage_client.audit_config.get( + "detect_secrets_plugins" + ), + ) + + if detect_secrets_output: + # Map line numbers back to metadata keys using the parallel list + # Line numbering: line 1 = "{", line 2 = first key-value, etc. + secrets_string = ", ".join( + [ + f"{secret['type']} in metadata key '{original_metadata_keys[secret['line_number'] - 2]}'" + for secret in detect_secrets_output + if 0 + <= secret["line_number"] - 2 + < len(original_metadata_keys) + ] + ) + report.status = "FAIL" + report.status_extended = f"Container {container.name} metadata contains potential secrets -> {secrets_string}." + else: + report.status_extended = f"Container {container.name} has no metadata (no sensitive data exposure risk)." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_public_read_acl_disabled/__init__.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_public_read_acl_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_public_read_acl_disabled/objectstorage_container_public_read_acl_disabled.metadata.json b/prowler/providers/openstack/services/objectstorage/objectstorage_container_public_read_acl_disabled/objectstorage_container_public_read_acl_disabled.metadata.json new file mode 100644 index 0000000000..f15becbcf5 --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_public_read_acl_disabled/objectstorage_container_public_read_acl_disabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "objectstorage_container_public_read_acl_disabled", + "CheckTitle": "Object storage containers do not grant anonymous read access", + "CheckType": [], + "ServiceName": "objectstorage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "OS::Swift::Container", + "ResourceGroup": "storage", + "Description": "**OpenStack Swift containers** are evaluated to verify that **anonymous read access** is not granted. The `.r:*` ACL allows any unauthenticated user on the internet to read objects in the container. This is a critical exposure that can lead to data leaks, especially when containers hold sensitive information such as backups, logs, or application data.", + "Risk": "Containers with `.r:*` in the read ACL are **publicly accessible** without authentication. Attackers can enumerate and download all objects, leading to **data breaches**, **credential exposure**, and **compliance violations**. Public containers are frequently targeted by automated scanners.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/swift/latest/overview_acl.html", + "https://docs.openstack.org/security-guide/object-storage.html" + ], + "Remediation": { + "Code": { + "CLI": "swift post --read-acl ''", + "NativeIaC": "", + "Other": "1. Navigate to **Object Store > Containers**\n2. Select the container with public read ACL\n3. Edit the container metadata\n4. Remove the `.r:*` entry from the Read ACL\n5. Save changes", + "Terraform": "" + }, + "Recommendation": { + "Text": "Remove `.r:*` from container read ACLs to prevent **anonymous access**. Use **project-scoped ACLs** (e.g., `project-id:*`) or specific user ACLs instead. If public access is required, consider using a **CDN** or **signed URLs** with time-limited tokens.", + "Url": "https://hub.prowler.com/check/objectstorage_container_public_read_acl_disabled" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Swift ACLs use a specific syntax where `.r:*` grants read access to all referrers (anonymous access). The `.r:` prefix indicates referrer-based ACL. This check specifically looks for the wildcard referrer pattern." +} diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_public_read_acl_disabled/objectstorage_container_public_read_acl_disabled.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_public_read_acl_disabled/objectstorage_container_public_read_acl_disabled.py new file mode 100644 index 0000000000..6a3b5c03ec --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_public_read_acl_disabled/objectstorage_container_public_read_acl_disabled.py @@ -0,0 +1,29 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.objectstorage.objectstorage_client import ( + objectstorage_client, +) + + +class objectstorage_container_public_read_acl_disabled(Check): + """Ensure object storage containers do not grant anonymous read access.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for container in objectstorage_client.containers: + report = CheckReportOpenStack(metadata=self.metadata(), resource=container) + acl_entries = [entry.strip() for entry in container.read_ACL.split(",")] + if ".r:*" in acl_entries: + report.status = "FAIL" + report.status_extended = f"Container {container.name} has public read ACL (.r:*) allowing anonymous access." + else: + report.status = "PASS" + report.status_extended = ( + f"Container {container.name} does not have public read ACL." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_sync_not_enabled/__init__.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_sync_not_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_sync_not_enabled/objectstorage_container_sync_not_enabled.metadata.json b/prowler/providers/openstack/services/objectstorage/objectstorage_container_sync_not_enabled/objectstorage_container_sync_not_enabled.metadata.json new file mode 100644 index 0000000000..35925433c5 --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_sync_not_enabled/objectstorage_container_sync_not_enabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "objectstorage_container_sync_not_enabled", + "CheckTitle": "Object storage containers do not have container sync enabled", + "CheckType": [], + "ServiceName": "objectstorage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OS::Swift::Container", + "ResourceGroup": "storage", + "Description": "**OpenStack Swift containers** are evaluated to verify that **container sync** is not configured. Container sync replicates objects to another Swift cluster, potentially outside the organization's control. Unauthorized sync configurations can be used for data exfiltration to external clusters.", + "Risk": "Container sync replicates all objects to a **remote Swift cluster**. If misconfigured or set up by an attacker, sensitive data is continuously **exfiltrated** to an external location. The sync operates automatically and silently, making it difficult to detect ongoing **data theft**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/swift/latest/overview_container_sync.html", + "https://docs.openstack.org/security-guide/object-storage.html" + ], + "Remediation": { + "Code": { + "CLI": "swift post -H 'X-Container-Sync-To:' -H 'X-Container-Sync-Key:'", + "NativeIaC": "", + "Other": "1. Identify containers with sync enabled\n2. Remove sync configuration using CLI\n3. Verify sync is disabled: `swift stat `\n4. Review sync logs for unauthorized data transfers\n5. Investigate who configured the sync and when", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable container sync unless explicitly required for **disaster recovery**. If sync is needed, ensure the target cluster is within your organization's **trust boundary**. Monitor sync configurations for unauthorized changes. Implement **RBAC policies** to restrict who can configure container sync.", + "Url": "https://hub.prowler.com/check/objectstorage_container_sync_not_enabled" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Container sync is configured via X-Container-Sync-To (target URL) and X-Container-Sync-Key (shared secret) headers. The sync middleware must be enabled in the Swift pipeline for sync to function. This check flags any container with a non-empty sync_to value." +} diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_sync_not_enabled/objectstorage_container_sync_not_enabled.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_sync_not_enabled/objectstorage_container_sync_not_enabled.py new file mode 100644 index 0000000000..ac7545e69e --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_sync_not_enabled/objectstorage_container_sync_not_enabled.py @@ -0,0 +1,28 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.objectstorage.objectstorage_client import ( + objectstorage_client, +) + + +class objectstorage_container_sync_not_enabled(Check): + """Ensure object storage containers do not have container sync configured.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for container in objectstorage_client.containers: + report = CheckReportOpenStack(metadata=self.metadata(), resource=container) + if container.sync_to: + report.status = "FAIL" + report.status_extended = f"Container {container.name} has container sync enabled (sync target: {container.sync_to})." + else: + report.status = "PASS" + report.status_extended = ( + f"Container {container.name} does not have container sync enabled." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_versioning_enabled/__init__.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_versioning_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_versioning_enabled/objectstorage_container_versioning_enabled.metadata.json b/prowler/providers/openstack/services/objectstorage/objectstorage_container_versioning_enabled/objectstorage_container_versioning_enabled.metadata.json new file mode 100644 index 0000000000..568e7c252f --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_versioning_enabled/objectstorage_container_versioning_enabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "objectstorage_container_versioning_enabled", + "CheckTitle": "Object storage containers have versioning enabled", + "CheckType": [], + "ServiceName": "objectstorage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OS::Swift::Container", + "ResourceGroup": "storage", + "Description": "**OpenStack Swift containers** are evaluated to verify that **object versioning** is enabled. Versioning preserves previous versions of objects when they are overwritten or deleted, enabling recovery from accidental modifications, ransomware attacks, and data corruption.", + "Risk": "Without versioning, overwritten or deleted objects **cannot be recovered**. Accidental deletions, **ransomware** encrypting objects, or malicious modifications result in **permanent data loss**. Versioning provides a safety net for forensic investigation and disaster recovery.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/swift/latest/overview_object_versioning.html", + "https://docs.openstack.org/security-guide/object-storage.html" + ], + "Remediation": { + "Code": { + "CLI": "swift post -H 'X-Versions-Location: '", + "NativeIaC": "", + "Other": "1. Create a versions container: `swift post _versions`\n2. Enable versioning (stack mode): `swift post -H 'X-Versions-Location: _versions'`\n Or enable versioning (history mode): `swift post -H 'X-History-Location: _versions'`\n3. Verify: `swift stat ` shows Versions header", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable **object versioning** on all containers storing important data. Create a dedicated versions container and set the `X-Versions-Location` (stack mode) or `X-History-Location` (history mode) header. Implement **lifecycle policies** to manage version retention and storage costs. Combine with container backups for comprehensive data protection.", + "Url": "https://hub.prowler.com/check/objectstorage_container_versioning_enabled" + } + }, + "Categories": [ + "forensics-ready" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Swift supports two versioning modes: X-Versions-Location (stack mode, stores versions in a separate container) and X-History-Location (history mode). This check verifies that either versions_location or history_location is set, indicating versioning is configured." +} diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_versioning_enabled/objectstorage_container_versioning_enabled.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_versioning_enabled/objectstorage_container_versioning_enabled.py new file mode 100644 index 0000000000..f2e811093f --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_versioning_enabled/objectstorage_container_versioning_enabled.py @@ -0,0 +1,30 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.objectstorage.objectstorage_client import ( + objectstorage_client, +) + + +class objectstorage_container_versioning_enabled(Check): + """Ensure object storage containers have versioning enabled for data protection.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for container in objectstorage_client.containers: + report = CheckReportOpenStack(metadata=self.metadata(), resource=container) + if container.versioning_enabled: + report.status = "PASS" + location = container.versions_location or container.history_location + mode = "versions" if container.versions_location else "history" + report.status_extended = f"Container {container.name} has versioning enabled ({mode} location: {location})." + else: + report.status = "FAIL" + report.status_extended = ( + f"Container {container.name} does not have versioning enabled." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_write_acl_restricted/__init__.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_write_acl_restricted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_write_acl_restricted/objectstorage_container_write_acl_restricted.metadata.json b/prowler/providers/openstack/services/objectstorage/objectstorage_container_write_acl_restricted/objectstorage_container_write_acl_restricted.metadata.json new file mode 100644 index 0000000000..fa2af1334a --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_write_acl_restricted/objectstorage_container_write_acl_restricted.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "openstack", + "CheckID": "objectstorage_container_write_acl_restricted", + "CheckTitle": "Object storage container write ACL is restricted", + "CheckType": [], + "ServiceName": "objectstorage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "OS::Swift::Container", + "ResourceGroup": "storage", + "Description": "**OpenStack Swift containers** are evaluated to verify that **write access** is restricted. The `*:*` write ACL allows any authenticated user from any project to create, modify, or delete objects. The `*` write ACL similarly grants unrestricted write access. Both configurations violate the principle of least privilege.", + "Risk": "Containers with **unrestricted write ACLs** allow any authenticated OpenStack user to upload, overwrite, or delete objects. This enables **data tampering**, **malware injection**, **ransomware attacks** (encrypting objects), and **resource abuse** (cryptomining payloads). Compromised credentials from any project can be used to modify data.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/swift/latest/overview_acl.html", + "https://docs.openstack.org/security-guide/object-storage.html" + ], + "Remediation": { + "Code": { + "CLI": "swift post --write-acl ':'", + "NativeIaC": "", + "Other": "1. Navigate to **Object Store > Containers**\n2. Select the container with unrestricted write ACL\n3. Edit the container metadata\n4. Replace `*:*` or `*` with specific `project-id:user-id` entries\n5. Save changes", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict write ACLs to specific project and user combinations (e.g., `project-id:user-id`). Never use `*:*` or `*` as write ACLs. Implement **RBAC policies** to control write access. Regularly audit container ACLs to detect **overly permissive configurations**.", + "Url": "https://hub.prowler.com/check/objectstorage_container_write_acl_restricted" + } + }, + "Categories": [ + "internet-exposed", + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "The `*:*` ACL means 'any user in any project' while `*` alone also grants unrestricted access. Both are dangerous for write operations as they allow data modification by any authenticated user in the OpenStack deployment." +} diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_container_write_acl_restricted/objectstorage_container_write_acl_restricted.py b/prowler/providers/openstack/services/objectstorage/objectstorage_container_write_acl_restricted/objectstorage_container_write_acl_restricted.py new file mode 100644 index 0000000000..0693ebedce --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_container_write_acl_restricted/objectstorage_container_write_acl_restricted.py @@ -0,0 +1,29 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.objectstorage.objectstorage_client import ( + objectstorage_client, +) + + +class objectstorage_container_write_acl_restricted(Check): + """Ensure object storage container write ACL does not allow all authenticated users.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for container in objectstorage_client.containers: + report = CheckReportOpenStack(metadata=self.metadata(), resource=container) + acl_entries = [entry.strip() for entry in container.write_ACL.split(",")] + if "*:*" in acl_entries or "*" in acl_entries: + report.status = "FAIL" + report.status_extended = f"Container {container.name} has unrestricted write ACL allowing all authenticated users to write." + else: + report.status = "PASS" + report.status_extended = ( + f"Container {container.name} has restricted write ACL." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/objectstorage/objectstorage_service.py b/prowler/providers/openstack/services/objectstorage/objectstorage_service.py new file mode 100644 index 0000000000..6a2a73d5a0 --- /dev/null +++ b/prowler/providers/openstack/services/objectstorage/objectstorage_service.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, List + +from openstack import exceptions as openstack_exceptions + +from prowler.lib.logger import logger +from prowler.providers.openstack.lib.service.service import OpenStackService + + +class ObjectStorage(OpenStackService): + """Service wrapper using openstacksdk object-store APIs.""" + + def __init__(self, provider) -> None: + super().__init__(__class__.__name__, provider) + self.containers: List[ObjectStorageContainer] = [] + self._list_containers() + + def _list_containers(self) -> None: + """List all object storage containers across all audited regions.""" + logger.info("ObjectStorage - Listing containers...") + for region, conn in self.regional_connections.items(): + try: + for container in conn.object_store.containers(): + # The list API only returns name/count/bytes; HEAD each + # container to retrieve ACLs, metadata, and versioning info. + try: + detail = conn.object_store.get_container_metadata( + getattr(container, "name", "") + ) + except Exception as head_error: + logger.warning( + f"Could not HEAD container {getattr(container, 'name', '')}: {head_error}" + ) + detail = container + + metadata = getattr(detail, "metadata", None) or {} + + # Extract versioning info (Swift supports two modes) + versions_location = getattr(detail, "versions_location", "") or "" + history_location = getattr(detail, "history_location", "") or "" + versioning_enabled = bool(versions_location or history_location) + + self.containers.append( + ObjectStorageContainer( + id=getattr(container, "name", ""), + name=getattr(container, "name", ""), + region=region, + project_id=self.project_id, + object_count=getattr(detail, "count", 0), + bytes_used=getattr(detail, "bytes", 0), + read_ACL=getattr(detail, "read_ACL", "") or "", + write_ACL=getattr(detail, "write_ACL", "") or "", + versioning_enabled=versioning_enabled, + versions_location=versions_location, + history_location=history_location, + sync_to=getattr(detail, "sync_to", "") or "", + sync_key=getattr(detail, "sync_key", "") or "", + metadata=metadata if isinstance(metadata, dict) else {}, + ) + ) + except openstack_exceptions.SDKException as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- " + f"Failed to list object storage containers in region {region}: {error}" + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- " + f"Unexpected error listing object storage containers in region {region}: {error}" + ) + + +@dataclass +class ObjectStorageContainer: + """Represents an OpenStack Swift container.""" + + id: str + name: str + region: str + project_id: str + object_count: int + bytes_used: int + read_ACL: str + write_ACL: str + versioning_enabled: bool + versions_location: str + history_location: str + sync_to: str + sync_key: str + metadata: Dict[str, str] 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 35fb9341d3..dcbb2d406b 100644 --- a/skills/prowler-attack-paths-query/SKILL.md +++ b/skills/prowler-attack-paths-query/SKILL.md @@ -7,7 +7,7 @@ description: > license: Apache-2.0 metadata: author: prowler-cloud - version: "1.0" + version: "1.1" scope: [root, api] auto_invoke: - "Creating Attack Paths queries" @@ -80,7 +80,16 @@ api/src/backend/api/attack_paths/queries/{provider}.py Example: `api/src/backend/api/attack_paths/queries/aws.py` -### Query Definition Pattern +### Query parameters for provider scoping + +Two parameters exist. Both are injected automatically by the query runner. + +| Parameter | Property it matches | Used on | Purpose | +| --------------- | ------------------- | -------------- | ------------------------------------ | +| `$provider_uid` | `id` | `AWSAccount` | Scopes to a specific AWS account | +| `$provider_id` | `_provider_id` | Any other node | Scopes nodes to the provider context | + +### Privilege Escalation Query Pattern ```python from api.attack_paths.queries.types import ( @@ -88,7 +97,6 @@ from api.attack_paths.queries.types import ( AttackPathsQueryDefinition, AttackPathsQueryParameterDefinition, ) -from tasks.jobs.attack_paths.config import PROWLER_FINDING_LABEL # {REFERENCE_ID} (e.g., EC2-001, GLUE-001) AWS_{QUERY_NAME} = AttackPathsQueryDefinition( @@ -129,7 +137,7 @@ AWS_{QUERY_NAME} = AttackPathsQueryDefinition( ) UNWIND nodes(path_principal) + nodes(path_target) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + 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 @@ -138,6 +146,36 @@ AWS_{QUERY_NAME} = AttackPathsQueryDefinition( ) ``` +### Network Exposure Query Pattern + +```python +AWS_{QUERY_NAME} = AttackPathsQueryDefinition( + id="aws-{kebab-case-name}", + name="{Human-friendly label}", + short_description="{Brief explanation.}", + description="{Detailed description.}", + provider="aws", + cypher=f""" + // 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 + """, + parameters=[], +) +``` + ### Register in Query List Add to the `{PROVIDER}_QUERIES` list at the bottom of the file: @@ -214,11 +252,12 @@ 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`. @@ -234,7 +273,7 @@ This informs query design by showing what data is actually available to query. ### 4. Create Query Definition -Use the standard pattern (see above) with: +Use the appropriate pattern (privilege escalation or network exposure) with: - **id**: Auto-generated as `{provider}-{kebab-case-description}` - **name**: Short, human-friendly label. No raw IAM permissions. For sourced queries (e.g., pathfinding.cloud), append the reference ID in parentheses: `"EC2 Instance Launch with Privileged Role (EC2-001)"`. If the name already has parentheses, prepend the ID inside them: `"ECS Service Creation with Privileged Role (ECS-003 - Existing Cluster)"`. @@ -263,7 +302,7 @@ Examples: - `aws-ec2-privesc-passrole-iam` - `aws-iam-privesc-attach-role-policy-assume-role` -- `aws-rds-unencrypted-storage` +- `aws-ec2-instances-internet-exposed` ### Query Constant Name @@ -275,7 +314,7 @@ Examples: - `AWS_EC2_PRIVESC_PASSROLE_IAM` - `AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY_ASSUME_ROLE` -- `AWS_RDS_UNENCRYPTED_STORAGE` +- `AWS_EC2_INSTANCES_INTERNET_EXPOSED` --- @@ -325,46 +364,91 @@ WHERE any(resource IN stmt.resource WHERE ) ``` +### 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 is a real graph relationship linking 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) +``` + ### Include Prowler Findings ```cypher UNWIND nodes(path_principal) + nodes(path_target) as n -OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {status: 'FAIL', provider_uid: $provider_uid}) +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 ``` +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 +``` + --- ## Common Node Labels by Provider ### AWS -| Label | Description | -| -------------------- | ----------------------------------- | -| `AWSAccount` | AWS account root | -| `AWSPrincipal` | IAM principal (user, role, service) | -| `AWSRole` | IAM role | -| `AWSUser` | IAM user | -| `AWSPolicy` | IAM policy | -| `AWSPolicyStatement` | Policy statement | -| `EC2Instance` | EC2 instance | -| `EC2SecurityGroup` | Security group | -| `S3Bucket` | S3 bucket | -| `RDSInstance` | RDS database instance | -| `LoadBalancer` | Classic ELB | -| `LoadBalancerV2` | ALB/NLB | -| `LaunchTemplate` | EC2 launch template | +| Label | Description | +| --------------------- | --------------------------------------- | +| `AWSAccount` | AWS account root | +| `AWSPrincipal` | IAM principal (user, role, service) | +| `AWSRole` | IAM role | +| `AWSUser` | IAM user | +| `AWSPolicy` | IAM policy | +| `AWSPolicyStatement` | Policy statement | +| `AWSTag` | Resource tag (key/value) | +| `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 | +| `Internet` | Internet sentinel node (`_provider_id`) | ### Common Relationships -| Relationship | Description | -| ---------------------- | ----------------------- | -| `TRUSTS_AWS_PRINCIPAL` | Role trust relationship | -| `STS_ASSUMEROLE_ALLOW` | Can assume role | -| `POLICY` | Has policy attached | -| `STATEMENT` | Policy has statement | +| Relationship | Description | +| ---------------------- | ---------------------------------- | +| `TRUSTS_AWS_PRINCIPAL` | Role trust relationship | +| `STS_ASSUMEROLE_ALLOW` | Can assume role | +| `CAN_ACCESS` | Internet-to-resource exposure link | +| `POLICY` | Has policy attached | +| `STATEMENT` | Policy has statement | --- @@ -393,7 +477,7 @@ parameters=[ ## Best Practices -1. **Always filter by provider_uid**: Use `{id: $provider_uid}` on account nodes and `{provider_uid: $provider_uid}` on ProwlerFinding nodes +1. **Always scope by provider**: Use `{id: $provider_uid}` on `AWSAccount` nodes. Use `{_provider_id: $provider_id}` on any other node that needs provider scoping (e.g., `Internet`). 2. **Use consistent naming**: Follow existing patterns in the file @@ -415,6 +499,8 @@ parameters=[ MATCH (aws)--(role:AWSRole) WHERE role.name = 'admin' ``` + The `Internet` node is an exception: it uses `OPTIONAL MATCH` with `_provider_id` for scoping instead of chaining from `aws`. + --- ## openCypher Compatibility @@ -425,23 +511,14 @@ Queries must be written in **openCypher Version 9** to ensure compatibility with ### Avoid These (Not in openCypher spec) -| Feature | Reason | -| --------------------------------------------------- | ----------------------------------------------- | -| APOC procedures (`apoc.*`) | Neo4j-specific plugin, not available in Neptune | -| Virtual nodes (`apoc.create.vNode`) | APOC-specific | -| Virtual relationships (`apoc.create.vRelationship`) | APOC-specific | -| Neptune extensions | Not available in Neo4j | -| `reduce()` function | Use `UNWIND` + aggregation instead | -| `FOREACH` clause | Use `WITH` + `UNWIND` + `SET` instead | -| Regex match operator (`=~`) | Not supported in Neptune | - -### CALL Subqueries - -Supported with limitations: - -- Use `WITH` clause to import variables: `CALL { WITH var ... }` -- Updates inside CALL subqueries are NOT supported -- Emitted variables cannot overlap with variables before the CALL +| Feature | Reason | Use instead | +| -------------------------- | ----------------------------------------------- | ------------------------------------------------------ | +| APOC procedures (`apoc.*`) | Neo4j-specific plugin, not available in Neptune | 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 section) | --- @@ -451,7 +528,7 @@ Supported with limitations: - **Repository**: https://github.com/DataDog/pathfinding.cloud - **All paths JSON**: `https://raw.githubusercontent.com/DataDog/pathfinding.cloud/main/docs/paths.json` -- Use WebFetch to query specific paths or list available services +- Always use Bash with `curl | jq` to fetch paths (WebFetch truncates the large JSON) ### Cartography Schema @@ -461,7 +538,6 @@ Supported with limitations: ### openCypher Specification - **Neptune openCypher compliance** (what Neptune supports): https://docs.aws.amazon.com/neptune/latest/userguide/feature-opencypher-compliance.html -- **Rewriting Cypher for Neptune** (converting Neo4j-specific syntax): https://docs.aws.amazon.com/neptune/latest/userguide/migration-opencypher-rewrites.html - **openCypher project** (spec, grammar, TCK): https://github.com/opencypher/openCypher --- @@ -485,13 +561,4 @@ Use the existing queries to learn: - How to include Prowler findings - Comment style -> **Compatibility Warning**: Some existing queries use Neo4j-specific features -> (e.g., `apoc.create.vNode`, `apoc.create.vRelationship`, regex `=~`) that are -> **NOT compatible** with Amazon Neptune. Use these queries to learn general -> patterns (structure, naming, Prowler findings integration, comment style) but -> **DO NOT copy APOC procedures or other Neo4j-specific syntax** into new queries. -> New queries must be pure openCypher Version 9. Refer to the -> [openCypher Compatibility](#opencypher-compatibility) section for the full list -> of features to avoid. - -**DO NOT** use generic templates. Match the exact style of existing **compatible** queries in the file. +**DO NOT** use generic templates. Match the exact style of existing queries in the file. diff --git a/tests/config/config_test.py b/tests/config/config_test.py index f67f3fda35..08b1f7d5e0 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -31,6 +31,7 @@ old_config_aws = { "ec2_allowed_interface_types": ["api_gateway_managed", "vpc_endpoint"], "ec2_allowed_instance_owners": ["amazon-elb"], "trusted_account_ids": [], + "trusted_ips": [], "log_group_retention_days": 365, "max_idle_disconnect_timeout_in_seconds": 600, "max_disconnect_timeout_in_seconds": 300, @@ -95,6 +96,7 @@ config_aws = { "fargate_linux_latest_version": "1.4.0", "fargate_windows_latest_version": "1.0.0", "trusted_account_ids": [], + "trusted_ips": [], "log_group_retention_days": 365, "max_idle_disconnect_timeout_in_seconds": 600, "max_disconnect_timeout_in_seconds": 300, diff --git a/tests/config/fixtures/config.yaml b/tests/config/fixtures/config.yaml index 44e826d382..1b63e2387f 100644 --- a/tests/config/fixtures/config.yaml +++ b/tests/config/fixtures/config.yaml @@ -72,6 +72,11 @@ aws: # trusted_account_ids : ["123456789012", "098765432109", "678901234567"] trusted_account_ids: [] + # AWS OpenSearch Configuration (opensearch_service_domains_not_publicly_accessible) + # Trusted IP addresses or CIDR ranges that should not be considered as public access, e.g. + # trusted_ips: ["1.2.3.4", "10.0.0.0/8"] + trusted_ips: [] + # AWS Cloudwatch Configuration # aws.cloudwatch_log_group_retention_policy_specific_days_enabled --> by default is 365 days log_group_retention_days: 365 diff --git a/tests/config/fixtures/config_old.yaml b/tests/config/fixtures/config_old.yaml index cbd3bf4fa0..33220e1246 100644 --- a/tests/config/fixtures/config_old.yaml +++ b/tests/config/fixtures/config_old.yaml @@ -25,6 +25,11 @@ ec2_allowed_instance_owners: # trusted_account_ids : ["123456789012", "098765432109", "678901234567"] trusted_account_ids: [] +# AWS OpenSearch Configuration (opensearch_service_domains_not_publicly_accessible) +# Trusted IP addresses or CIDR ranges that should not be considered as public access, e.g. +# trusted_ips: ["1.2.3.4", "10.0.0.0/8"] +trusted_ips: [] + # AWS Cloudwatch Configuration # aws.cloudwatch_log_group_retention_policy_specific_days_enabled --> by default is 365 days log_group_retention_days: 365 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/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions_test.py b/tests/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions_test.py new file mode 100644 index 0000000000..d171aa2473 --- /dev/null +++ b/tests/providers/aws/services/guardduty/guardduty_delegated_admin_enabled_all_regions/guardduty_delegated_admin_enabled_all_regions_test.py @@ -0,0 +1,233 @@ +from unittest.mock import patch + +import botocore +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + +orig = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call_org_admin_and_config(self, operation_name, api_params): + """Mock organization admin accounts and configuration APIs.""" + if operation_name == "ListOrganizationAdminAccounts": + return { + "AdminAccounts": [ + { + "AdminAccountId": "123456789012", + "AdminStatus": "ENABLED", + } + ] + } + if operation_name == "DescribeOrganizationConfiguration": + return { + "AutoEnableOrganizationMembers": "ALL", + } + return orig(self, operation_name, api_params) + + +def mock_make_api_call_org_admin_no_auto_enable(self, operation_name, api_params): + """Mock organization admin configured but auto-enable disabled.""" + if operation_name == "ListOrganizationAdminAccounts": + return { + "AdminAccounts": [ + { + "AdminAccountId": "123456789012", + "AdminStatus": "ENABLED", + } + ] + } + if operation_name == "DescribeOrganizationConfiguration": + return { + "AutoEnableOrganizationMembers": "NONE", + } + return orig(self, operation_name, api_params) + + +def mock_make_api_call_no_org_admin(self, operation_name, api_params): + """Mock no organization admin configured.""" + if operation_name == "ListOrganizationAdminAccounts": + return {"AdminAccounts": []} + if operation_name == "DescribeOrganizationConfiguration": + return { + "AutoEnableOrganizationMembers": "NONE", + } + return orig(self, operation_name, api_params) + + +class Test_guardduty_delegated_admin_enabled_all_regions: + @mock_aws + def test_no_detectors(self): + """Test when no GuardDuty detectors exist.""" + aws_provider = set_mocked_aws_provider() + + from prowler.providers.aws.services.guardduty.guardduty_service import GuardDuty + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + patch( + "prowler.providers.aws.services.guardduty.guardduty_delegated_admin_enabled_all_regions.guardduty_delegated_admin_enabled_all_regions.guardduty_client", + new=GuardDuty(aws_provider), + ), + ): + from prowler.providers.aws.services.guardduty.guardduty_delegated_admin_enabled_all_regions.guardduty_delegated_admin_enabled_all_regions import ( + guardduty_delegated_admin_enabled_all_regions, + ) + + check = guardduty_delegated_admin_enabled_all_regions() + result = check.execute() + + # Should have findings for each region (with unknown detectors) + assert len(result) > 0 + # All should fail since no detectors are enabled + for finding in result: + assert finding.status == "FAIL" + assert "detector not enabled" in finding.status_extended + + @patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_no_org_admin, + ) + @mock_aws + def test_detector_enabled_no_delegated_admin(self): + """Test when detector is enabled but no delegated admin is configured.""" + guardduty_client_boto = client("guardduty", region_name=AWS_REGION_EU_WEST_1) + detector_id = guardduty_client_boto.create_detector(Enable=True)["DetectorId"] + + aws_provider = set_mocked_aws_provider() + + from prowler.providers.aws.services.guardduty.guardduty_service import GuardDuty + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + patch( + "prowler.providers.aws.services.guardduty.guardduty_delegated_admin_enabled_all_regions.guardduty_delegated_admin_enabled_all_regions.guardduty_client", + new=GuardDuty(aws_provider), + ), + ): + from prowler.providers.aws.services.guardduty.guardduty_delegated_admin_enabled_all_regions.guardduty_delegated_admin_enabled_all_regions import ( + guardduty_delegated_admin_enabled_all_regions, + ) + + check = guardduty_delegated_admin_enabled_all_regions() + result = check.execute() + + # Find the result for our region + eu_west_1_result = None + for finding in result: + if finding.region == AWS_REGION_EU_WEST_1: + eu_west_1_result = finding + break + + assert eu_west_1_result is not None + assert eu_west_1_result.status == "FAIL" + assert ( + "no delegated administrator configured" + in eu_west_1_result.status_extended + ) + assert eu_west_1_result.resource_id == detector_id + + @patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_org_admin_no_auto_enable, + ) + @mock_aws + def test_detector_enabled_with_admin_no_auto_enable(self): + """Test when detector is enabled with delegated admin but auto-enable is off.""" + guardduty_client_boto = client("guardduty", region_name=AWS_REGION_EU_WEST_1) + detector_id = guardduty_client_boto.create_detector(Enable=True)["DetectorId"] + + aws_provider = set_mocked_aws_provider() + + from prowler.providers.aws.services.guardduty.guardduty_service import GuardDuty + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + patch( + "prowler.providers.aws.services.guardduty.guardduty_delegated_admin_enabled_all_regions.guardduty_delegated_admin_enabled_all_regions.guardduty_client", + new=GuardDuty(aws_provider), + ), + ): + from prowler.providers.aws.services.guardduty.guardduty_delegated_admin_enabled_all_regions.guardduty_delegated_admin_enabled_all_regions import ( + guardduty_delegated_admin_enabled_all_regions, + ) + + check = guardduty_delegated_admin_enabled_all_regions() + result = check.execute() + + # Find the result for our region + eu_west_1_result = None + for finding in result: + if finding.region == AWS_REGION_EU_WEST_1: + eu_west_1_result = finding + break + + assert eu_west_1_result is not None + assert eu_west_1_result.status == "FAIL" + assert ( + "organization auto-enable not configured" + in eu_west_1_result.status_extended + ) + assert eu_west_1_result.resource_id == detector_id + + @patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_org_admin_and_config, + ) + @mock_aws + def test_detector_enabled_with_admin_and_auto_enable(self): + """Test when detector is enabled with delegated admin and auto-enable is on (PASS).""" + guardduty_client_boto = client("guardduty", region_name=AWS_REGION_EU_WEST_1) + detector_id = guardduty_client_boto.create_detector(Enable=True)["DetectorId"] + + aws_provider = set_mocked_aws_provider() + + from prowler.providers.aws.services.guardduty.guardduty_service import GuardDuty + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + patch( + "prowler.providers.aws.services.guardduty.guardduty_delegated_admin_enabled_all_regions.guardduty_delegated_admin_enabled_all_regions.guardduty_client", + new=GuardDuty(aws_provider), + ), + ): + from prowler.providers.aws.services.guardduty.guardduty_delegated_admin_enabled_all_regions.guardduty_delegated_admin_enabled_all_regions import ( + guardduty_delegated_admin_enabled_all_regions, + ) + + check = guardduty_delegated_admin_enabled_all_regions() + result = check.execute() + + # Find the result for our region + eu_west_1_result = None + for finding in result: + if finding.region == AWS_REGION_EU_WEST_1: + eu_west_1_result = finding + break + + assert eu_west_1_result is not None + assert eu_west_1_result.status == "PASS" + assert "delegated admin configured" in eu_west_1_result.status_extended + assert "auto-enable active" in eu_west_1_result.status_extended + assert eu_west_1_result.resource_id == detector_id + assert ( + eu_west_1_result.resource_arn + == f"arn:aws:guardduty:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:detector/{detector_id}" + ) diff --git a/tests/providers/aws/services/iam/lib/policy_test.py b/tests/providers/aws/services/iam/lib/policy_test.py index fe25b4ca0f..afea8ca658 100644 --- a/tests/providers/aws/services/iam/lib/policy_test.py +++ b/tests/providers/aws/services/iam/lib/policy_test.py @@ -13,6 +13,7 @@ from prowler.providers.aws.services.iam.lib.policy import ( is_condition_block_restrictive_organization, is_condition_block_restrictive_sns_endpoint, is_condition_restricting_from_private_ip, + is_condition_restricting_to_trusted_ips, is_policy_public, ) @@ -1982,6 +1983,49 @@ class Test_Policy: } assert not is_condition_restricting_from_private_ip(condition_from_invalid_ip) + def test_is_condition_restricting_to_trusted_ips_no_trusted_ips(self): + condition = {"IpAddress": {"aws:SourceIp": "1.2.3.4"}} + assert not is_condition_restricting_to_trusted_ips(condition) + + def test_is_condition_restricting_to_trusted_ips_empty_trusted_ips(self): + condition = {"IpAddress": {"aws:SourceIp": "1.2.3.4"}} + assert not is_condition_restricting_to_trusted_ips(condition, []) + + def test_is_condition_restricting_to_trusted_ips_matching(self): + condition = {"IpAddress": {"aws:SourceIp": "1.2.3.4"}} + assert is_condition_restricting_to_trusted_ips(condition, ["1.2.3.4"]) + + def test_is_condition_restricting_to_trusted_ips_not_matching(self): + condition = {"IpAddress": {"aws:SourceIp": "5.6.7.8"}} + assert not is_condition_restricting_to_trusted_ips(condition, ["1.2.3.4"]) + + def test_is_condition_restricting_to_trusted_ips_wildcard(self): + condition = {"IpAddress": {"aws:SourceIp": "*"}} + assert not is_condition_restricting_to_trusted_ips(condition, ["1.2.3.4"]) + + def test_is_condition_restricting_to_trusted_ips_open_cidr(self): + condition = {"IpAddress": {"aws:SourceIp": "0.0.0.0/0"}} + assert not is_condition_restricting_to_trusted_ips(condition, ["1.2.3.4"]) + + def test_is_condition_restricting_to_trusted_ips_multiple_ips_all_trusted(self): + condition = {"IpAddress": {"aws:SourceIp": ["1.2.3.4", "5.6.7.8"]}} + assert is_condition_restricting_to_trusted_ips( + condition, ["1.2.3.4", "5.6.7.8"] + ) + + def test_is_condition_restricting_to_trusted_ips_multiple_ips_partial_trusted(self): + condition = {"IpAddress": {"aws:SourceIp": ["1.2.3.4", "9.9.9.9"]}} + assert not is_condition_restricting_to_trusted_ips( + condition, ["1.2.3.4", "5.6.7.8"] + ) + + def test_is_condition_restricting_to_trusted_ips_cidr_range(self): + condition = {"IpAddress": {"aws:SourceIp": "10.0.0.0/8"}} + assert is_condition_restricting_to_trusted_ips(condition, ["10.0.0.0/8"]) + + def test_is_condition_restricting_to_trusted_ips_no_condition(self): + assert not is_condition_restricting_to_trusted_ips({}, ["1.2.3.4"]) + def test_is_policy_public_(self): policy = { "Statement": [ @@ -2271,6 +2315,48 @@ class Test_Policy: } assert is_policy_public(policy, TRUSTED_AWS_ACCOUNT_NUMBER) + def test_is_policy_public_with_trusted_ips(self): + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": ["*"], + "Condition": { + "IpAddress": {"aws:SourceIp": ["1.2.3.4", "5.6.7.8"]} + }, + "Resource": "*", + } + ], + } + assert not is_policy_public( + policy, + TRUSTED_AWS_ACCOUNT_NUMBER, + trusted_ips=["1.2.3.4", "5.6.7.8"], + ) + + def test_is_policy_public_with_trusted_ips_partial_match(self): + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": ["*"], + "Condition": { + "IpAddress": {"aws:SourceIp": ["1.2.3.4", "9.9.9.9"]} + }, + "Resource": "*", + } + ], + } + assert is_policy_public( + policy, + TRUSTED_AWS_ACCOUNT_NUMBER, + trusted_ips=["1.2.3.4", "5.6.7.8"], + ) + def test_check_admin_access(self): policy = { "Version": "2012-10-17", diff --git a/tests/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible_test.py b/tests/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible_test.py index cdbb87c1a8..8003e9a1b7 100644 --- a/tests/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible_test.py +++ b/tests/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible_test.py @@ -74,6 +74,19 @@ policy_data_source_whole_internet = { ], } +policy_data_trusted_ip = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": ["es:ESHttp*"], + "Condition": {"IpAddress": {"aws:SourceIp": ["1.2.3.4", "5.6.7.8"]}}, + "Resource": f"arn:aws:es:us-west-2:{AWS_ACCOUNT_NUMBER}:domain/{domain_name}/*", + } + ], +} + class Test_opensearch_service_domains_not_publicly_accessible: @mock_aws @@ -304,3 +317,87 @@ class Test_opensearch_service_domains_not_publicly_accessible: assert result[0].resource_arn == domain_arn assert result[0].region == AWS_REGION_US_WEST_2 assert result[0].resource_tags == [] + + @mock_aws + def test_policy_data_not_restricted_with_trusted_ips(self): + opensearch_client = client("opensearch", region_name=AWS_REGION_US_WEST_2) + domain_arn = opensearch_client.create_domain( + DomainName=domain_name, + AccessPolicies=dumps(policy_data_trusted_ip), + )["DomainStatus"]["ARN"] + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_WEST_2]) + aws_provider._audit_config = {"trusted_ips": ["1.2.3.4", "5.6.7.8"]} + + from prowler.providers.aws.services.opensearch.opensearch_service import ( + OpenSearchService, + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible.opensearch_client", + new=OpenSearchService(aws_provider), + ), + ): + from prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible import ( + opensearch_service_domains_not_publicly_accessible, + ) + + check = opensearch_service_domains_not_publicly_accessible() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Opensearch domain {domain_name} is not publicly accessible." + ) + assert result[0].resource_id == domain_name + assert result[0].resource_arn == domain_arn + assert result[0].region == AWS_REGION_US_WEST_2 + assert result[0].resource_tags == [] + + @mock_aws + def test_policy_data_not_restricted_with_trusted_ips_partial_match(self): + opensearch_client = client("opensearch", region_name=AWS_REGION_US_WEST_2) + domain_arn = opensearch_client.create_domain( + DomainName=domain_name, + AccessPolicies=dumps(policy_data_trusted_ip), + )["DomainStatus"]["ARN"] + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_WEST_2]) + aws_provider._audit_config = {"trusted_ips": ["1.2.3.4"]} + + from prowler.providers.aws.services.opensearch.opensearch_service import ( + OpenSearchService, + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible.opensearch_client", + new=OpenSearchService(aws_provider), + ), + ): + from prowler.providers.aws.services.opensearch.opensearch_service_domains_not_publicly_accessible.opensearch_service_domains_not_publicly_accessible import ( + opensearch_service_domains_not_publicly_accessible, + ) + + check = opensearch_service_domains_not_publicly_accessible() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Opensearch domain {domain_name} is publicly accessible via access policy." + ) + assert result[0].resource_id == domain_name + assert result[0].resource_arn == domain_arn + assert result[0].region == AWS_REGION_US_WEST_2 + assert result[0].resource_tags == [] 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/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated_test.py b/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated_test.py index 84daaa758c..47ef92551b 100644 --- a/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated_test.py +++ b/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_connection_activated/mysql_flexible_server_audit_log_connection_activated_test.py @@ -56,6 +56,57 @@ class Test_mysql_flexible_server_audit_log_connection_activated: result = check.execute() assert len(result) == 0 + def test_mysql_audit_log_connection_activated_lowercase(self): + server_name = str(uuid4()) + mysql_client = mock.MagicMock + mysql_client.flexible_servers = { + AZURE_SUBSCRIPTION_ID: { + "/subscriptions/resource_id": FlexibleServer( + resource_id="/subscriptions/resource_id", + name=server_name, + location="location", + version="version", + configurations={ + "audit_log_events": Configuration( + resource_id=f"/subscriptions/{server_name}/configurations/audit_log_events", + description="description", + value="connection", + ) + }, + ) + } + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.mysql.mysql_flexible_server_audit_log_connection_activated.mysql_flexible_server_audit_log_connection_activated.mysql_client", + new=mysql_client, + ), + ): + from prowler.providers.azure.services.mysql.mysql_flexible_server_audit_log_connection_activated.mysql_flexible_server_audit_log_connection_activated import ( + mysql_flexible_server_audit_log_connection_activated, + ) + + check = mysql_flexible_server_audit_log_connection_activated() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == server_name + assert result[0].location == "location" + assert ( + result[0].resource_id + == f"/subscriptions/{server_name}/configurations/audit_log_events" + ) + assert ( + result[0].status_extended + == f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + ) + def test_mysql_audit_log_connection_not_connection(self): server_name = str(uuid4()) mysql_client = mock.MagicMock diff --git a/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled_test.py b/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled_test.py index ad243c5807..7c32f337fd 100644 --- a/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled_test.py +++ b/tests/providers/azure/services/mysql/mysql_flexible_server_audit_log_enabled/mysql_flexible_server_audit_log_enabled_test.py @@ -56,6 +56,57 @@ class Test_mysql_flexible_server_audit_log_enabled: result = check.execute() assert len(result) == 0 + def test_mysql_audit_log_enabled_lowercase(self): + server_name = str(uuid4()) + mysql_client = mock.MagicMock + mysql_client.flexible_servers = { + AZURE_SUBSCRIPTION_ID: { + "/subscriptions/resource_id": FlexibleServer( + resource_id="/subscriptions/resource_id", + name=server_name, + location="location", + version="version", + configurations={ + "audit_log_enabled": Configuration( + resource_id=f"/subscriptions/{server_name}/configurations/audit_log_enabled", + description="description", + value="on", + ) + }, + ) + } + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.mysql.mysql_flexible_server_audit_log_enabled.mysql_flexible_server_audit_log_enabled.mysql_client", + new=mysql_client, + ), + ): + from prowler.providers.azure.services.mysql.mysql_flexible_server_audit_log_enabled.mysql_flexible_server_audit_log_enabled import ( + mysql_flexible_server_audit_log_enabled, + ) + + check = mysql_flexible_server_audit_log_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == server_name + assert result[0].location == "location" + assert ( + result[0].resource_id + == f"/subscriptions/{server_name}/configurations/audit_log_enabled" + ) + assert ( + result[0].status_extended + == f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + ) + def test_mysql_audit_log_disabled(self): server_name = str(uuid4()) mysql_client = mock.MagicMock diff --git a/tests/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled_test.py b/tests/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled_test.py index f540fe4865..2b87a28d8f 100644 --- a/tests/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled_test.py +++ b/tests/providers/azure/services/mysql/mysql_flexible_server_ssl_connection_enabled/mysql_flexible_server_ssl_connection_enabled_test.py @@ -107,6 +107,57 @@ class Test_mysql_flexible_server_ssl_connection_enabled: == f"SSL connection is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." ) + def test_mysql_connection_enabled_lowercase(self): + server_name = str(uuid4()) + mysql_client = mock.MagicMock + mysql_client.flexible_servers = { + AZURE_SUBSCRIPTION_ID: { + "/subscriptions/resource_id": FlexibleServer( + resource_id="/subscriptions/resource_id", + name=server_name, + location="location", + version="version", + configurations={ + "require_secure_transport": Configuration( + resource_id=f"/subscriptions/{server_name}/configurations/require_secure_transport", + description="description", + value="on", + ) + }, + ) + } + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.mysql.mysql_flexible_server_ssl_connection_enabled.mysql_flexible_server_ssl_connection_enabled.mysql_client", + new=mysql_client, + ), + ): + from prowler.providers.azure.services.mysql.mysql_flexible_server_ssl_connection_enabled.mysql_flexible_server_ssl_connection_enabled import ( + mysql_flexible_server_ssl_connection_enabled, + ) + + check = mysql_flexible_server_ssl_connection_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == server_name + assert result[0].location == "location" + assert ( + result[0].resource_id + == f"/subscriptions/{server_name}/configurations/require_secure_transport" + ) + assert ( + result[0].status_extended + == f"SSL connection is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}." + ) + def test_mysql_ssl_connection_disabled(self): server_name = str(uuid4()) mysql_client = mock.MagicMock diff --git a/tests/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled_test.py b/tests/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled_test.py index 23394addca..a99be2ea54 100644 --- a/tests/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled_test.py +++ b/tests/providers/azure/services/vm/vm_backup_enabled/vm_backup_enabled_test.py @@ -221,6 +221,85 @@ class Test_vm_backup_enabled: == f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_ID} is not protected by Azure Backup." ) + def test_vm_protected_by_backup_case_insensitive(self): + vm_id = str(uuid4()) + vm_name = "vmtest" + vault_id = str(uuid4()) + vault_name = "vault1" + mock_vm_client = mock.MagicMock() + mock_recovery_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.vm.vm_backup_enabled.vm_backup_enabled.vm_client", + new=mock_vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.recovery_client", + new=mock_recovery_client, + ), + ): + from azure.mgmt.recoveryservicesbackup.activestamp.models import ( + DataSourceType, + ) + + from prowler.providers.azure.services.recovery.recovery_service import ( + BackupItem, + BackupVault, + ) + from prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled import ( + vm_backup_enabled, + ) + from prowler.providers.azure.services.vm.vm_service import ( + ManagedDiskParameters, + OSDisk, + StorageProfile, + VirtualMachine, + ) + + vm = VirtualMachine( + resource_id=vm_id, + resource_name=vm_name, + location="eastus", + security_profile=None, + extensions=[], + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters(id="managed_disk_id"), + ), + data_disks=[], + ), + ) + backup_item = BackupItem( + id=str(uuid4()), + name="someprefix;VMTEST", + workload_type=DataSourceType.VM, + ) + vault = BackupVault( + id=vault_id, + name=vault_name, + location="eastus", + backup_protected_items={backup_item.id: backup_item}, + ) + mock_vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + mock_recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}} + check = vm_backup_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == vm_name + assert result[0].resource_id == vm_id + assert ( + result[0].status_extended + == f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_ID} is protected by Azure Backup (vault: {vault_name})." + ) + def test_vm_protected_by_backup_non_vm_workload(self): vm_id = str(uuid4()) vm_name = "VMTest" diff --git a/tests/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period_test.py b/tests/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period_test.py index b43b75548a..28aab1b38b 100644 --- a/tests/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period_test.py +++ b/tests/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period_test.py @@ -156,6 +156,100 @@ class Test_vm_sufficient_daily_backup_retention_period: in result[0].status_extended ) + def test_vm_with_sufficient_retention_case_insensitive(self): + from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType + + from prowler.providers.azure.services.recovery.recovery_service import ( + BackupItem, + BackupPolicy, + BackupVault, + ) + from prowler.providers.azure.services.vm.vm_service import ( + ManagedDiskParameters, + OSDisk, + StorageProfile, + VirtualMachine, + ) + + vm_id = str(uuid4()) + vm_name = "vmtest" + vault_id = str(uuid4()) + policy_id = str(uuid4()) + retention_days = 14 + min_retention_days = 7 + + vm = VirtualMachine( + resource_id=vm_id, + resource_name=vm_name, + location="eastus", + security_profile=None, + extensions=[], + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters(id="managed_disk_id"), + ), + data_disks=[], + ), + ) + backup_item = BackupItem( + id=str(uuid4()), + name="someprefix;VMTEST", + workload_type=DataSourceType.VM, + backup_policy_id=policy_id, + ) + backup_policy = BackupPolicy( + id=policy_id, + name="policy1", + retention_days=retention_days, + ) + vault = BackupVault( + id=vault_id, + name="vault1", + location="eastus", + backup_protected_items={backup_item.id: backup_item}, + backup_policies={policy_id: backup_policy}, + ) + vm_client = mock.MagicMock() + recovery_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}} + vm_client.audit_config = { + "vm_backup_min_daily_retention_days": min_retention_days + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider( + audit_config=vm_client.audit_config + ), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.recovery_client", + new=recovery_client, + ), + ): + from prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period import ( + vm_sufficient_daily_backup_retention_period, + ) + + check = vm_sufficient_daily_backup_retention_period() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == vm_name + assert result[0].resource_id == vm_id + assert ( + f"has a daily backup retention period of {retention_days} days" + in result[0].status_extended + ) + def test_vm_with_insufficient_retention(self): from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType 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/tests/providers/openstack/services/objectstorage/objectstorage_container_acl_not_globally_shared/objectstorage_container_acl_not_globally_shared_test.py b/tests/providers/openstack/services/objectstorage/objectstorage_container_acl_not_globally_shared/objectstorage_container_acl_not_globally_shared_test.py new file mode 100644 index 0000000000..3e2cdd6f9b --- /dev/null +++ b/tests/providers/openstack/services/objectstorage/objectstorage_container_acl_not_globally_shared/objectstorage_container_acl_not_globally_shared_test.py @@ -0,0 +1,283 @@ +"""Tests for objectstorage_container_acl_not_globally_shared check.""" + +from unittest import mock + +from prowler.providers.openstack.services.objectstorage.objectstorage_service import ( + ObjectStorageContainer, +) +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_objectstorage_container_acl_not_globally_shared: + """Test suite for objectstorage_container_acl_not_globally_shared check.""" + + def test_no_containers(self): + """Test when no containers exist.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_acl_not_globally_shared.objectstorage_container_acl_not_globally_shared.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_acl_not_globally_shared.objectstorage_container_acl_not_globally_shared import ( + objectstorage_container_acl_not_globally_shared, + ) + + check = objectstorage_container_acl_not_globally_shared() + result = check.execute() + + assert len(result) == 0 + + def test_container_not_globally_shared(self): + """Test container without global sharing (PASS).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-1", + name="project-scoped", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=10, + bytes_used=1024, + read_ACL="project-123:*", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_acl_not_globally_shared.objectstorage_container_acl_not_globally_shared.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_acl_not_globally_shared.objectstorage_container_acl_not_globally_shared import ( + objectstorage_container_acl_not_globally_shared, + ) + + check = objectstorage_container_acl_not_globally_shared() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Container project-scoped read ACL is not globally shared." + ) + assert result[0].resource_id == "container-1" + assert result[0].resource_name == "project-scoped" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_globally_shared(self): + """Test container with global sharing (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-2", + name="global-shared", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=5, + bytes_used=512, + read_ACL="*:*", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_acl_not_globally_shared.objectstorage_container_acl_not_globally_shared.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_acl_not_globally_shared.objectstorage_container_acl_not_globally_shared import ( + objectstorage_container_acl_not_globally_shared, + ) + + check = objectstorage_container_acl_not_globally_shared() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Container global-shared has globally shared read ACL (*:*) allowing all authenticated users from any project." + ) + assert result[0].resource_id == "container-2" + assert result[0].resource_name == "global-shared" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_globally_shared_bare_wildcard(self): + """Test container with * (bare wildcard) read ACL (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-3", + name="bare-wildcard", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="*", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_acl_not_globally_shared.objectstorage_container_acl_not_globally_shared.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_acl_not_globally_shared.objectstorage_container_acl_not_globally_shared import ( + objectstorage_container_acl_not_globally_shared, + ) + + check = objectstorage_container_acl_not_globally_shared() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_container_star_colon_star_in_multi_entry_acl(self): + """Test container with *:* in multi-entry read ACL (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-4", + name="multi-entry-global", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="project-123:user-456,*:*", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_acl_not_globally_shared.objectstorage_container_acl_not_globally_shared.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_acl_not_globally_shared.objectstorage_container_acl_not_globally_shared import ( + objectstorage_container_acl_not_globally_shared, + ) + + check = objectstorage_container_acl_not_globally_shared() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_multiple_containers_mixed(self): + """Test multiple containers with mixed ACLs.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-pass", + name="Pass", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ), + ObjectStorageContainer( + id="container-fail", + name="Fail", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="*:*", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_acl_not_globally_shared.objectstorage_container_acl_not_globally_shared.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_acl_not_globally_shared.objectstorage_container_acl_not_globally_shared import ( + objectstorage_container_acl_not_globally_shared, + ) + + check = objectstorage_container_acl_not_globally_shared() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/objectstorage/objectstorage_container_listing_disabled/objectstorage_container_listing_disabled_test.py b/tests/providers/openstack/services/objectstorage/objectstorage_container_listing_disabled/objectstorage_container_listing_disabled_test.py new file mode 100644 index 0000000000..73bd1bdaba --- /dev/null +++ b/tests/providers/openstack/services/objectstorage/objectstorage_container_listing_disabled/objectstorage_container_listing_disabled_test.py @@ -0,0 +1,334 @@ +"""Tests for objectstorage_container_listing_disabled check.""" + +from unittest import mock + +from prowler.providers.openstack.services.objectstorage.objectstorage_service import ( + ObjectStorageContainer, +) +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_objectstorage_container_listing_disabled: + """Test suite for objectstorage_container_listing_disabled check.""" + + def test_no_containers(self): + """Test when no containers exist.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled import ( + objectstorage_container_listing_disabled, + ) + + check = objectstorage_container_listing_disabled() + result = check.execute() + + assert len(result) == 0 + + def test_container_no_listing(self): + """Test container without public listing (PASS).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-1", + name="no-listing", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=10, + bytes_used=1024, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled import ( + objectstorage_container_listing_disabled, + ) + + check = objectstorage_container_listing_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Container no-listing does not have public listing enabled." + ) + assert result[0].resource_id == "container-1" + assert result[0].resource_name == "no-listing" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_with_listing(self): + """Test container with public listing (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-2", + name="public-listing", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=5, + bytes_used=512, + read_ACL=".r:*,.rlistings", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled import ( + objectstorage_container_listing_disabled, + ) + + check = objectstorage_container_listing_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Container public-listing has public listing enabled (.rlistings) allowing anonymous object enumeration." + ) + assert result[0].resource_id == "container-2" + assert result[0].resource_name == "public-listing" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_listing_via_global_acl_star_colon_star(self): + """Test container with *:* read ACL enabling listing (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-3", + name="global-acl-listing", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=5, + bytes_used=512, + read_ACL="*:*", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled import ( + objectstorage_container_listing_disabled, + ) + + check = objectstorage_container_listing_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Container global-acl-listing has listing enabled via global read ACL (*:*) allowing all authenticated users to list objects." + ) + assert result[0].resource_id == "container-3" + assert result[0].resource_name == "global-acl-listing" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_listing_via_bare_wildcard(self): + """Test container with * read ACL enabling listing (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-4", + name="bare-wildcard-listing", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="*", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled import ( + objectstorage_container_listing_disabled, + ) + + check = objectstorage_container_listing_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_container_rlistings_takes_priority_over_global(self): + """Test that .rlistings is reported when both .rlistings and *:* are present.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-5", + name="both-patterns", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL=".rlistings,*:*", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled import ( + objectstorage_container_listing_disabled, + ) + + check = objectstorage_container_listing_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ".rlistings" in result[0].status_extended + + def test_multiple_containers_mixed(self): + """Test multiple containers with mixed listing status.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-pass", + name="Pass", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL=".r:*", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ), + ObjectStorageContainer( + id="container-fail", + name="Fail", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL=".r:*,.rlistings", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_listing_disabled.objectstorage_container_listing_disabled import ( + objectstorage_container_listing_disabled, + ) + + check = objectstorage_container_listing_disabled() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/objectstorage/objectstorage_container_metadata_sensitive_data/objectstorage_container_metadata_sensitive_data_test.py b/tests/providers/openstack/services/objectstorage/objectstorage_container_metadata_sensitive_data/objectstorage_container_metadata_sensitive_data_test.py new file mode 100644 index 0000000000..6cae6432c7 --- /dev/null +++ b/tests/providers/openstack/services/objectstorage/objectstorage_container_metadata_sensitive_data/objectstorage_container_metadata_sensitive_data_test.py @@ -0,0 +1,243 @@ +"""Tests for objectstorage_container_metadata_sensitive_data check.""" + +from unittest import mock + +from prowler.providers.openstack.services.objectstorage.objectstorage_service import ( + ObjectStorageContainer, +) +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_objectstorage_container_metadata_sensitive_data: + """Test suite for objectstorage_container_metadata_sensitive_data check.""" + + def test_no_containers(self): + """Test when no containers exist.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [] + objectstorage_client.audit_config = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data import ( + objectstorage_container_metadata_sensitive_data, + ) + + check = objectstorage_container_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 0 + + def test_container_no_metadata(self): + """Test container with no metadata (PASS).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.audit_config = {} + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-1", + name="no-metadata", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=10, + bytes_used=1024, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data import ( + objectstorage_container_metadata_sensitive_data, + ) + + check = objectstorage_container_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Container no-metadata has no metadata (no sensitive data exposure risk)." + ) + assert result[0].resource_id == "container-1" + assert result[0].resource_name == "no-metadata" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_safe_metadata(self): + """Test container with safe metadata (PASS).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.audit_config = {} + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-2", + name="safe-metadata", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=5, + bytes_used=512, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={"environment": "production", "application": "web-app"}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data import ( + objectstorage_container_metadata_sensitive_data, + ) + + check = objectstorage_container_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Container safe-metadata metadata does not contain sensitive data." + ) + + def test_container_password_in_metadata(self): + """Test container with password in metadata (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.audit_config = {} + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-3", + name="password-metadata", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={"db_password": "supersecret123"}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data import ( + objectstorage_container_metadata_sensitive_data, + ) + + check = objectstorage_container_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "contains potential secrets" in result[0].status_extended + + def test_multiple_containers_mixed(self): + """Test multiple containers with mixed metadata.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.audit_config = {} + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-pass", + name="Safe", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={"tier": "web"}, + ), + ObjectStorageContainer( + id="container-fail", + name="Unsafe", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={"admin_password": "secret123"}, + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_metadata_sensitive_data.objectstorage_container_metadata_sensitive_data import ( + objectstorage_container_metadata_sensitive_data, + ) + + check = objectstorage_container_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/objectstorage/objectstorage_container_public_read_acl_disabled/objectstorage_container_public_read_acl_disabled_test.py b/tests/providers/openstack/services/objectstorage/objectstorage_container_public_read_acl_disabled/objectstorage_container_public_read_acl_disabled_test.py new file mode 100644 index 0000000000..a8b484b8ec --- /dev/null +++ b/tests/providers/openstack/services/objectstorage/objectstorage_container_public_read_acl_disabled/objectstorage_container_public_read_acl_disabled_test.py @@ -0,0 +1,245 @@ +"""Tests for objectstorage_container_public_read_acl_disabled check.""" + +from unittest import mock + +from prowler.providers.openstack.services.objectstorage.objectstorage_service import ( + ObjectStorageContainer, +) +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_objectstorage_container_public_read_acl_disabled: + """Test suite for objectstorage_container_public_read_acl_disabled check.""" + + def test_no_containers(self): + """Test when no containers exist.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_public_read_acl_disabled.objectstorage_container_public_read_acl_disabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_public_read_acl_disabled.objectstorage_container_public_read_acl_disabled import ( + objectstorage_container_public_read_acl_disabled, + ) + + check = objectstorage_container_public_read_acl_disabled() + result = check.execute() + + assert len(result) == 0 + + def test_container_no_public_read(self): + """Test container without public read ACL (PASS).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-1", + name="private-container", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=10, + bytes_used=1024, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_public_read_acl_disabled.objectstorage_container_public_read_acl_disabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_public_read_acl_disabled.objectstorage_container_public_read_acl_disabled import ( + objectstorage_container_public_read_acl_disabled, + ) + + check = objectstorage_container_public_read_acl_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Container private-container does not have public read ACL." + ) + assert result[0].resource_id == "container-1" + assert result[0].resource_name == "private-container" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_public_read(self): + """Test container with public read ACL (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-2", + name="public-container", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=5, + bytes_used=512, + read_ACL=".r:*,.rlistings", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_public_read_acl_disabled.objectstorage_container_public_read_acl_disabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_public_read_acl_disabled.objectstorage_container_public_read_acl_disabled import ( + objectstorage_container_public_read_acl_disabled, + ) + + check = objectstorage_container_public_read_acl_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Container public-container has public read ACL (.r:*) allowing anonymous access." + ) + assert result[0].resource_id == "container-2" + assert result[0].resource_name == "public-container" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_domain_restricted_referrer_not_flagged(self): + """Test container with domain-restricted referrer ACL is not flagged (PASS).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-3", + name="domain-restricted", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=5, + bytes_used=512, + read_ACL=".r:*.example.com,.rlistings", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_public_read_acl_disabled.objectstorage_container_public_read_acl_disabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_public_read_acl_disabled.objectstorage_container_public_read_acl_disabled import ( + objectstorage_container_public_read_acl_disabled, + ) + + check = objectstorage_container_public_read_acl_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Container domain-restricted does not have public read ACL." + ) + + def test_multiple_containers_mixed(self): + """Test multiple containers with mixed ACLs.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-pass", + name="Pass", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ), + ObjectStorageContainer( + id="container-fail", + name="Fail", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL=".r:*", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_public_read_acl_disabled.objectstorage_container_public_read_acl_disabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_public_read_acl_disabled.objectstorage_container_public_read_acl_disabled import ( + objectstorage_container_public_read_acl_disabled, + ) + + check = objectstorage_container_public_read_acl_disabled() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/objectstorage/objectstorage_container_sync_not_enabled/objectstorage_container_sync_not_enabled_test.py b/tests/providers/openstack/services/objectstorage/objectstorage_container_sync_not_enabled/objectstorage_container_sync_not_enabled_test.py new file mode 100644 index 0000000000..24e4cdfbec --- /dev/null +++ b/tests/providers/openstack/services/objectstorage/objectstorage_container_sync_not_enabled/objectstorage_container_sync_not_enabled_test.py @@ -0,0 +1,199 @@ +"""Tests for objectstorage_container_sync_not_enabled check.""" + +from unittest import mock + +from prowler.providers.openstack.services.objectstorage.objectstorage_service import ( + ObjectStorageContainer, +) +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_objectstorage_container_sync_not_enabled: + """Test suite for objectstorage_container_sync_not_enabled check.""" + + def test_no_containers(self): + """Test when no containers exist.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_sync_not_enabled.objectstorage_container_sync_not_enabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_sync_not_enabled.objectstorage_container_sync_not_enabled import ( + objectstorage_container_sync_not_enabled, + ) + + check = objectstorage_container_sync_not_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_container_no_sync(self): + """Test container without sync (PASS).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-1", + name="no-sync", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=10, + bytes_used=1024, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_sync_not_enabled.objectstorage_container_sync_not_enabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_sync_not_enabled.objectstorage_container_sync_not_enabled import ( + objectstorage_container_sync_not_enabled, + ) + + check = objectstorage_container_sync_not_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Container no-sync does not have container sync enabled." + ) + assert result[0].resource_id == "container-1" + assert result[0].resource_name == "no-sync" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_with_sync(self): + """Test container with sync enabled (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-2", + name="synced-container", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=5, + bytes_used=512, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="https://other-cluster/v1/AUTH_test/container-2", + sync_key="shared-secret", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_sync_not_enabled.objectstorage_container_sync_not_enabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_sync_not_enabled.objectstorage_container_sync_not_enabled import ( + objectstorage_container_sync_not_enabled, + ) + + check = objectstorage_container_sync_not_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Container synced-container has container sync enabled (sync target: https://other-cluster/v1/AUTH_test/container-2)." + ) + assert result[0].resource_id == "container-2" + assert result[0].resource_name == "synced-container" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_containers_mixed(self): + """Test multiple containers with mixed sync status.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-pass", + name="Pass", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ), + ObjectStorageContainer( + id="container-fail", + name="Fail", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="https://external/v1/AUTH_test/container", + sync_key="key", + metadata={}, + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_sync_not_enabled.objectstorage_container_sync_not_enabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_sync_not_enabled.objectstorage_container_sync_not_enabled import ( + objectstorage_container_sync_not_enabled, + ) + + check = objectstorage_container_sync_not_enabled() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/objectstorage/objectstorage_container_versioning_enabled/objectstorage_container_versioning_enabled_test.py b/tests/providers/openstack/services/objectstorage/objectstorage_container_versioning_enabled/objectstorage_container_versioning_enabled_test.py new file mode 100644 index 0000000000..5e66f27c6d --- /dev/null +++ b/tests/providers/openstack/services/objectstorage/objectstorage_container_versioning_enabled/objectstorage_container_versioning_enabled_test.py @@ -0,0 +1,249 @@ +"""Tests for objectstorage_container_versioning_enabled check.""" + +from unittest import mock + +from prowler.providers.openstack.services.objectstorage.objectstorage_service import ( + ObjectStorageContainer, +) +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_objectstorage_container_versioning_enabled: + """Test suite for objectstorage_container_versioning_enabled check.""" + + def test_no_containers(self): + """Test when no containers exist.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_versioning_enabled.objectstorage_container_versioning_enabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_versioning_enabled.objectstorage_container_versioning_enabled import ( + objectstorage_container_versioning_enabled, + ) + + check = objectstorage_container_versioning_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_container_versioning_enabled_versions_location(self): + """Test container with versioning enabled via X-Versions-Location (PASS).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-1", + name="versioned-container", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=10, + bytes_used=1024, + read_ACL="", + write_ACL="", + versioning_enabled=True, + versions_location="versioned-container_versions", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_versioning_enabled.objectstorage_container_versioning_enabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_versioning_enabled.objectstorage_container_versioning_enabled import ( + objectstorage_container_versioning_enabled, + ) + + check = objectstorage_container_versioning_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Container versioned-container has versioning enabled (versions location: versioned-container_versions)." + ) + assert result[0].resource_id == "container-1" + assert result[0].resource_name == "versioned-container" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_versioning_enabled_history_location(self): + """Test container with versioning enabled via X-History-Location (PASS).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-1", + name="history-container", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=10, + bytes_used=1024, + read_ACL="", + write_ACL="", + versioning_enabled=True, + versions_location="", + history_location="history-container_versions", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_versioning_enabled.objectstorage_container_versioning_enabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_versioning_enabled.objectstorage_container_versioning_enabled import ( + objectstorage_container_versioning_enabled, + ) + + check = objectstorage_container_versioning_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Container history-container has versioning enabled (history location: history-container_versions)." + ) + assert result[0].resource_id == "container-1" + assert result[0].resource_name == "history-container" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_versioning_disabled(self): + """Test container without versioning (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-2", + name="no-versioning", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=5, + bytes_used=512, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_versioning_enabled.objectstorage_container_versioning_enabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_versioning_enabled.objectstorage_container_versioning_enabled import ( + objectstorage_container_versioning_enabled, + ) + + check = objectstorage_container_versioning_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Container no-versioning does not have versioning enabled." + ) + assert result[0].resource_id == "container-2" + assert result[0].resource_name == "no-versioning" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_containers_mixed(self): + """Test multiple containers with mixed versioning status.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-pass", + name="Pass", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="", + versioning_enabled=True, + versions_location="Pass_versions", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ), + ObjectStorageContainer( + id="container-fail", + name="Fail", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_versioning_enabled.objectstorage_container_versioning_enabled.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_versioning_enabled.objectstorage_container_versioning_enabled import ( + objectstorage_container_versioning_enabled, + ) + + check = objectstorage_container_versioning_enabled() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/objectstorage/objectstorage_container_write_acl_restricted/objectstorage_container_write_acl_restricted_test.py b/tests/providers/openstack/services/objectstorage/objectstorage_container_write_acl_restricted/objectstorage_container_write_acl_restricted_test.py new file mode 100644 index 0000000000..dc55902b78 --- /dev/null +++ b/tests/providers/openstack/services/objectstorage/objectstorage_container_write_acl_restricted/objectstorage_container_write_acl_restricted_test.py @@ -0,0 +1,329 @@ +"""Tests for objectstorage_container_write_acl_restricted check.""" + +from unittest import mock + +from prowler.providers.openstack.services.objectstorage.objectstorage_service import ( + ObjectStorageContainer, +) +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_objectstorage_container_write_acl_restricted: + """Test suite for objectstorage_container_write_acl_restricted check.""" + + def test_no_containers(self): + """Test when no containers exist.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted import ( + objectstorage_container_write_acl_restricted, + ) + + check = objectstorage_container_write_acl_restricted() + result = check.execute() + + assert len(result) == 0 + + def test_container_restricted_write(self): + """Test container with restricted write ACL (PASS).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-1", + name="restricted-write", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=10, + bytes_used=1024, + read_ACL="", + write_ACL="project-123:user-456", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted import ( + objectstorage_container_write_acl_restricted, + ) + + check = objectstorage_container_write_acl_restricted() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Container restricted-write has restricted write ACL." + ) + assert result[0].resource_id == "container-1" + assert result[0].resource_name == "restricted-write" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_unrestricted_write_star_colon_star(self): + """Test container with *:* write ACL (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-2", + name="unrestricted-write", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=5, + bytes_used=512, + read_ACL="", + write_ACL="*:*", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted import ( + objectstorage_container_write_acl_restricted, + ) + + check = objectstorage_container_write_acl_restricted() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Container unrestricted-write has unrestricted write ACL allowing all authenticated users to write." + ) + assert result[0].resource_id == "container-2" + assert result[0].resource_name == "unrestricted-write" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_container_unrestricted_write_star_only(self): + """Test container with * write ACL (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-3", + name="star-write", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="*", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted import ( + objectstorage_container_write_acl_restricted, + ) + + check = objectstorage_container_write_acl_restricted() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_container_star_in_multi_entry_acl(self): + """Test container with * in multi-entry write ACL (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-4", + name="star-multi-entry", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="*,project-123:user-456", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted import ( + objectstorage_container_write_acl_restricted, + ) + + check = objectstorage_container_write_acl_restricted() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Container star-multi-entry has unrestricted write ACL allowing all authenticated users to write." + ) + + def test_container_star_colon_star_in_multi_entry_acl(self): + """Test container with *:* in multi-entry write ACL (FAIL).""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-5", + name="star-colon-multi", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="project-123:user-456,*:*", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted import ( + objectstorage_container_write_acl_restricted, + ) + + check = objectstorage_container_write_acl_restricted() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_multiple_containers_mixed(self): + """Test multiple containers with mixed write ACLs.""" + objectstorage_client = mock.MagicMock() + objectstorage_client.containers = [ + ObjectStorageContainer( + id="container-pass", + name="Pass", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ), + ObjectStorageContainer( + id="container-fail", + name="Fail", + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + object_count=0, + bytes_used=0, + read_ACL="", + write_ACL="*:*", + versioning_enabled=False, + versions_location="", + history_location="", + sync_to="", + sync_key="", + metadata={}, + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted.objectstorage_client", + new=objectstorage_client, + ), + ): + from prowler.providers.openstack.services.objectstorage.objectstorage_container_write_acl_restricted.objectstorage_container_write_acl_restricted import ( + objectstorage_container_write_acl_restricted, + ) + + check = objectstorage_container_write_acl_restricted() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/objectstorage/openstack_objectstorage_service_test.py b/tests/providers/openstack/services/objectstorage/openstack_objectstorage_service_test.py new file mode 100644 index 0000000000..a7e07c97f9 --- /dev/null +++ b/tests/providers/openstack/services/objectstorage/openstack_objectstorage_service_test.py @@ -0,0 +1,403 @@ +"""Tests for OpenStack ObjectStorage service.""" + +from unittest.mock import MagicMock, patch + +from openstack import exceptions as openstack_exceptions + +from prowler.providers.openstack.services.objectstorage.objectstorage_service import ( + ObjectStorage, + ObjectStorageContainer, +) +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class TestObjectStorageService: + """Test suite for ObjectStorage service.""" + + def test_objectstorage_service_initialization(self): + """Test ObjectStorage service initializes correctly.""" + provider = set_mocked_openstack_provider() + + with patch.object( + ObjectStorage, "_list_containers", return_value=[] + ) as mock_list: + service = ObjectStorage(provider) + + assert service.service_name == "ObjectStorage" + assert service.provider == provider + assert service.connection == provider.connection + assert service.regional_connections == provider.regional_connections + assert service.audited_regions == [OPENSTACK_REGION] + assert service.region == OPENSTACK_REGION + assert service.project_id == OPENSTACK_PROJECT_ID + assert service.containers == [] + mock_list.assert_called_once() + + def test_objectstorage_list_containers_success(self): + """Test listing containers successfully.""" + provider = set_mocked_openstack_provider() + + mock_container1 = MagicMock() + mock_container1.name = "container-1" + mock_container1.count = 10 + mock_container1.bytes = 1024 + mock_container1.read_ACL = ".r:*,.rlistings" + mock_container1.write_ACL = "*:*" + mock_container1.versions_location = "container-1_versions" + mock_container1.history_location = "" + mock_container1.sync_to = "https://other-cluster/v1/AUTH_test/container-1" + mock_container1.sync_key = "shared-secret" + mock_container1.metadata = {"environment": "production"} + + mock_container2 = MagicMock() + mock_container2.name = "container-2" + mock_container2.count = 0 + mock_container2.bytes = 0 + mock_container2.read_ACL = "" + mock_container2.write_ACL = "" + mock_container2.versions_location = "" + mock_container2.history_location = "" + mock_container2.sync_to = "" + mock_container2.sync_key = "" + mock_container2.metadata = {} + + provider.connection.object_store.containers.return_value = [ + mock_container1, + mock_container2, + ] + + # get_container_metadata returns the detailed mock for each container + def mock_get_metadata(name): + return {"container-1": mock_container1, "container-2": mock_container2}[ + name + ] + + provider.connection.object_store.get_container_metadata.side_effect = ( + mock_get_metadata + ) + + service = ObjectStorage(provider) + + assert len(service.containers) == 2 + assert isinstance(service.containers[0], ObjectStorageContainer) + assert service.containers[0].id == "container-1" + assert service.containers[0].name == "container-1" + assert service.containers[0].region == OPENSTACK_REGION + assert service.containers[0].project_id == OPENSTACK_PROJECT_ID + assert service.containers[0].object_count == 10 + assert service.containers[0].bytes_used == 1024 + assert service.containers[0].read_ACL == ".r:*,.rlistings" + assert service.containers[0].write_ACL == "*:*" + assert service.containers[0].versioning_enabled is True + assert service.containers[0].versions_location == "container-1_versions" + assert service.containers[0].history_location == "" + assert ( + service.containers[0].sync_to + == "https://other-cluster/v1/AUTH_test/container-1" + ) + assert service.containers[0].sync_key == "shared-secret" + assert service.containers[0].metadata == {"environment": "production"} + + assert service.containers[1].id == "container-2" + assert service.containers[1].versioning_enabled is False + assert service.containers[1].sync_to == "" + assert service.containers[1].metadata == {} + + def test_objectstorage_list_containers_empty(self): + """Test listing containers when none exist.""" + provider = set_mocked_openstack_provider() + provider.connection.object_store.containers.return_value = [] + + service = ObjectStorage(provider) + + assert service.containers == [] + + def test_objectstorage_list_containers_missing_attributes(self): + """Test listing containers with missing attributes uses fallback to list data.""" + provider = set_mocked_openstack_provider() + + mock_container = MagicMock() + mock_container.name = "container-1" + del mock_container.count + del mock_container.bytes + del mock_container.read_ACL + del mock_container.write_ACL + del mock_container.versions_location + del mock_container.history_location + del mock_container.sync_to + del mock_container.sync_key + del mock_container.metadata + + provider.connection.object_store.containers.return_value = [mock_container] + + # HEAD also returns missing attributes (same mock) + provider.connection.object_store.get_container_metadata.return_value = ( + mock_container + ) + + service = ObjectStorage(provider) + + assert len(service.containers) == 1 + assert service.containers[0].id == "container-1" + assert service.containers[0].name == "container-1" + assert service.containers[0].object_count == 0 + assert service.containers[0].bytes_used == 0 + assert service.containers[0].read_ACL == "" + assert service.containers[0].write_ACL == "" + assert service.containers[0].versioning_enabled is False + assert service.containers[0].versions_location == "" + assert service.containers[0].history_location == "" + assert service.containers[0].sync_to == "" + assert service.containers[0].sync_key == "" + assert service.containers[0].metadata == {} + + def test_objectstorage_list_containers_head_failure_falls_back(self): + """Test that HEAD failure falls back to list data gracefully.""" + provider = set_mocked_openstack_provider() + + mock_container = MagicMock() + mock_container.name = "container-1" + mock_container.count = 5 + mock_container.bytes = 256 + mock_container.read_ACL = None + mock_container.write_ACL = None + mock_container.versions_location = None + mock_container.history_location = None + mock_container.sync_to = None + mock_container.sync_key = None + mock_container.metadata = {} + + provider.connection.object_store.containers.return_value = [mock_container] + provider.connection.object_store.get_container_metadata.side_effect = Exception( + "HEAD failed" + ) + + service = ObjectStorage(provider) + + # Should still create the container using list data as fallback + assert len(service.containers) == 1 + assert service.containers[0].name == "container-1" + assert service.containers[0].object_count == 5 + assert service.containers[0].bytes_used == 256 + + def test_objectstorage_list_containers_sdk_exception(self): + """Test handling SDKException when listing containers.""" + provider = set_mocked_openstack_provider() + provider.connection.object_store.containers.side_effect = ( + openstack_exceptions.SDKException("API error") + ) + + service = ObjectStorage(provider) + + assert service.containers == [] + + def test_objectstorage_list_containers_generic_exception(self): + """Test handling generic exception when listing containers.""" + provider = set_mocked_openstack_provider() + provider.connection.object_store.containers.side_effect = Exception( + "Unexpected error" + ) + + service = ObjectStorage(provider) + + assert service.containers == [] + + def test_objectstorage_container_dataclass_attributes(self): + """Test ObjectStorageContainer dataclass has all required attributes.""" + container = ObjectStorageContainer( + id="container-1", + name="container-1", + region="RegionOne", + project_id="project-1", + object_count=10, + bytes_used=1024, + read_ACL=".r:*", + write_ACL="*:*", + versioning_enabled=True, + versions_location="container-1_versions", + history_location="", + sync_to="https://other-cluster/v1/AUTH_test/container-1", + sync_key="shared-secret", + metadata={"environment": "production"}, + ) + + assert container.id == "container-1" + assert container.name == "container-1" + assert container.region == "RegionOne" + assert container.project_id == "project-1" + assert container.object_count == 10 + assert container.bytes_used == 1024 + assert container.read_ACL == ".r:*" + assert container.write_ACL == "*:*" + assert container.versioning_enabled is True + assert container.versions_location == "container-1_versions" + assert container.history_location == "" + assert container.sync_to == "https://other-cluster/v1/AUTH_test/container-1" + assert container.sync_key == "shared-secret" + assert container.metadata == {"environment": "production"} + + def test_objectstorage_service_inherits_from_base(self): + """Test ObjectStorage service inherits from OpenStackService.""" + provider = set_mocked_openstack_provider() + + with patch.object(ObjectStorage, "_list_containers", return_value=[]): + service = ObjectStorage(provider) + + assert hasattr(service, "service_name") + assert hasattr(service, "provider") + assert hasattr(service, "connection") + assert hasattr(service, "regional_connections") + assert hasattr(service, "audited_regions") + assert hasattr(service, "session") + assert hasattr(service, "region") + assert hasattr(service, "project_id") + assert hasattr(service, "identity") + assert hasattr(service, "audit_config") + assert hasattr(service, "fixer_config") + + def test_objectstorage_list_containers_multi_region(self): + """Test listing containers across multiple regions.""" + provider = set_mocked_openstack_provider() + + # Create two mock connections for two regions + mock_conn_uk1 = MagicMock() + mock_conn_de1 = MagicMock() + + provider.regional_connections = {"UK1": mock_conn_uk1, "DE1": mock_conn_de1} + + mock_container_uk = MagicMock() + mock_container_uk.name = "container-uk" + mock_container_uk.count = 5 + mock_container_uk.bytes = 512 + mock_container_uk.read_ACL = "" + mock_container_uk.write_ACL = "" + mock_container_uk.versions_location = "" + mock_container_uk.history_location = "" + mock_container_uk.sync_to = "" + mock_container_uk.sync_key = "" + mock_container_uk.metadata = {} + + mock_container_de = MagicMock() + mock_container_de.name = "container-de" + mock_container_de.count = 10 + mock_container_de.bytes = 1024 + mock_container_de.read_ACL = ".r:*" + mock_container_de.write_ACL = "" + mock_container_de.versions_location = "" + mock_container_de.history_location = "" + mock_container_de.sync_to = "" + mock_container_de.sync_key = "" + mock_container_de.metadata = {} + + mock_conn_uk1.object_store.containers.return_value = [mock_container_uk] + mock_conn_uk1.object_store.get_container_metadata.return_value = ( + mock_container_uk + ) + mock_conn_de1.object_store.containers.return_value = [mock_container_de] + mock_conn_de1.object_store.get_container_metadata.return_value = ( + mock_container_de + ) + + service = ObjectStorage(provider) + + assert len(service.containers) == 2 + uk_container = next(c for c in service.containers if c.id == "container-uk") + de_container = next(c for c in service.containers if c.id == "container-de") + assert uk_container.region == "UK1" + assert de_container.region == "DE1" + + def test_objectstorage_list_containers_multi_region_partial_failure(self): + """Test that a failing region doesn't prevent other regions from being listed.""" + provider = set_mocked_openstack_provider() + + mock_conn_ok = MagicMock() + mock_conn_fail = MagicMock() + + provider.regional_connections = {"UK1": mock_conn_ok, "DE1": mock_conn_fail} + + mock_container = MagicMock() + mock_container.name = "container-uk" + mock_container.count = 5 + mock_container.bytes = 512 + mock_container.read_ACL = "" + mock_container.write_ACL = "" + mock_container.versions_location = "" + mock_container.history_location = "" + mock_container.sync_to = "" + mock_container.sync_key = "" + mock_container.metadata = {} + + mock_conn_ok.object_store.containers.return_value = [mock_container] + mock_conn_ok.object_store.get_container_metadata.return_value = mock_container + mock_conn_fail.object_store.containers.side_effect = ( + openstack_exceptions.SDKException("API error in DE1") + ) + + service = ObjectStorage(provider) + + assert len(service.containers) == 1 + assert service.containers[0].id == "container-uk" + assert service.containers[0].region == "UK1" + + def test_objectstorage_list_containers_multi_region_one_empty(self): + """Test multi-region where one region has containers and the other is empty.""" + provider = set_mocked_openstack_provider() + + mock_conn_uk1 = MagicMock() + mock_conn_de1 = MagicMock() + + provider.regional_connections = {"UK1": mock_conn_uk1, "DE1": mock_conn_de1} + + mock_container = MagicMock() + mock_container.name = "container-uk" + mock_container.count = 5 + mock_container.bytes = 512 + mock_container.read_ACL = "" + mock_container.write_ACL = "" + mock_container.versions_location = "" + mock_container.history_location = "" + mock_container.sync_to = "" + mock_container.sync_key = "" + mock_container.metadata = {} + + mock_conn_uk1.object_store.containers.return_value = [mock_container] + mock_conn_uk1.object_store.get_container_metadata.return_value = mock_container + mock_conn_de1.object_store.containers.return_value = [] + + service = ObjectStorage(provider) + + assert len(service.containers) == 1 + assert service.containers[0].id == "container-uk" + assert service.containers[0].region == "UK1" + + def test_objectstorage_list_containers_history_location_versioning(self): + """Test that history_location (X-History-Location) enables versioning.""" + provider = set_mocked_openstack_provider() + + mock_container = MagicMock() + mock_container.name = "history-container" + mock_container.count = 3 + mock_container.bytes = 256 + mock_container.read_ACL = "" + mock_container.write_ACL = "" + mock_container.versions_location = "" + mock_container.history_location = "history-container_versions" + mock_container.sync_to = "" + mock_container.sync_key = "" + mock_container.metadata = {} + + provider.connection.object_store.containers.return_value = [mock_container] + provider.connection.object_store.get_container_metadata.return_value = ( + mock_container + ) + + service = ObjectStorage(provider) + + assert len(service.containers) == 1 + assert service.containers[0].versioning_enabled is True + assert service.containers[0].versions_location == "" + assert service.containers[0].history_location == "history-container_versions" diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 011ed8ec9a..2c17bcc5d6 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -2,18 +2,40 @@ 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) -### 🐞 Changed +### 🚀 Added -- 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) +- 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) -## [1.19.1] (Prowler v5.19.1 UNRELEASED) +### 🔄 Changed + +- 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) + +--- + +## [1.20.0] (Prowler v5.20.0) + +### 🚀 Added + +- 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 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 0a62efedb7..3c480e3c48 100644 --- a/ui/components/providers/organizations/org-setup-form.tsx +++ b/ui/components/providers/organizations/org-setup-form.tsx @@ -1,5 +1,6 @@ "use client"; +import { useClipboard } from "@heroui/use-clipboard"; import { zodResolver } from "@hookform/resolvers/zod"; import { Check, Copy, ExternalLink } from "lucide-react"; import { useSession } from "next-auth/react"; @@ -7,18 +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 } from "@/lib"; +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"; @@ -39,7 +51,7 @@ const orgSetupSchema = z.object({ .min(1, "Role ARN is required") .regex( /^arn:aws:iam::\d{12}:role\//, - "Must be a valid IAM Role ARN (e.g., arn:aws:iam::123456789012:role/ProwlerOrgRole)", + "Must be a valid IAM Role ARN (e.g., arn:aws:iam::123456789012:role/ProwlerScan)", ), stackSetDeployed: z.boolean().refine((value) => value, { message: "You must confirm the StackSet deployment before continuing.", @@ -48,34 +60,55 @@ 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 [isExternalIdCopied, setIsExternalIdCopied] = useState(false); const stackSetExternalId = session?.tenantId ?? ""; + const { organizationId } = useOrgSetupStore(); + const { toast } = useToast(); + const { copied: isExternalIdCopied, copy: copyExternalId } = useClipboard({ + timeout: 1500, + }); + const { copied: isTemplateUrlCopied, copy: copyTemplateUrl } = useClipboard({ + 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, }, @@ -90,9 +123,10 @@ export function OrgSetupForm({ const awsOrgId = watch("awsOrgId") || ""; const isOrgIdValid = /^o-[a-z0-9]{10,32}$/.test(awsOrgId.trim()); - const stackSetQuickLink = - stackSetExternalId && - getAWSCredentialsTemplateLinks(stackSetExternalId).cloudformationQuickLink; + const templateLinks = stackSetExternalId + ? getAWSCredentialsTemplateLinks(stackSetExternalId) + : null; + const orgQuickLink = templateLinks?.cloudformationOrgQuickLink; const { apiError, setApiError, submitOrganizationSetup } = useOrgSetupSubmission({ @@ -109,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), @@ -135,7 +171,9 @@ export function OrgSetupForm({ }); }, [ formId, + intent, isOrgIdValid, + isSaving, isSubmitting, isValid, onBack, @@ -159,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; } @@ -240,6 +311,8 @@ export function OrgSetupForm({ autoCapitalize="none" autoCorrect="off" spellCheck={false} + isReadOnly={isReadOnlyOrgId} + isDisabled={isReadOnlyOrgId} /> + {/* External ID - shown first for both deployment steps */}

- 1) Launch the Prowler CloudFormation StackSet in your AWS - Console. -

- -
- -
-

- 2) Use the following Prowler External ID parameter in the - StackSet. + Use the following External ID when deploying + the CloudFormation Stack and StackSet.

@@ -302,15 +351,7 @@ export function OrgSetupForm({
+ {/* Step 1: Management account - CloudFormation Stack */}

- 3) Copy the Prowler IAM Role ARN from AWS and confirm the - StackSet is successfully deployed by clicking the checkbox - below. + 1) Deploy the ProwlerScan role in your{" "} + management account using a CloudFormation + Stack. +

+ +
+ + {/* Step 2: Member accounts - CloudFormation StackSet */} +
+

+ 2) Deploy the ProwlerScan role to{" "} + member accounts using a CloudFormation + StackSet. +

+

+ Open the StackSets console, select{" "} + Service-managed permissions, and paste the + template URL below. Set the ExternalId{" "} + parameter to the value shown above. +

+
+ + {PROWLER_CF_TEMPLATE_URL} + + +
+ +
+ + {/* Step 3: Role ARN + confirm */} +
+

+ 3) Paste the management account Role ARN and confirm both + deployments are complete.

@@ -365,7 +479,8 @@ export function OrgSetupForm({ htmlFor="stackSetDeployed" className="text-text-neutral-tertiary text-xs leading-5 font-normal" > - The StackSet has been successfully deployed in AWS + The Stack and StackSet have been successfully deployed in + AWS * diff --git a/ui/components/providers/provider-info.tsx b/ui/components/providers/provider-info.tsx deleted file mode 100644 index 9043c8b8e8..0000000000 --- a/ui/components/providers/provider-info.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Tooltip } from "@heroui/tooltip"; - -import { ProviderType } from "@/types"; - -import { ConnectionFalse, ConnectionPending, ConnectionTrue } from "../icons"; -import { getProviderLogo } from "../ui/entities"; - -interface ProviderInfoProps { - connected: boolean | null; - provider: ProviderType; - providerAlias: string; - providerUID?: string; -} - -export const ProviderInfo = ({ - connected, - provider, - providerAlias, - providerUID, -}: ProviderInfoProps) => { - 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 b4bf22af4b..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", @@ -176,7 +188,7 @@ export const RadioGroupProvider: FC = ({
{errorMessage && ( - + {errorMessage} )} 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/fields/wizard-input-field.tsx b/ui/components/providers/workflow/forms/fields/wizard-input-field.tsx index 88188b5487..b769bd1461 100644 --- a/ui/components/providers/workflow/forms/fields/wizard-input-field.tsx +++ b/ui/components/providers/workflow/forms/fields/wizard-input-field.tsx @@ -163,7 +163,7 @@ export const WizardInputField = ({ )}
- +
); }} diff --git a/ui/components/providers/workflow/forms/fields/wizard-textarea-field.tsx b/ui/components/providers/workflow/forms/fields/wizard-textarea-field.tsx index 05768b0e04..473de8d48d 100644 --- a/ui/components/providers/workflow/forms/fields/wizard-textarea-field.tsx +++ b/ui/components/providers/workflow/forms/fields/wizard-textarea-field.tsx @@ -87,7 +87,7 @@ export const WizardTextareaField = ({ {description}

)} - +
); }} diff --git a/ui/components/providers/workflow/forms/select-credentials-type/alibabacloud/radio-group-alibabacloud-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/alibabacloud/radio-group-alibabacloud-via-credentials-type-form.tsx index 0359f07c86..1315a6b559 100644 --- a/ui/components/providers/workflow/forms/select-credentials-type/alibabacloud/radio-group-alibabacloud-via-credentials-type-form.tsx +++ b/ui/components/providers/workflow/forms/select-credentials-type/alibabacloud/radio-group-alibabacloud-via-credentials-type-form.tsx @@ -50,7 +50,7 @@ export const RadioGroupAlibabaCloudViaCredentialsTypeForm = < {errorMessage && ( - + {errorMessage} )} diff --git a/ui/components/providers/workflow/forms/select-credentials-type/aws/radio-group-aws-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/aws/radio-group-aws-via-credentials-type-form.tsx index b88208347a..294d68515b 100644 --- a/ui/components/providers/workflow/forms/select-credentials-type/aws/radio-group-aws-via-credentials-type-form.tsx +++ b/ui/components/providers/workflow/forms/select-credentials-type/aws/radio-group-aws-via-credentials-type-form.tsx @@ -43,7 +43,7 @@ export const RadioGroupAWSViaCredentialsTypeForm = ({ {errorMessage && ( - + {errorMessage} )} diff --git a/ui/components/providers/workflow/forms/select-credentials-type/cloudflare/radio-group-cloudflare-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/cloudflare/radio-group-cloudflare-via-credentials-type-form.tsx index ba60dd6c25..67ea17267e 100644 --- a/ui/components/providers/workflow/forms/select-credentials-type/cloudflare/radio-group-cloudflare-via-credentials-type-form.tsx +++ b/ui/components/providers/workflow/forms/select-credentials-type/cloudflare/radio-group-cloudflare-via-credentials-type-form.tsx @@ -44,7 +44,7 @@ export const RadioGroupCloudflareViaCredentialsTypeForm = ({ {errorMessage && ( - + {errorMessage} )} diff --git a/ui/components/providers/workflow/forms/select-credentials-type/gcp/radio-group-gcp-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/gcp/radio-group-gcp-via-credentials-type-form.tsx index 279d17a790..1f32d154a7 100644 --- a/ui/components/providers/workflow/forms/select-credentials-type/gcp/radio-group-gcp-via-credentials-type-form.tsx +++ b/ui/components/providers/workflow/forms/select-credentials-type/gcp/radio-group-gcp-via-credentials-type-form.tsx @@ -47,7 +47,7 @@ export const RadioGroupGCPViaCredentialsTypeForm = ({ {errorMessage && ( - + {errorMessage} )} diff --git a/ui/components/providers/workflow/forms/select-credentials-type/github/radio-group-github-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/github/radio-group-github-via-credentials-type-form.tsx index f6964d98d0..fa2d5ab98f 100644 --- a/ui/components/providers/workflow/forms/select-credentials-type/github/radio-group-github-via-credentials-type-form.tsx +++ b/ui/components/providers/workflow/forms/select-credentials-type/github/radio-group-github-via-credentials-type-form.tsx @@ -54,7 +54,7 @@ export const RadioGroupGitHubViaCredentialsTypeForm = ({ {errorMessage && ( - + {errorMessage} )} diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx index dcbba07d2b..ed871352a7 100644 --- a/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx @@ -44,7 +44,7 @@ export const RadioGroupM365ViaCredentialsTypeForm = ({ {errorMessage && ( - + {errorMessage} )} 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/form/Form.test.tsx b/ui/components/ui/form/Form.test.tsx new file mode 100644 index 0000000000..41f01750e5 --- /dev/null +++ b/ui/components/ui/form/Form.test.tsx @@ -0,0 +1,65 @@ +import { render, screen } from "@testing-library/react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { describe, expect, it } from "vitest"; + +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "./Form"; + +interface TestValues { + providerUid: string; +} + +function TestFormWithError() { + const form = useForm({ + defaultValues: { + providerUid: "", + }, + }); + + useEffect(() => { + form.setError("providerUid", { + type: "manual", + message: "Provider ID is required", + }); + }, [form]); + + return ( +
+ ( + + Provider UID + + + + + + )} + /> + + ); +} + +describe("Form", () => { + it("should use the existing error text token for labels and messages", async () => { + // Given + render(); + + // When + const label = await screen.findByText("Provider UID"); + const message = await screen.findByText("Provider ID is required"); + + // Then + expect(label).toHaveClass("text-text-error-primary"); + expect(message).toHaveClass("text-text-error-primary"); + }); +}); diff --git a/ui/components/ui/form/Form.tsx b/ui/components/ui/form/Form.tsx index f4cc7d5403..011b0e0492 100644 --- a/ui/components/ui/form/Form.tsx +++ b/ui/components/ui/form/Form.tsx @@ -100,7 +100,10 @@ const FormLabel = React.forwardRef< return (
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 1fe10ef603..7b4cdf4c7e 100644 --- a/ui/lib/external-urls.ts +++ b/ui/lib/external-urls.ts @@ -8,6 +8,18 @@ export const DOCS_URLS = { "https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations", } as const; +// CloudFormation template URL for the ProwlerScan role. +// Also used (URL-encoded) as the templateURL param in cloudformationQuickLink +// and cloudformationOrgQuickLink below — keep both in sync. +export const PROWLER_CF_TEMPLATE_URL = + "https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml"; + +// AWS Console URL for creating a new StackSet. +// Hardcoded to us-east-1 — StackSets are typically managed from this region. +// Users in AWS GovCloud or China partitions would need different URLs. +export const STACKSET_CONSOLE_URL = + "https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacksets/create"; + export const getProviderHelpText = (provider: string) => { switch (provider) { case "aws": @@ -45,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?", @@ -70,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?", @@ -86,6 +108,7 @@ export const getAWSCredentialsTemplateLinks = ( cloudformation: string; terraform: string; cloudformationQuickLink: string; + cloudformationOrgQuickLink: string; } => { let links = {}; @@ -107,11 +130,24 @@ export const getAWSCredentialsTemplateLinks = ( }; } + const encodedTemplateUrl = encodeURIComponent(PROWLER_CF_TEMPLATE_URL); + const cfBaseUrl = + "https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate"; + const s3Params = bucketName + ? `¶m_EnableS3Integration=true¶m_S3IntegrationBucketName=${bucketName}` + : ""; + return { ...(links as { cloudformation: string; terraform: string; }), - cloudformationQuickLink: `https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateURL=https%3A%2F%2Fprowler-cloud-public.s3.eu-west-1.amazonaws.com%2Fpermissions%2Ftemplates%2Faws%2Fcloudformation%2Fprowler-scan-role.yml&stackName=Prowler¶m_ExternalId=${externalId}${bucketName ? `¶m_EnableS3Integration=true¶m_S3IntegrationBucketName=${bucketName}` : ""}`, + cloudformationQuickLink: + `${cfBaseUrl}?templateURL=${encodedTemplateUrl}` + + `&stackName=Prowler¶m_ExternalId=${externalId}${s3Params}`, + cloudformationOrgQuickLink: + `${cfBaseUrl}?templateURL=${encodedTemplateUrl}` + + `&stackName=Prowler¶m_ExternalId=${externalId}` + + `¶m_EnableOrganizations=true${s3Params}`, }; }; 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: