mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
Merge remote-tracking branch 'origin/master' into mintlify/55c1f1a5
This commit is contained in:
@@ -115,6 +115,13 @@ DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION=""
|
||||
# The name of the S3 bucket where scan output should be stored
|
||||
DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET=""
|
||||
|
||||
# The storage address the browser can reach, used only to sign report download URLs
|
||||
# (e.g. "https://storage.example.com"). Leave empty on AWS S3. Set it when storage is
|
||||
# only reachable inside the container network, such as MinIO on "http://minio:9000".
|
||||
# The reverse proxy in front of it must forward the Host header unchanged: SigV4 signs
|
||||
# Host, so rewriting it to the internal name invalidates the signature.
|
||||
DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL=""
|
||||
|
||||
# Django settings
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,prowler-api
|
||||
DJANGO_BIND_ADDRESS=0.0.0.0
|
||||
|
||||
@@ -451,6 +451,17 @@ modules:
|
||||
e2e:
|
||||
- ui/tests/home/**
|
||||
|
||||
- name: ui-registry
|
||||
match:
|
||||
- ui/actions/registry/**
|
||||
- ui/app/**/registry/**
|
||||
- ui/components/registry/**
|
||||
- ui/lib/registry/**
|
||||
- ui/tests/registry/**
|
||||
tests: []
|
||||
e2e:
|
||||
- ui/tests/registry/**
|
||||
|
||||
- name: ui-shadcn
|
||||
match:
|
||||
- ui/components/shadcn/**
|
||||
|
||||
@@ -10,12 +10,12 @@ on:
|
||||
- master
|
||||
- "v5.*"
|
||||
paths:
|
||||
- '.github/workflows/ui-e2e-tests-v2.yml'
|
||||
- '.github/test-impact.yml'
|
||||
- 'ui/**'
|
||||
- 'api/**' # API changes can affect UI E2E
|
||||
- '!ui/CHANGELOG.md'
|
||||
- '!api/CHANGELOG.md'
|
||||
- ".github/workflows/ui-e2e-tests-v2.yml"
|
||||
- ".github/test-impact.yml"
|
||||
- "ui/**"
|
||||
- "api/**" # API changes can affect UI E2E
|
||||
- "!ui/CHANGELOG.md"
|
||||
- "!api/CHANGELOG.md"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -40,11 +40,11 @@ jobs:
|
||||
(needs.impact-analysis.outputs.has-ui-e2e == 'true' || needs.impact-analysis.outputs.run-all == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
AUTH_SECRET: 'fallback-ci-secret-for-testing'
|
||||
AUTH_SECRET: "fallback-ci-secret-for-testing"
|
||||
AUTH_TRUST_HOST: true
|
||||
NEXTAUTH_URL: 'http://localhost:3000'
|
||||
AUTH_URL: 'http://localhost:3000'
|
||||
UI_API_BASE_URL: 'http://localhost:8080/api/v1'
|
||||
NEXTAUTH_URL: "http://localhost:3000"
|
||||
AUTH_URL: "http://localhost:3000"
|
||||
UI_API_BASE_URL: "http://localhost:8080/api/v1"
|
||||
E2E_ADMIN_USER: ${{ secrets.E2E_ADMIN_USER }}
|
||||
E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }}
|
||||
E2E_AWS_PROVIDER_ACCOUNT_ID: ${{ secrets.E2E_AWS_PROVIDER_ACCOUNT_ID }}
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
E2E_M365_SECRET_ID: ${{ secrets.E2E_M365_SECRET_ID }}
|
||||
E2E_M365_TENANT_ID: ${{ secrets.E2E_M365_TENANT_ID }}
|
||||
E2E_M365_CERTIFICATE_CONTENT: ${{ secrets.E2E_M365_CERTIFICATE_CONTENT }}
|
||||
E2E_KUBERNETES_CONTEXT: 'kind-kind'
|
||||
E2E_KUBERNETES_CONTEXT: "kind-kind"
|
||||
E2E_KUBERNETES_KUBECONFIG_PATH: /home/runner/.kube/config
|
||||
E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY: ${{ secrets.E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY }}
|
||||
E2E_GCP_PROJECT_ID: ${{ secrets.E2E_GCP_PROJECT_ID }}
|
||||
@@ -292,7 +292,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: 'ui/.nvmrc'
|
||||
node-version-file: "ui/.nvmrc"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
@@ -337,60 +337,59 @@ jobs:
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm run test:e2e:install
|
||||
|
||||
- name: Run E2E tests
|
||||
- name: Run standard E2E tests
|
||||
id: standard-e2e
|
||||
working-directory: ./ui
|
||||
run: |
|
||||
if [[ "${RUN_ALL_TESTS}" == "true" ]]; then
|
||||
echo "Running ALL E2E tests..."
|
||||
echo "Running all standard E2E tests..."
|
||||
pnpm run test:e2e
|
||||
else
|
||||
echo "Running targeted E2E tests: ${E2E_TEST_PATHS}"
|
||||
# Convert glob patterns to playwright test paths
|
||||
# e.g., "ui/tests/providers/**" -> "tests/providers"
|
||||
echo "Running targeted standard E2E tests: ${E2E_TEST_PATHS}"
|
||||
TEST_PATHS="${E2E_TEST_PATHS}"
|
||||
# Remove ui/ prefix and convert ** to empty (playwright handles recursion)
|
||||
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
|
||||
TEST_PATHS=$(echo "$TEST_PATHS" | grep -vE '^tests/(setups|registry)/' || true)
|
||||
|
||||
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
|
||||
[[ "$dir" == "tests/setups/" || "$dir" == "tests/registry/" ]] && 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=$(echo "$TEST_PATHS" | grep -vx 'tests/' || true)
|
||||
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
|
||||
# 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'
|
||||
while IFS= read -r path; do
|
||||
[[ -z "$path" ]] && continue
|
||||
if find "$path" -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | head -1 | grep -q .; then
|
||||
VALID_PATHS="${VALID_PATHS}${path}"$'\n'
|
||||
else
|
||||
echo "Skipping empty test directory: $p"
|
||||
echo "Skipping empty test directory: $path"
|
||||
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
|
||||
|
||||
if [[ -n "$VALID_PATHS" ]]; then
|
||||
TEST_PATHS=$(echo "$VALID_PATHS" | tr '\n' ' ')
|
||||
echo "Resolved standard test paths: $TEST_PATHS"
|
||||
read -ra test_paths <<< "$TEST_PATHS"
|
||||
pnpm exec playwright test "${test_paths[@]}"
|
||||
else
|
||||
echo "No standard E2E test paths selected."
|
||||
fi
|
||||
TEST_PATHS=$(echo "$VALID_PATHS" | tr '\n' ' ')
|
||||
echo "Resolved test paths: $TEST_PATHS"
|
||||
read -ra test_paths <<< "$TEST_PATHS"
|
||||
pnpm exec playwright test "${test_paths[@]}"
|
||||
fi
|
||||
|
||||
- name: Run Registry fixture E2E tests
|
||||
if: |
|
||||
!cancelled() &&
|
||||
(steps.standard-e2e.outcome == 'success' || steps.standard-e2e.outcome == 'failure') &&
|
||||
(env.RUN_ALL_TESTS == 'true' || contains(format(' {0} ', env.E2E_TEST_PATHS), ' ui/tests/registry/'))
|
||||
working-directory: ./ui
|
||||
run: pnpm run test:e2e:registry
|
||||
|
||||
- name: Upload test reports
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
if: failure()
|
||||
|
||||
+4
-30
@@ -125,41 +125,15 @@ vulnerabilities:
|
||||
purls:
|
||||
- "pkg:npm/fast-uri"
|
||||
expired_at: 2027-01-31
|
||||
- id: CVE-2026-75931
|
||||
purls:
|
||||
- "pkg:npm/fast-uri"
|
||||
expired_at: 2027-01-31
|
||||
- id: CVE-2026-69192
|
||||
purls:
|
||||
- "pkg:npm/ip-address"
|
||||
expired_at: 2027-01-31
|
||||
|
||||
# CVE-2026-62901 is a DoS in System.Net.WebSockets (unchecked input for loop condition,
|
||||
# CWE-606), fixed in .NET 9.0.19 / 10.0.11 (published 2026-08-11). The vulnerable runtime
|
||||
# ships inside the PowerShell tarball the Dockerfile pins: 7.5.9 is the latest 7.5.x and
|
||||
# bundles .NET 9.0.18; 7.6.4 bundles .NET 10.0.x < 10.0.11, so no published PowerShell
|
||||
# release contains the fix yet. Prowler only invokes pwsh locally to run M365 module
|
||||
# cmdlets; the image does not accept inbound WebSocket connections, so the DoS path is
|
||||
# not reachable from the network. Remove this temporary suppression as soon as a
|
||||
# PowerShell release shipping .NET 9.0.19+ is available.
|
||||
- id: CVE-2026-62901
|
||||
purls:
|
||||
- "pkg:nuget/Microsoft.NETCore.App.Runtime.linux-x64"
|
||||
- "pkg:nuget/Microsoft.NETCore.App.Runtime.linux-arm64"
|
||||
expired_at: 2026-09-15
|
||||
|
||||
# Modules compiled into the Trivy binary the images ship. The binary is pinned by version
|
||||
# and verified by checksum in the Dockerfile; only a rebuild by its vendor moves these.
|
||||
# CVE-2026-71556 affects go-git worktree operations that can follow symlinks outside a
|
||||
# cloned repository. Trivy 0.73.0, the latest published release and the version the
|
||||
# images ship, still pins that vulnerable version:
|
||||
# https://github.com/aquasecurity/trivy/blob/v0.73.0/go.mod#L46
|
||||
# Trivy main already contains the 5.19.2 fix, but no published release includes it yet:
|
||||
# https://github.com/aquasecurity/trivy/commit/a2edba9a03987ba0d2ebc8212c1a9a1e6979497b
|
||||
# Prowler invokes Trivy only with `fs` on an existing local path or with `image`; it does
|
||||
# not ask Trivy to clone or mutate a Git worktree, so the affected path is not reachable.
|
||||
# Remove this temporary suppression as soon as a fixed Trivy release is available.
|
||||
- id: CVE-2026-71556
|
||||
purls:
|
||||
- "pkg:golang/github.com/go-git/go-git/v5"
|
||||
expired_at: 2026-09-15
|
||||
|
||||
# CVE-2026-84304 is a DoS in grpc-go <= 1.83.0: a peer fragments a gRPC stream into
|
||||
# millions of tiny HTTP/2 DATA frames until the receiver runs out of heap. Fixed in
|
||||
# 1.83.1 (published 2026-09-01). Trivy 0.74.0, the latest published release and the
|
||||
|
||||
+13
-8
@@ -3,7 +3,7 @@ FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee56815788280
|
||||
LABEL maintainer="https://github.com/prowler-cloud/prowler"
|
||||
LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
|
||||
|
||||
ARG POWERSHELL_VERSION=7.5.9
|
||||
ARG POWERSHELL_VERSION=7.5.11
|
||||
ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
|
||||
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
|
||||
ENV POWERSHELL_TELEMETRY_OPTOUT=1
|
||||
@@ -17,25 +17,30 @@ ENV ZIZMOR_VERSION=${ZIZMOR_VERSION}
|
||||
# Pinned here, not fetched with the artefact: a compromised release ships its own checksum.
|
||||
ARG TRIVY_SHA256_AMD64=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
|
||||
ARG TRIVY_SHA256_ARM64=b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5
|
||||
ARG POWERSHELL_SHA256_AMD64=492ff26bb958336bf61e597ce19e07648b4003bd2a08659e02f0e3e0446ebfe0
|
||||
ARG POWERSHELL_SHA256_ARM64=2503b71da3e83635592b092df59a0aca4c3606b4d9b068217bb00be989cb0d56
|
||||
ARG POWERSHELL_SHA256_AMD64=82a8b13d92b0f3ae48e56cf2f3f7961679371736ca90145ca71617c2913ba9d8
|
||||
ARG POWERSHELL_SHA256_ARM64=830ebda118c731ece3fa7e6b7e8573a21346387cbbca5b2f5e3b9bfe24f96672
|
||||
ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03
|
||||
ARG ZIZMOR_SHA256_ARM64=d66e37ef8a375fb07939c630ebf9709a6e0f20242bdc3faf672a7ed97e0b768d
|
||||
|
||||
# High CVEs fixed in Debian trixie-security but not yet in the pinned base image:
|
||||
# High CVEs fixed in Debian trixie but not yet in the pinned base image:
|
||||
# openssl/libssl3t64/openssl-provider-legacy 3.5.7-1~deb13u2 CVE-2026-14456,
|
||||
# -14457, -18798, -54874, -63072, -63073, -63074, -63075, -63076, -75803
|
||||
# (image ships 3.5.6-1~deb13u2)
|
||||
# libsqlite3-0 3.46.1-7+deb13u2 CVE-2026-11822, -11824
|
||||
# gzip 1.13-1+deb13u1 CVE-2026-41992
|
||||
# perl-base 5.40.1-6+deb13u1 CVE-2026-42497, -48962, -57432
|
||||
# libssh2-1t64 1.11.1-1+deb13u2 CVE-2026-58050
|
||||
# libpcre2-8-0 10.46-1~deb13u2 CVE-2026-86145, -89161
|
||||
# Taken as a targeted --only-upgrade rather than by moving the digest: the newest
|
||||
# published python:3.12-slim-trixie carries the same vulnerable version. The three
|
||||
# packages are all built from openssl and are flagged separately, so all are named.
|
||||
# Drop them once the base image ships 3.5.7-1~deb13u2 or later.
|
||||
# published python:3.12-slim-trixie carries the same vulnerable versions. The three
|
||||
# openssl packages are flagged separately, so all are named.
|
||||
# Drop each one once the base image ships its fixed version.
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
wget libicu76 libunwind8 libssl3 libcurl4 ca-certificates apt-transport-https gnupg \
|
||||
build-essential pkg-config libzstd-dev zlib1g-dev \
|
||||
&& apt-get install -y --no-install-recommends --only-upgrade \
|
||||
util-linux libssl3t64 openssl openssl-provider-legacy \
|
||||
libsqlite3-0 gzip perl-base libssh2-1t64 libpcre2-8-0 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install PowerShell
|
||||
|
||||
@@ -126,12 +126,12 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/user-guide/compliance/tutorials/compliance) | [Categories](https://docs.prowler.com/user-guide/cli/tutorials/misc#categories) | Support | Interface |
|
||||
|---|---|---|---|---|---|---|
|
||||
| AWS | 639 | 86 | 47 | 19 | Official | UI, API, CLI |
|
||||
| Azure | 191 | 22 | 21 | 16 | Official | UI, API, CLI |
|
||||
| GCP | 109 | 20 | 19 | 12 | Official | UI, API, CLI |
|
||||
| Kubernetes | 92 | 7 | 8 | 11 | Official | UI, API, CLI |
|
||||
| AWS | 662 | 86 | 50 | 19 | Official | UI, API, CLI |
|
||||
| Azure | 191 | 22 | 25 | 16 | Official | UI, API, CLI |
|
||||
| GCP | 110 | 20 | 22 | 12 | Official | UI, API, CLI |
|
||||
| Kubernetes | 92 | 7 | 11 | 11 | Official | UI, API, CLI |
|
||||
| GitHub | 24 | 3 | 2 | 5 | Official | UI, API, CLI |
|
||||
| M365 | 143 | 10 | 6 | 10 | Official | UI, API, CLI |
|
||||
| M365 | 144 | 10 | 9 | 10 | Official | UI, API, CLI |
|
||||
| OCI | 52 | 14 | 5 | 10 | Official | UI, API, CLI |
|
||||
| Alibaba Cloud | 63 | 9 | 6 | 9 | Official | UI, API, CLI |
|
||||
| Cloudflare | 29 | 3 | 2 | 5 | Official | UI, API, CLI |
|
||||
|
||||
+13
-8
@@ -2,7 +2,7 @@ FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee56815788280
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud/api"
|
||||
|
||||
ARG POWERSHELL_VERSION=7.5.9
|
||||
ARG POWERSHELL_VERSION=7.5.11
|
||||
ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
|
||||
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
|
||||
ENV POWERSHELL_TELEMETRY_OPTOUT=1
|
||||
@@ -16,19 +16,23 @@ ENV ZIZMOR_VERSION=${ZIZMOR_VERSION}
|
||||
# Pinned here, not fetched with the artefact: a compromised release ships its own checksum.
|
||||
ARG TRIVY_SHA256_AMD64=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
|
||||
ARG TRIVY_SHA256_ARM64=b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5
|
||||
ARG POWERSHELL_SHA256_AMD64=492ff26bb958336bf61e597ce19e07648b4003bd2a08659e02f0e3e0446ebfe0
|
||||
ARG POWERSHELL_SHA256_ARM64=2503b71da3e83635592b092df59a0aca4c3606b4d9b068217bb00be989cb0d56
|
||||
ARG POWERSHELL_SHA256_AMD64=82a8b13d92b0f3ae48e56cf2f3f7961679371736ca90145ca71617c2913ba9d8
|
||||
ARG POWERSHELL_SHA256_ARM64=830ebda118c731ece3fa7e6b7e8573a21346387cbbca5b2f5e3b9bfe24f96672
|
||||
ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03
|
||||
ARG ZIZMOR_SHA256_ARM64=d66e37ef8a375fb07939c630ebf9709a6e0f20242bdc3faf672a7ed97e0b768d
|
||||
|
||||
# High CVEs fixed in Debian trixie-security but not yet in the pinned base image:
|
||||
# High CVEs fixed in Debian trixie but not yet in the pinned base image:
|
||||
# openssl/libssl3t64/openssl-provider-legacy 3.5.7-1~deb13u2 CVE-2026-14456,
|
||||
# -14457, -18798, -54874, -63072, -63073, -63074, -63075, -63076, -75803
|
||||
# (image ships 3.5.6-1~deb13u2)
|
||||
# libsqlite3-0 3.46.1-7+deb13u2 CVE-2026-11822, -11824
|
||||
# gzip 1.13-1+deb13u1 CVE-2026-41992
|
||||
# perl-base 5.40.1-6+deb13u1 CVE-2026-42497, -48962, -57432
|
||||
# libssh2-1t64 1.11.1-1+deb13u2 CVE-2026-58050
|
||||
# libpcre2-8-0 10.46-1~deb13u2 CVE-2026-86145, -89161
|
||||
# Taken as a targeted --only-upgrade rather than by moving the digest: the newest
|
||||
# published python:3.12-slim-trixie carries the same vulnerable version. The three
|
||||
# packages are all built from openssl and are flagged separately, so all are named.
|
||||
# Drop them once the base image ships 3.5.7-1~deb13u2 or later.
|
||||
# published python:3.12-slim-trixie carries the same vulnerable versions. The three
|
||||
# openssl packages are flagged separately, so all are named.
|
||||
# Drop each one once the base image ships its fixed version.
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
wget \
|
||||
@@ -46,6 +50,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3-dev \
|
||||
&& apt-get install -y --no-install-recommends --only-upgrade \
|
||||
util-linux libssl3t64 openssl openssl-provider-legacy \
|
||||
libsqlite3-0 gzip perl-base libssh2-1t64 libpcre2-8-0 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install PowerShell
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
`libsqlite3-0`, `gzip`, `perl-base` and `libpcre2-8-0` upgraded in the API container image, patching high Debian CVEs
|
||||
@@ -0,0 +1 @@
|
||||
PowerShell from 7.5.9 to 7.5.11 in the API container image, bundling .NET runtime 9.0.20 and patching CVE-2026-62901
|
||||
@@ -0,0 +1 @@
|
||||
Lapsed pending invitations are reported as expired and no longer block a new invitation for the same email
|
||||
@@ -0,0 +1 @@
|
||||
Report download URLs can be signed against a browser-reachable storage host via `DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL`, so downloads complete on deployments where storage is only reachable inside the container network
|
||||
@@ -0,0 +1 @@
|
||||
A scan report download no longer fails with a server error when `DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION` is unset, which is common on storage with no meaningful region
|
||||
@@ -1439,8 +1439,20 @@ class InvitationFilter(FilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
|
||||
expires_at = DateFilter(field_name="expires_at", lookup_expr="date")
|
||||
state = ChoiceFilter(choices=Invitation.State.choices)
|
||||
state__in = ChoiceInFilter(choices=Invitation.State.choices, lookup_expr="in")
|
||||
state = ChoiceFilter(choices=Invitation.State.choices, method="filter_state")
|
||||
state__in = ChoiceInFilter(
|
||||
choices=Invitation.State.choices, lookup_expr="in", method="filter_state_in"
|
||||
)
|
||||
|
||||
def filter_state(self, queryset, name, value):
|
||||
return self.filter_state_in(queryset, name, [value])
|
||||
|
||||
def filter_state_in(self, queryset, name, value):
|
||||
lapsed = Invitation.lapsed_q()
|
||||
query = Q(state__in=value) & ~lapsed
|
||||
if Invitation.State.EXPIRED in value:
|
||||
query |= lapsed
|
||||
return queryset.filter(query)
|
||||
|
||||
class Meta:
|
||||
model = Invitation
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import uuid
|
||||
|
||||
import api.rls
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0097_attack_paths_scan_db_defaults"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="TenantOnboardingProfile",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"declared_cloud_accounts",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("1", "1"),
|
||||
("2-10", "2-10"),
|
||||
("11-50", "11-50"),
|
||||
("51-200", "51-200"),
|
||||
("200+", "200+"),
|
||||
],
|
||||
max_length=16,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"declared_role",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("security", "Security"),
|
||||
("devops_platform", "DevOps / Platform"),
|
||||
("developer", "Developer"),
|
||||
("compliance_grc", "Compliance / GRC"),
|
||||
("other", "Other"),
|
||||
],
|
||||
max_length=32,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"declared_seniority",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("practitioner", "Practitioner / IC"),
|
||||
("lead", "Team lead / Manager"),
|
||||
("director", "Director / Head of"),
|
||||
("executive", "VP / C-level"),
|
||||
("founder", "Founder / Owner"),
|
||||
],
|
||||
max_length=32,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
("skipped", models.BooleanField(default=False)),
|
||||
(
|
||||
"submitted_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="tenant_onboarding_profiles",
|
||||
related_query_name="tenant_onboarding_profile",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "tenant_onboarding_profiles",
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="tenantonboardingprofile",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("tenant_id",), name="unique_tenant_onboarding_profile"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="tenantonboardingprofile",
|
||||
# `statements` written out explicitly: RowLevelSecurityConstraint
|
||||
# .deconstruct() does not serialize it, so an autogenerated
|
||||
# migration falls back to ["SELECT"] and leaves the table without
|
||||
# INSERT/UPDATE/DELETE policies.
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_tenantonboardingprofile",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
# The onboarding profile step was reverted after 0098 had been merged, so
|
||||
# the table goes away through a new migration rather than by deleting 0098.
|
||||
dependencies = [
|
||||
("api", "0098_tenant_onboarding_profile"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="TenantOnboardingProfile",
|
||||
),
|
||||
]
|
||||
@@ -1380,6 +1380,15 @@ class Invitation(RowLevelSecurityProtectedModel):
|
||||
self.email = self.email.strip().lower()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def lapsed_q(cls):
|
||||
"""Pending invitations whose expiry date has already passed."""
|
||||
return Q(state=cls.State.PENDING, expires_at__lte=datetime.now(UTC))
|
||||
|
||||
@property
|
||||
def is_lapsed(self):
|
||||
return self.state == self.State.PENDING and self.expires_at <= datetime.now(UTC)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "invitations"
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ from django.db import close_old_connections, connection, connections
|
||||
from django.db.models import Count
|
||||
from django.db.models.signals import pre_delete
|
||||
from django.http import JsonResponse
|
||||
from django.test import RequestFactory
|
||||
from django.test import RequestFactory, override_settings
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from django.urls import reverse
|
||||
from django_celery_results.models import TaskResult
|
||||
@@ -4540,6 +4540,52 @@ class TestScanViewSet:
|
||||
assert response.status_code == status.HTTP_302_FOUND
|
||||
assert response["Location"] == presigned_url
|
||||
|
||||
@override_settings(
|
||||
DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL="https://storage.example.com",
|
||||
DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID="access-key",
|
||||
DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY="secret-key",
|
||||
DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN="",
|
||||
DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION="eu-west-1",
|
||||
)
|
||||
def test_report_s3_redirects_to_the_public_storage_host(
|
||||
self, authenticated_client, scans_fixture, monkeypatch
|
||||
):
|
||||
"""The object is looked up internally but the redirect the browser follows is public."""
|
||||
scan = scans_fixture[0]
|
||||
bucket = "test-bucket"
|
||||
key = "report.zip"
|
||||
scan.output_location = f"s3://{bucket}/{key}"
|
||||
scan.state = StateChoices.COMPLETED
|
||||
scan.save()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"api.v1.views.env",
|
||||
type("env", (), {"str": lambda self, *_args, **_kwargs: bucket})(),
|
||||
)
|
||||
|
||||
head_calls = []
|
||||
|
||||
class InternalS3Client:
|
||||
def head_object(self, Bucket, Key):
|
||||
head_calls.append((Bucket, Key))
|
||||
return {}
|
||||
|
||||
def generate_presigned_url(self, *_args, **_kwargs):
|
||||
raise AssertionError("the internal client must not sign the redirect")
|
||||
|
||||
monkeypatch.setattr("api.v1.views.get_s3_client", lambda: InternalS3Client())
|
||||
|
||||
url = reverse("scan-report", kwargs={"pk": scan.id})
|
||||
response = authenticated_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_302_FOUND
|
||||
assert head_calls == [(bucket, key)]
|
||||
|
||||
location = urlparse(response["Location"])
|
||||
assert location.netloc == "storage.example.com"
|
||||
assert location.path == f"/{bucket}/{key}"
|
||||
assert "X-Amz-Signature" in parse_qs(location.query)
|
||||
|
||||
def test_report_s3_success_no_local_files(
|
||||
self, authenticated_client, scans_fixture, monkeypatch
|
||||
):
|
||||
@@ -8784,6 +8830,190 @@ class TestInvitationViewSet:
|
||||
user.id
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _invitation_create_payload(email, role):
|
||||
return json.dumps(
|
||||
{
|
||||
"data": {
|
||||
"type": "invitations",
|
||||
"attributes": {"email": email},
|
||||
"relationships": {
|
||||
"roles": {"data": [{"type": "roles", "id": str(role.id)}]}
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _create_lapsed_invitation(email, tenant, inviter):
|
||||
return Invitation.objects.create(
|
||||
email=email,
|
||||
state=Invitation.State.PENDING,
|
||||
expires_at=datetime.now(UTC) - timedelta(days=1),
|
||||
inviter=inviter,
|
||||
tenant=tenant,
|
||||
)
|
||||
|
||||
def test_invitations_create_with_lapsed_pending_invitation_for_same_email(
|
||||
self,
|
||||
authenticated_client,
|
||||
create_test_user,
|
||||
tenants_fixture,
|
||||
invitations_fixture,
|
||||
roles_fixture,
|
||||
):
|
||||
lapsed_invitation, expired_invitation = invitations_fixture
|
||||
lapsed_invitation.expires_at = datetime.now(UTC) - timedelta(days=1)
|
||||
lapsed_invitation.save()
|
||||
other_email_lapsed_invitation = self._create_lapsed_invitation(
|
||||
"other@prowler.com", tenants_fixture[0], create_test_user
|
||||
)
|
||||
|
||||
response = authenticated_client.post(
|
||||
reverse("invitation-list"),
|
||||
data=self._invitation_create_payload(
|
||||
lapsed_invitation.email, roles_fixture[0]
|
||||
),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
new_invitation = Invitation.objects.get(id=response.json()["data"]["id"])
|
||||
assert new_invitation.email == lapsed_invitation.email
|
||||
assert new_invitation.state == Invitation.State.PENDING
|
||||
lapsed_invitation.refresh_from_db()
|
||||
assert lapsed_invitation.state == Invitation.State.EXPIRED
|
||||
expired_invitation.refresh_from_db()
|
||||
assert expired_invitation.state == Invitation.State.EXPIRED
|
||||
other_email_lapsed_invitation.refresh_from_db()
|
||||
assert other_email_lapsed_invitation.state == Invitation.State.PENDING
|
||||
|
||||
def test_invitations_create_with_active_pending_invitation_for_same_email(
|
||||
self,
|
||||
authenticated_client,
|
||||
create_test_user,
|
||||
tenants_fixture,
|
||||
invitations_fixture,
|
||||
roles_fixture,
|
||||
):
|
||||
active_invitation, _ = invitations_fixture
|
||||
self._create_lapsed_invitation(
|
||||
active_invitation.email, tenants_fixture[0], create_test_user
|
||||
)
|
||||
invitation_count = Invitation.objects.count()
|
||||
|
||||
response = authenticated_client.post(
|
||||
reverse("invitation-list"),
|
||||
data=self._invitation_create_payload(
|
||||
active_invitation.email, roles_fixture[0]
|
||||
),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert (
|
||||
response.json()["errors"][0]["source"]["pointer"]
|
||||
== "/data/attributes/email"
|
||||
)
|
||||
assert Invitation.objects.count() == invitation_count
|
||||
active_invitation.refresh_from_db()
|
||||
assert active_invitation.state == Invitation.State.PENDING
|
||||
|
||||
def test_invitations_create_ignores_pending_invitations_from_other_tenants(
|
||||
self, authenticated_client, create_test_user, tenants_fixture, roles_fixture
|
||||
):
|
||||
email = "cross_tenant@prowler.com"
|
||||
other_tenant = tenants_fixture[1]
|
||||
other_tenant_lapsed_invitation = self._create_lapsed_invitation(
|
||||
email, other_tenant, create_test_user
|
||||
)
|
||||
Invitation.objects.create(
|
||||
email=email, inviter=create_test_user, tenant=other_tenant
|
||||
)
|
||||
|
||||
response = authenticated_client.post(
|
||||
reverse("invitation-list"),
|
||||
data=self._invitation_create_payload(email, roles_fixture[0]),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
other_tenant_lapsed_invitation.refresh_from_db()
|
||||
assert other_tenant_lapsed_invitation.state == Invitation.State.PENDING
|
||||
|
||||
def test_invitations_report_lapsed_pending_invitation_as_expired(
|
||||
self,
|
||||
authenticated_client,
|
||||
create_test_user,
|
||||
tenants_fixture,
|
||||
invitations_fixture,
|
||||
):
|
||||
active_invitation, expired_invitation = invitations_fixture
|
||||
lapsed_invitation = self._create_lapsed_invitation(
|
||||
"lapsed@prowler.com", tenants_fixture[0], create_test_user
|
||||
)
|
||||
|
||||
list_response = authenticated_client.get(reverse("invitation-list"))
|
||||
retrieve_response = authenticated_client.get(
|
||||
reverse("invitation-detail", kwargs={"pk": lapsed_invitation.id})
|
||||
)
|
||||
|
||||
assert list_response.status_code == status.HTTP_200_OK
|
||||
assert retrieve_response.status_code == status.HTTP_200_OK
|
||||
assert {
|
||||
invitation["id"]: invitation["attributes"]["state"]
|
||||
for invitation in list_response.json()["data"]
|
||||
} == {
|
||||
str(active_invitation.id): Invitation.State.PENDING.value,
|
||||
str(expired_invitation.id): Invitation.State.EXPIRED.value,
|
||||
str(lapsed_invitation.id): Invitation.State.EXPIRED.value,
|
||||
}
|
||||
assert (
|
||||
retrieve_response.json()["data"]["attributes"]["state"]
|
||||
== Invitation.State.EXPIRED.value
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filter_name, filter_value, expected_invitations",
|
||||
[
|
||||
("state", "pending", {"active"}),
|
||||
("state", "expired", {"expired", "lapsed"}),
|
||||
("state", "accepted", set()),
|
||||
("state__in", "pending", {"active"}),
|
||||
("state__in", "expired", {"expired", "lapsed"}),
|
||||
("state__in", "pending,expired", {"active", "expired", "lapsed"}),
|
||||
("state__in", "accepted,revoked", set()),
|
||||
],
|
||||
)
|
||||
def test_invitations_filter_state_treats_lapsed_pending_as_expired(
|
||||
self,
|
||||
authenticated_client,
|
||||
create_test_user,
|
||||
tenants_fixture,
|
||||
invitations_fixture,
|
||||
filter_name,
|
||||
filter_value,
|
||||
expected_invitations,
|
||||
):
|
||||
active_invitation, expired_invitation = invitations_fixture
|
||||
lapsed_invitation = self._create_lapsed_invitation(
|
||||
"lapsed@prowler.com", tenants_fixture[0], create_test_user
|
||||
)
|
||||
invitation_ids = {
|
||||
"active": str(active_invitation.id),
|
||||
"expired": str(expired_invitation.id),
|
||||
"lapsed": str(lapsed_invitation.id),
|
||||
}
|
||||
|
||||
response = authenticated_client.get(
|
||||
reverse("invitation-list"), {f"filter[{filter_name}]": filter_value}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert {invitation["id"] for invitation in response.json()["data"]} == {
|
||||
invitation_ids[name] for name in expected_invitations
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"email",
|
||||
[
|
||||
@@ -8791,8 +9021,10 @@ class TestInvitationViewSet:
|
||||
"invalid_email@",
|
||||
# There is a pending invitation with this email
|
||||
"testing@prowler.com",
|
||||
"TESTING@prowler.com",
|
||||
# User is already a member of the tenant
|
||||
TEST_USER,
|
||||
TEST_USER.upper(),
|
||||
],
|
||||
)
|
||||
def test_invitations_create_invalid_email(
|
||||
@@ -9047,6 +9279,56 @@ class TestInvitationViewSet:
|
||||
== "This invitation cannot be revoked."
|
||||
)
|
||||
|
||||
def test_invitations_delete_lapsed_invitation(
|
||||
self, authenticated_client, invitations_fixture
|
||||
):
|
||||
invitation, *_ = invitations_fixture
|
||||
invitation.expires_at = datetime.now(UTC) - timedelta(days=1)
|
||||
invitation.save()
|
||||
|
||||
response = authenticated_client.delete(
|
||||
reverse("invitation-detail", kwargs={"pk": str(invitation.id)})
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert (
|
||||
response.json()["errors"][0]["detail"]
|
||||
== "This invitation cannot be revoked."
|
||||
)
|
||||
invitation.refresh_from_db()
|
||||
assert invitation.state == Invitation.State.PENDING
|
||||
|
||||
def test_invitations_partial_update_lapsed_invitation(
|
||||
self, authenticated_client, invitations_fixture
|
||||
):
|
||||
invitation, *_ = invitations_fixture
|
||||
invitation.expires_at = datetime.now(UTC) - timedelta(days=1)
|
||||
invitation.save()
|
||||
data = {
|
||||
"data": {
|
||||
"id": str(invitation.id),
|
||||
"type": "invitations",
|
||||
"attributes": {
|
||||
"email": invitation.email,
|
||||
"expires_at": self.TOMORROW_ISO,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response = authenticated_client.patch(
|
||||
reverse("invitation-detail", kwargs={"pk": str(invitation.id)}),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert (
|
||||
response.json()["errors"][0]["detail"]
|
||||
== "This invitation cannot be updated."
|
||||
)
|
||||
invitation.refresh_from_db()
|
||||
assert invitation.is_lapsed
|
||||
|
||||
def test_invitations_accept_invitation_new_user(self, client, invitations_fixture):
|
||||
invitation, *_ = invitations_fixture
|
||||
|
||||
|
||||
@@ -2149,6 +2149,12 @@ class InvitationSerializer(RLSSerializer):
|
||||
if tenant_id is not None:
|
||||
self.fields["roles"].queryset = Role.objects.filter(tenant_id=tenant_id)
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
if instance.is_lapsed:
|
||||
data["state"] = Invitation.State.EXPIRED.value
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
model = Invitation
|
||||
fields = [
|
||||
@@ -2175,6 +2181,7 @@ class InvitationBaseWriteSerializer(BaseWriteSerializer):
|
||||
self.fields["roles"].queryset = Role.objects.filter(tenant_id=tenant_id)
|
||||
|
||||
def validate_email(self, value):
|
||||
value = value.strip().lower()
|
||||
user = User.objects.filter(email=value).first()
|
||||
tenant_id = self.context["tenant_id"]
|
||||
if user and Membership.objects.filter(user=user, tenant=tenant_id).exists():
|
||||
@@ -2182,9 +2189,13 @@ class InvitationBaseWriteSerializer(BaseWriteSerializer):
|
||||
"The user may already be a member of the tenant or there was an issue with the "
|
||||
"email provided."
|
||||
)
|
||||
if Invitation.objects.filter(
|
||||
email=value, state=Invitation.State.PENDING
|
||||
).exists():
|
||||
pending_invitations = Invitation.objects.filter(
|
||||
tenant_id=tenant_id, email=value, state=Invitation.State.PENDING
|
||||
)
|
||||
pending_invitations.filter(Invitation.lapsed_q()).update(
|
||||
state=Invitation.State.EXPIRED
|
||||
)
|
||||
if pending_invitations.filter(expires_at__gt=datetime.now(UTC)).exists():
|
||||
raise ValidationError(
|
||||
"Unable to process your request. Please check the information provided and "
|
||||
"try again."
|
||||
|
||||
@@ -326,7 +326,7 @@ from rest_framework_simplejwt.token_blacklist.models import (
|
||||
)
|
||||
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.jobs.export import get_s3_client, get_s3_presign_client
|
||||
from tasks.tasks import (
|
||||
QUEUED_SCAN_TASK_STATE,
|
||||
backfill_compliance_summaries_task,
|
||||
@@ -2407,7 +2407,8 @@ class ScanViewSet(ProviderVisibilityMixin, BaseRLSViewSet):
|
||||
}
|
||||
if content_type:
|
||||
params["ResponseContentType"] = content_type
|
||||
url = client.generate_presigned_url(
|
||||
# The browser follows this URL, so it is signed against the public host.
|
||||
url = (get_s3_presign_client() or client).generate_presigned_url(
|
||||
"get_object",
|
||||
Params=params,
|
||||
ExpiresIn=300,
|
||||
@@ -4471,7 +4472,7 @@ class InvitationViewSet(BaseRLSViewSet):
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
if instance.state != Invitation.State.PENDING:
|
||||
if instance.state != Invitation.State.PENDING or instance.is_lapsed:
|
||||
raise ValidationError(detail="This invitation cannot be updated.")
|
||||
serializer = self.get_serializer(
|
||||
instance,
|
||||
@@ -4485,7 +4486,7 @@ class InvitationViewSet(BaseRLSViewSet):
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
if instance.state != Invitation.State.PENDING:
|
||||
if instance.state != Invitation.State.PENDING or instance.is_lapsed:
|
||||
raise ValidationError(detail="This invitation cannot be revoked.")
|
||||
instance.state = Invitation.State.REVOKED
|
||||
instance.save()
|
||||
|
||||
@@ -295,6 +295,11 @@ DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY = env.str(
|
||||
)
|
||||
DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN = env.str("DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN", "")
|
||||
DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION = env.str("DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION", "")
|
||||
# Browser-reachable storage host used to sign download URLs. Empty means sign against the
|
||||
# same endpoint the API talks to, which is what Prowler Cloud on S3 does.
|
||||
DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL = env.str(
|
||||
"DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL", ""
|
||||
)
|
||||
|
||||
# HTTP Security Headers
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
|
||||
@@ -6,6 +6,7 @@ import boto3
|
||||
import config.django.base as base
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Scan
|
||||
from botocore.config import Config
|
||||
from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.conf import settings
|
||||
@@ -222,7 +223,9 @@ def get_s3_client():
|
||||
aws_access_key_id=settings.DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY,
|
||||
aws_session_token=settings.DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN,
|
||||
region_name=settings.DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION,
|
||||
# Storage that has no meaningful region, MinIO among it, is usually configured
|
||||
# without one, and botocore rejects an empty region before any request is made.
|
||||
region_name=settings.DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION or "us-east-1",
|
||||
)
|
||||
s3_client.list_buckets()
|
||||
except (ClientError, NoCredentialsError, ParamValidationError, ValueError):
|
||||
@@ -232,6 +235,44 @@ def get_s3_client():
|
||||
return s3_client
|
||||
|
||||
|
||||
def get_s3_presign_client():
|
||||
"""Return a client that signs URLs against the public storage host.
|
||||
|
||||
None means no public host is configured and the caller should presign with its own
|
||||
client, which leaves deployments on real S3 with the URL they get today.
|
||||
"""
|
||||
public_endpoint = settings.DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL
|
||||
if not public_endpoint:
|
||||
return None
|
||||
|
||||
# Blank keys are signed as-is (empty credential scope) instead of deferring to the
|
||||
# provider chain, so static credentials are only passed when they are set.
|
||||
credentials = {}
|
||||
if (
|
||||
settings.DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID
|
||||
and settings.DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY
|
||||
):
|
||||
credentials = {
|
||||
"aws_access_key_id": settings.DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID,
|
||||
"aws_secret_access_key": settings.DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY,
|
||||
# An empty string is a token as far as botocore is concerned: it appends an
|
||||
# empty X-Amz-Security-Token that storage counts when it recomputes the signature.
|
||||
"aws_session_token": settings.DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN or None,
|
||||
}
|
||||
|
||||
return boto3.client(
|
||||
"s3",
|
||||
**credentials,
|
||||
# SigV4 puts the region in the credential scope, and MinIO answers to us-east-1
|
||||
# unless it was told otherwise, so an empty region would sign an unusable URL.
|
||||
region_name=settings.DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION or "us-east-1",
|
||||
endpoint_url=public_endpoint,
|
||||
# The signature covers the host, so the addressing style has to be pinned rather
|
||||
# than guessed from the endpoint: MinIO serves path-style.
|
||||
config=Config(signature_version="s3v4", s3={"addressing_style": "path"}),
|
||||
)
|
||||
|
||||
|
||||
def _upload_to_s3(
|
||||
tenant_id: str, scan_id: str, local_path: str, relative_key: str
|
||||
) -> str | None:
|
||||
|
||||
@@ -4,15 +4,18 @@ import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from django.test import override_settings
|
||||
from tasks.jobs.export import (
|
||||
_compress_output_files,
|
||||
_generate_compliance_output_directory,
|
||||
_generate_output_directory,
|
||||
_upload_to_s3,
|
||||
get_s3_client,
|
||||
get_s3_presign_client,
|
||||
)
|
||||
|
||||
|
||||
@@ -47,6 +50,19 @@ class TestOutputs:
|
||||
assert client is not None
|
||||
client_mock.list_buckets.assert_called()
|
||||
|
||||
@patch("tasks.jobs.export.boto3.client")
|
||||
@override_settings(
|
||||
DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID="access-key",
|
||||
DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY="secret-key",
|
||||
DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN="",
|
||||
DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION="",
|
||||
)
|
||||
def test_get_s3_client_without_a_region_uses_a_default(self, mock_boto_client):
|
||||
"""botocore rejects an empty region up front, and the download views do not catch it."""
|
||||
get_s3_client()
|
||||
|
||||
assert mock_boto_client.call_args.kwargs["region_name"] == "us-east-1"
|
||||
|
||||
@patch("tasks.jobs.export.boto3.client")
|
||||
@patch("tasks.jobs.export.settings")
|
||||
def test_get_s3_client_fallback(self, mock_settings, mock_boto_client):
|
||||
@@ -243,3 +259,107 @@ class TestOutputs:
|
||||
assert os.path.isdir(os.path.dirname(ens))
|
||||
assert threatscore.endswith(f"aws-test-check-{expected_timestamp}")
|
||||
assert ens.endswith(f"aws-test-check-{expected_timestamp}")
|
||||
|
||||
|
||||
PRESIGN_SETTINGS = {
|
||||
"DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID": "access-key",
|
||||
"DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY": "secret-key",
|
||||
"DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN": "",
|
||||
"DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION": "eu-west-1",
|
||||
}
|
||||
|
||||
|
||||
def _presign(client):
|
||||
return client.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": "output-bucket", "Key": "tenant/scan/report.zip"},
|
||||
ExpiresIn=300,
|
||||
)
|
||||
|
||||
|
||||
class TestS3PresignClient:
|
||||
@override_settings(**PRESIGN_SETTINGS, DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL="")
|
||||
def test_no_public_endpoint_returns_none(self):
|
||||
assert get_s3_presign_client() is None
|
||||
|
||||
@override_settings(
|
||||
**PRESIGN_SETTINGS,
|
||||
DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL="https://storage.example.com",
|
||||
)
|
||||
def test_url_targets_the_public_host_in_path_style(self):
|
||||
url = urlparse(_presign(get_s3_presign_client()))
|
||||
|
||||
assert url.netloc == "storage.example.com"
|
||||
assert url.path == "/output-bucket/tenant/scan/report.zip"
|
||||
|
||||
@override_settings(
|
||||
**PRESIGN_SETTINGS,
|
||||
DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL="https://storage.example.com",
|
||||
)
|
||||
def test_signature_covers_the_public_host(self):
|
||||
query = parse_qs(urlparse(_presign(get_s3_presign_client())).query)
|
||||
|
||||
assert query["X-Amz-SignedHeaders"] == ["host"]
|
||||
assert "/eu-west-1/s3/aws4_request" in query["X-Amz-Credential"][0]
|
||||
|
||||
@override_settings(
|
||||
**PRESIGN_SETTINGS,
|
||||
DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL="https://storage.example.com",
|
||||
)
|
||||
def test_signature_is_bound_to_the_host_it_was_signed_against(self):
|
||||
"""Rewriting the host afterwards cannot work, which is why the endpoint is a setting."""
|
||||
public = parse_qs(urlparse(_presign(get_s3_presign_client())).query)
|
||||
|
||||
with override_settings(
|
||||
DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL="http://minio:9000"
|
||||
):
|
||||
internal = parse_qs(urlparse(_presign(get_s3_presign_client())).query)
|
||||
|
||||
assert public["X-Amz-Signature"] != internal["X-Amz-Signature"]
|
||||
|
||||
@override_settings(
|
||||
**{**PRESIGN_SETTINGS, "DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION": ""},
|
||||
DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL="https://storage.example.com",
|
||||
)
|
||||
def test_region_falls_back_to_the_minio_default(self):
|
||||
query = parse_qs(urlparse(_presign(get_s3_presign_client())).query)
|
||||
|
||||
assert "/us-east-1/s3/aws4_request" in query["X-Amz-Credential"][0]
|
||||
|
||||
@override_settings(
|
||||
**PRESIGN_SETTINGS,
|
||||
DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL="https://storage.example.com",
|
||||
)
|
||||
def test_unset_session_token_is_left_out_of_the_url(self):
|
||||
"""An empty token still reaches the URL as a blank param that storage signs over."""
|
||||
url = _presign(get_s3_presign_client())
|
||||
query = parse_qs(urlparse(url).query, keep_blank_values=True)
|
||||
|
||||
assert "X-Amz-Security-Token" not in query
|
||||
|
||||
@override_settings(
|
||||
**{**PRESIGN_SETTINGS, "DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN": "session-token"},
|
||||
DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL="https://storage.example.com",
|
||||
)
|
||||
def test_session_token_is_forwarded_when_set(self):
|
||||
query = parse_qs(urlparse(_presign(get_s3_presign_client())).query)
|
||||
|
||||
assert query["X-Amz-Security-Token"] == ["session-token"]
|
||||
|
||||
@override_settings(
|
||||
**{
|
||||
**PRESIGN_SETTINGS,
|
||||
"DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID": "",
|
||||
"DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY": "",
|
||||
},
|
||||
DJANGO_OUTPUT_S3_AWS_PUBLIC_ENDPOINT_URL="https://storage.example.com",
|
||||
)
|
||||
def test_blank_static_credentials_defer_to_the_provider_chain(self, monkeypatch):
|
||||
"""Empty keys would otherwise be signed as-is, yielding a blank credential scope."""
|
||||
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "chain-key")
|
||||
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "chain-secret")
|
||||
monkeypatch.delenv("AWS_SESSION_TOKEN", raising=False)
|
||||
|
||||
query = parse_qs(urlparse(_presign(get_s3_presign_client())).query)
|
||||
|
||||
assert query["X-Amz-Credential"][0].startswith("chain-key/")
|
||||
|
||||
@@ -212,6 +212,14 @@ mainConfig:
|
||||
# MEDIUM
|
||||
ecr_repository_vulnerability_minimum_severity: "MEDIUM"
|
||||
|
||||
# AWS Inspector2
|
||||
# aws.inspector2_coverage_recently_scanned
|
||||
# Maximum days since Inspector2 last scanned an actively covered resource
|
||||
inspector2_max_days_since_last_scan: 3
|
||||
# aws.inspector2_active_findings_within_max_age
|
||||
# Maximum days an Inspector2 finding can stay active since it was first observed
|
||||
inspector2_active_finding_max_age_days: 192
|
||||
|
||||
# AWS Trusted Advisor
|
||||
# aws.trustedadvisor_premium_support_plan_subscribed
|
||||
verify_premium_support_plans: True
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_cis
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
# Shorten the long FedRAMP KSI descriptions for better display
|
||||
ksi_short_names = {
|
||||
"A secure cloud service offering will protect user data, control access, and apply zero trust principles": "Identity and Access Management",
|
||||
"A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system": "Cloud Native Architecture",
|
||||
"A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly": "Change Management",
|
||||
"A secure cloud service provider will continuously educate their employees on cybersecurity measures, testing them regularly": "Cybersecurity Education",
|
||||
"A secure cloud service offering will document, report, and analyze security incidents to ensure regulatory compliance and continuous security improvement": "Incident Reporting",
|
||||
"A secure cloud service offering will monitor, log, and audit all important events, activity, and changes": "Monitoring, Logging, and Auditing",
|
||||
"A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured": "Policy and Inventory",
|
||||
"A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss": "Recovery Planning",
|
||||
"A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources": "Service Configuration",
|
||||
"A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources": "Third-Party Information Resources",
|
||||
}
|
||||
|
||||
# Replace long descriptions with short names - use contains for partial matching
|
||||
if not aux.empty:
|
||||
for long_desc, short_name in ksi_short_names.items():
|
||||
mask = aux["REQUIREMENTS_DESCRIPTION"].str.contains(
|
||||
long_desc, na=False, regex=False
|
||||
)
|
||||
aux.loc[mask, "REQUIREMENTS_DESCRIPTION"] = short_name
|
||||
|
||||
return get_section_containers_cis(
|
||||
aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
)
|
||||
@@ -1,46 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_cis
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
# Shorten the long FedRAMP KSI descriptions for better display
|
||||
ksi_short_names = {
|
||||
"A secure cloud service offering will protect user data, control access, and apply zero trust principles": "Identity and Access Management",
|
||||
"A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system": "Cloud Native Architecture",
|
||||
"A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly": "Change Management",
|
||||
"A secure cloud service provider will continuously educate their employees on cybersecurity measures, testing them regularly": "Cybersecurity Education",
|
||||
"A secure cloud service offering will document, report, and analyze security incidents to ensure regulatory compliance and continuous security improvement": "Incident Reporting",
|
||||
"A secure cloud service offering will monitor, log, and audit all important events, activity, and changes": "Monitoring, Logging, and Auditing",
|
||||
"A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured": "Policy and Inventory",
|
||||
"A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss": "Recovery Planning",
|
||||
"A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources": "Service Configuration",
|
||||
"A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources": "Third-Party Information Resources",
|
||||
}
|
||||
|
||||
# Replace long descriptions with short names - use contains for partial matching
|
||||
if not aux.empty:
|
||||
for long_desc, short_name in ksi_short_names.items():
|
||||
mask = aux["REQUIREMENTS_DESCRIPTION"].str.contains(
|
||||
long_desc, na=False, regex=False
|
||||
)
|
||||
aux.loc[mask, "REQUIREMENTS_DESCRIPTION"] = short_name
|
||||
|
||||
return get_section_containers_cis(
|
||||
aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
)
|
||||
@@ -1,46 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_cis
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
# Shorten the long FedRAMP KSI descriptions for better display
|
||||
ksi_short_names = {
|
||||
"A secure cloud service offering will protect user data, control access, and apply zero trust principles": "Identity and Access Management",
|
||||
"A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system": "Cloud Native Architecture",
|
||||
"A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly": "Change Management",
|
||||
"A secure cloud service provider will continuously educate their employees on cybersecurity measures, testing them regularly": "Cybersecurity Education",
|
||||
"A secure cloud service offering will document, report, and analyze security incidents to ensure regulatory compliance and continuous security improvement": "Incident Reporting",
|
||||
"A secure cloud service offering will monitor, log, and audit all important events, activity, and changes": "Monitoring, Logging, and Auditing",
|
||||
"A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured": "Policy and Inventory",
|
||||
"A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss": "Recovery Planning",
|
||||
"A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources": "Service Configuration",
|
||||
"A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources": "Third-Party Information Resources",
|
||||
}
|
||||
|
||||
# Replace long descriptions with short names - use contains for partial matching
|
||||
if not aux.empty:
|
||||
for long_desc, short_name in ksi_short_names.items():
|
||||
mask = aux["REQUIREMENTS_DESCRIPTION"].str.contains(
|
||||
long_desc, na=False, regex=False
|
||||
)
|
||||
aux.loc[mask, "REQUIREMENTS_DESCRIPTION"] = short_name
|
||||
|
||||
return get_section_containers_cis(
|
||||
aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
)
|
||||
@@ -154,6 +154,8 @@ Only fields with a numeric range, a fixed value set, or a length cap are listed.
|
||||
| `max_days_secret_unused` | `7..365` days | |
|
||||
| `max_days_secret_unrotated` | `1..180` days | NIST IA-5: rotate quarterly; CIS ≤90 |
|
||||
| `min_kinesis_stream_retention_hours` | `24..8760` h | 1 day .. 1 year |
|
||||
| `inspector2_max_days_since_last_scan` | `1..90` days | |
|
||||
| `inspector2_active_finding_max_age_days` | `1..365` days | Default `192` matches the FedRAMP 20x rule that marks vulnerabilities still open after 192 days as accepted |
|
||||
| `shodan_api_key` | ≤512 chars | |
|
||||
|
||||
### Azure
|
||||
|
||||
@@ -40,6 +40,17 @@ The former build-time variables map to the new runtime variables as follows:
|
||||
|
||||
`UI_CLOUD_ENABLED` is a plain runtime boolean flag that enables Prowler Cloud behavior when set to the exact string `"true"` and defaults to off; unlike the other renamed variables it has no legacy fallback, so `NEXT_PUBLIC_IS_CLOUD_ENV` is no longer read.
|
||||
|
||||
`UI_SELF_REGISTRATION_ENABLED` is a runtime opt-out flag that Prowler Local Server reads only when `UI_CLOUD_ENABLED` is `"true"`. It defaults to on and turns off when set to `"false"`, matched case-insensitively so the same value can be shared with a backend setting written `False`. When it is off, the sign-up page only opens with an invitation token, the sign-in page drops its "Sign up" link, and the profile hides "Create organization"; invited users can still complete their registration. Outside a Prowler Cloud deployment the flag is ignored and account creation stays open.
|
||||
## Registry UI Rollout and Rollback
|
||||
|
||||
`UI_REGISTRY_ENABLED` is an optional runtime flag for Prowler Cloud and Private Cloud. Registry is eligible only when both `UI_CLOUD_ENABLED` and `UI_REGISTRY_ENABLED` are the exact string `"true"` and the current user has the backend-authorized `manage_registry` permission. Unset, `"false"`, or malformed values fail closed. The flag defaults to off and is not a replacement for backend authorization. Registry access is independent of billing; Private Cloud can use it with `CLOUD_BILLING_ENABLED=false`.
|
||||
|
||||
Roll out Registry only after the Registry backend dependency is deployed, intended roles have `manage_registry`, and acceptance with real credentials has exercised installation, provider account creation, credentials, connection, and scan launch. Deploy the UI with `UI_REGISTRY_ENABLED` unset or `"false"`; set it to `"true"` only in the prepared process environment, then restart or otherwise apply the environment update required by the platform. A Registry key must belong to the configured Registry environment; a production key does not authenticate against a development Registry.
|
||||
|
||||
The catalog displays all artifacts, including built-ins and packages containing only checks or compliance frameworks. Only external provider artifacts support Add. After confirmed installation, open Providers and select the option labeled Registry to configure an account. Creating accounts and running scans also require the corresponding provider and scan permissions. Removing an artifact keeps existing provider accounts, but future connections or scans can fail until the artifact is installed again.
|
||||
|
||||
To roll back, set `UI_REGISTRY_ENABLED=false` or remove it and apply the environment update. Proxy, page, and action checks deny on their next request. Navigation refreshes from server-authorized access when the page is requested again. Rollback does not delete Registry credentials, tenant artifact records, or provider accounts.
|
||||
|
||||
The build-time-only Sentry variables used for source-map upload — `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN`, and `SENTRY_RELEASE` — keep their names, as they are not part of Prowler Local Server's runtime configuration.
|
||||
|
||||
## Enabling Third-Party Integrations
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 234 KiB |
@@ -91,6 +91,8 @@ The following list includes all the AWS checks with configurable variables that
|
||||
| `iam_user_access_not_stale_to_sagemaker` | `max_unused_sagemaker_access_days` | Integer | `90` |
|
||||
| `iam_user_accesskey_unused` | `max_unused_access_keys_days` | Integer | `45` |
|
||||
| `iam_user_console_access_unused` | `max_console_access_days` | Integer | `45` |
|
||||
| `inspector2_active_findings_within_max_age` | `inspector2_active_finding_max_age_days` | Integer | `192` |
|
||||
| `inspector2_coverage_recently_scanned` | `inspector2_max_days_since_last_scan` | Integer | `3` |
|
||||
| `kinesis_stream_data_retention_period` | `min_kinesis_stream_retention_hours` | Integer | `168` |
|
||||
| `neptune_cluster_backup_enabled` | `minimum_backup_retention_period` | Integer | `7` |
|
||||
| `opensearch_service_domains_not_publicly_accessible` | `trusted_ips` | List of Strings | `[]` |
|
||||
@@ -490,6 +492,14 @@ aws:
|
||||
# MEDIUM
|
||||
ecr_repository_vulnerability_minimum_severity: "MEDIUM"
|
||||
|
||||
# AWS Inspector2
|
||||
# aws.inspector2_coverage_recently_scanned
|
||||
# Maximum days since Inspector2 last scanned an actively covered resource
|
||||
inspector2_max_days_since_last_scan: 3
|
||||
# aws.inspector2_active_findings_within_max_age
|
||||
# Maximum days an Inspector2 finding can stay active since it was first observed
|
||||
inspector2_active_finding_max_age_days: 192
|
||||
|
||||
# AWS Trusted Advisor
|
||||
# aws.trustedadvisor_premium_support_plan_subscribed
|
||||
verify_premium_support_plans: True
|
||||
|
||||
@@ -40,6 +40,8 @@ It matters most where nothing else does. Resolving an identity means calling STS
|
||||
|
||||
A region configured for the session still wins when it belongs to the declared partition, so a deployment in `us-gov-west-1` is not sent to `us-gov-east-1`. A region belonging to a different partition is ignored, since a partition that has been declared explicitly is the more deliberate statement of the two.
|
||||
|
||||
When no configured region says which one to prefer, the first region of the partition is tried, and up to two more follow if it cannot be reached. A network that routes to only one region of its partition therefore works without having to declare which one that is. Only a connection failure moves on to the next region: a credential error is reported from the first, since it would be the same everywhere. A region excluded from the scan is tried last, so it is avoided whenever another region of the partition answers.
|
||||
|
||||
<Note>
|
||||
Set it wherever the scan runs. For deployments that scan from containers, that means the environment of the containers doing the scanning, not only the one accepting the request.
|
||||
</Note>
|
||||
|
||||
@@ -22,9 +22,12 @@ Prowler requires read-only access to Cloudflare zones and their settings. The fo
|
||||
| Resource | Permission | Access | Description |
|
||||
|----------|------------|--------|-------------|
|
||||
| `Account` | `Account Settings` | `Read` | Required to list accounts and verify user identity |
|
||||
| `Zone` | `Zone` | `Read` | Required to list zones, rulesets, bot management, and SSL settings |
|
||||
| `Zone` | `Zone Settings` | `Read` | Required to read zone security settings (TLS, HSTS, WAF, etc.) |
|
||||
| `Zone` | `DNS` | `Read` | Required to read DNS records and DNSSEC status |
|
||||
| `Zone` | `Zone` | `Read` | Required to list zones |
|
||||
| `Zone` | `Zone Settings` | `Read` | Required to read zone security settings (SSL/TLS mode, TLS versions, HSTS, Always Use HTTPS, WAF, etc.) |
|
||||
| `Zone` | `DNS` | `Read` | Required to read DNS records (SPF, DMARC, DKIM, CAA) and DNSSEC status |
|
||||
| `Zone` | `SSL and Certificates` | `Read` | Required to read Universal SSL settings |
|
||||
| `Zone` | `Bot Management` | `Read` | Required to read Bot Fight Mode |
|
||||
| `Zone` | `Zone WAF` | `Read` | Required to read WAF custom, rate limiting, and managed rulesets |
|
||||
|
||||
<Warning>
|
||||
Ensure the API Token has access to all zones targeted for scanning. Missing permissions may cause some checks to fail or return incomplete results.
|
||||
@@ -46,8 +49,8 @@ Create a **User API Token**, not an Account API Token. User API Tokens are creat
|
||||
|
||||
**Quick Setup:** Use these pre-configured links to open the Cloudflare Dashboard with the required permissions already selected:
|
||||
|
||||
- [Create User API Token](https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&accountId=%2A&zoneId=all&name=Prowler%20Security%20Scanner) — creates a **User API Token** (recommended). Opens the **Create Custom Token** form prefilled with the four required read-only scopes (`Account Settings`, `Zone`, `Zone Settings`, `DNS`) and the name `Prowler Security Scanner`. Adjust **Account Resources** and **Zone Resources** to match the accounts and zones you want to scan, then click **Create Token**.
|
||||
- [Create Account-Owned API Token](https://dash.cloudflare.com/?to=/:account/api-tokens&permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&name=Prowler%20Security%20Scanner) — creates an [account-owned token](https://developers.cloudflare.com/fundamentals/api/how-to/account-owned-token-template/) instead. Use this for automation or CI/CD where the token should not depend on a specific user account remaining active. Requires the **Super Administrator** or **Administrator** role on the account.
|
||||
- [Create User API Token](https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22ssl_and_certificates%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22bot_management%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_waf%22%2C%22type%22%3A%22read%22%7D%5D&accountId=%2A&zoneId=all&name=Prowler%20Security%20Scanner) — creates a **User API Token** (recommended). Opens the **Create Custom Token** form prefilled with the seven required read-only scopes (`Account Settings`, `Zone`, `Zone Settings`, `DNS`, `SSL and Certificates`, `Bot Management`, `Zone WAF`) and the name `Prowler Security Scanner`. Adjust **Account Resources** and **Zone Resources** to match the accounts and zones you want to scan, then click **Create Token**.
|
||||
- [Create Account-Owned API Token](https://dash.cloudflare.com/?to=/:account/api-tokens&permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22ssl_and_certificates%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22bot_management%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_waf%22%2C%22type%22%3A%22read%22%7D%5D&name=Prowler%20Security%20Scanner) — creates an [account-owned token](https://developers.cloudflare.com/fundamentals/api/how-to/account-owned-token-template/) instead. Use this for automation or CI/CD where the token should not depend on a specific user account remaining active. Requires the **Super Administrator** or **Administrator** role on the account.
|
||||
|
||||
<Note>
|
||||
Template URLs only pre-fill the token creation form. Review the permissions, configure resources, and click **Create Token** to complete the process.
|
||||
@@ -66,6 +69,9 @@ Template URLs only pre-fill the token creation form. Review the permissions, con
|
||||
- `Zone` — `Zone` — `Read`
|
||||
- `Zone` — `Zone Settings` — `Read`
|
||||
- `Zone` — `DNS` — `Read`
|
||||
- `Zone` — `SSL and Certificates` — `Read`
|
||||
- `Zone` — `Bot Management` — `Read`
|
||||
- `Zone` — `Zone WAF` — `Read`
|
||||
- **Zone Resources:** Select either:
|
||||
- **Include → All zones** (to scan all zones in the account)
|
||||
- **Include → Specific zone** (to limit access to specific zones)
|
||||
|
||||
@@ -11,16 +11,16 @@ Prowler for Cloudflare scans zones for security misconfigurations, including SSL
|
||||
Set up authentication for Cloudflare with the [Cloudflare Authentication](/user-guide/providers/cloudflare/authentication) guide before starting either path:
|
||||
|
||||
- Create a Cloudflare User API Token (recommended) or locate the Global API Key
|
||||
- Grant the required read-only permissions (`Account Settings:Read`, `Zone:Read`, `Zone Settings:Read`, `DNS:Read`)
|
||||
- Grant the required read-only permissions (`Account Settings:Read`, `Zone:Read`, `Zone Settings:Read`, `DNS:Read`, `SSL and Certificates:Read`, `Bot Management:Read`, `Zone WAF:Read`)
|
||||
- Identify the Cloudflare Account ID to use as the provider identifier
|
||||
|
||||
<Note>
|
||||
**Quick Setup:** Use these pre-configured links to create a token with the required permissions already selected:
|
||||
|
||||
- [Create User API Token](https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&accountId=%2A&zoneId=all&name=Prowler%20Security%20Scanner) — creates a User API Token (recommended).
|
||||
- [Create Account-Owned API Token](https://dash.cloudflare.com/?to=/:account/api-tokens&permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&name=Prowler%20Security%20Scanner) — creates an [account-owned token](https://developers.cloudflare.com/fundamentals/api/how-to/account-owned-token-template/), better suited for automation and CI/CD.
|
||||
- [Create User API Token](https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22ssl_and_certificates%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22bot_management%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_waf%22%2C%22type%22%3A%22read%22%7D%5D&accountId=%2A&zoneId=all&name=Prowler%20Security%20Scanner) — creates a User API Token (recommended).
|
||||
- [Create Account-Owned API Token](https://dash.cloudflare.com/?to=/:account/api-tokens&permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22ssl_and_certificates%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22bot_management%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_waf%22%2C%22type%22%3A%22read%22%7D%5D&name=Prowler%20Security%20Scanner) — creates an [account-owned token](https://developers.cloudflare.com/fundamentals/api/how-to/account-owned-token-template/), better suited for automation and CI/CD.
|
||||
|
||||
Both links open the Cloudflare Dashboard with the four required read-only scopes (`Account Settings`, `Zone`, `Zone Settings`, `DNS`) and the name `Prowler Security Scanner` prefilled. See [Cloudflare Authentication](/user-guide/providers/cloudflare/authentication#api-token-recommended) for the equivalent manual steps.
|
||||
Both links open the Cloudflare Dashboard with the seven required read-only scopes (`Account Settings`, `Zone`, `Zone Settings`, `DNS`, `SSL and Certificates`, `Bot Management`, `Zone WAF`) and the name `Prowler Security Scanner` prefilled. See [Cloudflare Authentication](/user-guide/providers/cloudflare/authentication#api-token-recommended) for the equivalent manual steps.
|
||||
</Note>
|
||||
|
||||
<CardGroup cols={2}>
|
||||
|
||||
@@ -241,7 +241,7 @@ steps:
|
||||
|
||||
### Cloudflare
|
||||
|
||||
Create a Cloudflare API Token with `Zone:Read`, `Zone Settings:Read`, and `DNS:Read` permissions ([provider auth docs](/user-guide/providers/cloudflare/authentication)). Then:
|
||||
Create a Cloudflare API Token with the `Account Settings:Read`, `Zone:Read`, `Zone Settings:Read`, `DNS:Read`, `SSL and Certificates:Read`, `Bot Management:Read`, and `Zone WAF:Read` permissions ([provider auth docs](/user-guide/providers/cloudflare/authentication)). Then:
|
||||
|
||||
```yaml
|
||||
- uses: prowler-cloud/prowler@5.25
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"glue:GetSecurityConfiguration*",
|
||||
"glue:SearchTables",
|
||||
"glue:GetMLTransforms",
|
||||
"inspector2:BatchGetFindingDetails",
|
||||
"lambda:GetFunction*",
|
||||
"lambda:GetLayerVersion",
|
||||
"logs:FilterLogEvents",
|
||||
|
||||
@@ -210,6 +210,7 @@ Resources:
|
||||
- "glue:GetSecurityConfiguration*"
|
||||
- "glue:SearchTables"
|
||||
- "glue:GetMLTransforms"
|
||||
- "inspector2:BatchGetFindingDetails"
|
||||
- "lambda:GetFunction*"
|
||||
- "logs:FilterLogEvents"
|
||||
- "lightsail:GetRelationalDatabases"
|
||||
@@ -479,6 +480,7 @@ Resources:
|
||||
- "glue:GetSecurityConfiguration*"
|
||||
- "glue:SearchTables"
|
||||
- "glue:GetMLTransforms"
|
||||
- "inspector2:BatchGetFindingDetails"
|
||||
- "lambda:GetFunction*"
|
||||
- "logs:FilterLogEvents"
|
||||
- "lightsail:GetRelationalDatabases"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
`inspector2_coverage_scan_status_active`, `inspector2_coverage_recently_scanned`, `inspector2_active_findings_no_known_exploited_vulnerabilities`, `inspector2_active_findings_kev_within_due_date`, `inspector2_active_findings_within_max_age`, `elbv2_listener_fips_tls_enabled` and `transfer_server_fips_security_policy_enabled` checks for AWS provider, covering FedRAMP 20x Class C vulnerability detection, CISA KEV remediation and FIPS cryptography rules; the KEV checks require `inspector2:BatchGetFindingDetails`, now in the Prowler additions policy
|
||||
@@ -0,0 +1 @@
|
||||
Bootstrap STS calls now try up to two more regions of the partition declared in `PROWLER_AWS_PARTITION` when the first one cannot be reached, so a deployment that routes to only one region of its partition no longer fails on an endpoint it has no path to. This covers validating credentials, assuming a role and getting an MFA session token
|
||||
@@ -0,0 +1 @@
|
||||
Azure Defender security contacts and Key Vault key rotation policies now use the endpoints of the selected cloud (`--azure-region`) instead of the hardcoded `management.azure.com` and `vault.azure.net` hosts, so both work on `AzureUSGovernment` and `AzureChinaCloud`
|
||||
@@ -0,0 +1 @@
|
||||
`FedRAMP-20x-FRR-Class-C` universal compliance framework (`fedramp_20x_frr_class_c_2026`) with the 158 provider rules of the FedRAMP 20x Class C ruleset from the FedRAMP Consolidated Rules 2026 for AWS, Azure, GCP, Kubernetes and M365
|
||||
@@ -0,0 +1 @@
|
||||
`FedRAMP-20x-KSI` universal compliance framework (`fedramp_20x_ksi_2026`) with the 46 Key Security Indicators from the FedRAMP Consolidated Rules 2026 mapped for AWS, Azure, GCP, Kubernetes and M365
|
||||
@@ -0,0 +1 @@
|
||||
`fedramp_20x_ksi_low_aws`, `fedramp_20x_ksi_low_azure` and `fedramp_20x_ksi_low_gcp` FedRAMP 20x Phase One pilot frameworks, superseded by `fedramp_20x_ksi_2026`
|
||||
@@ -0,0 +1 @@
|
||||
`security_2sv_enforced` reports domain-wide 2-Step Verification failures as FAIL even when every failing setting is overridden for a group or organizational unit
|
||||
@@ -0,0 +1 @@
|
||||
`KeyError` in M365 Defender malware, anti-phishing and inbound anti-spam checks when the tenant has Standard or Strict preset security policies
|
||||
@@ -0,0 +1 @@
|
||||
`libsqlite3-0`, `gzip`, `perl-base`, `libssh2-1t64` and `libpcre2-8-0` upgraded in the SDK container image, patching nine high Debian CVEs
|
||||
@@ -0,0 +1 @@
|
||||
PowerShell from 7.5.9 to 7.5.11 in the SDK container image, bundling .NET runtime 9.0.20 and patching CVE-2026-62901
|
||||
@@ -0,0 +1 @@
|
||||
`smn_topic_subscriptions` check for Huawei Cloud provider: SMN topics have at least one subscription configured
|
||||
@@ -1,383 +0,0 @@
|
||||
{
|
||||
"Framework": "FedRAMP-20x-KSI-Low",
|
||||
"Name": "FedRAMP 20x Key Security Indicators (KSIs) - Low Impact Level v25.05C",
|
||||
"Version": "25.05C",
|
||||
"Provider": "AWS",
|
||||
"Description": "FedRAMP 20x Key Security Indicators (KSIs) Low Impact Level represent core security indicators for cloud service providers, focusing on automation, continuous monitoring, and cloud-native security principles per FedRAMP 20x Phase One pilot requirements for Low impact systems.",
|
||||
"Requirements": [
|
||||
{
|
||||
"Id": "ksi-cmt",
|
||||
"Name": "KSI-CMT: Change Management",
|
||||
"Description": "A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-cmt",
|
||||
"Section": "Change Management",
|
||||
"Service": "aws"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"cloudtrail_multi_region_enabled",
|
||||
"cloudtrail_log_file_validation_enabled",
|
||||
"cloudtrail_s3_dataevents_read_enabled",
|
||||
"cloudtrail_s3_dataevents_write_enabled",
|
||||
"cloudwatch_changes_to_network_acls_alarm_configured",
|
||||
"cloudwatch_changes_to_network_gateways_alarm_configured",
|
||||
"cloudwatch_changes_to_network_route_tables_alarm_configured",
|
||||
"cloudwatch_changes_to_vpcs_alarm_configured",
|
||||
"cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled",
|
||||
"cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled",
|
||||
"cloudwatch_log_metric_filter_aws_organizations_changes",
|
||||
"cloudwatch_log_metric_filter_for_s3_bucket_policy_changes",
|
||||
"cloudwatch_log_metric_filter_policy_changes",
|
||||
"cloudwatch_log_metric_filter_security_group_changes",
|
||||
"config_recorder_all_regions_enabled",
|
||||
"ec2_instance_managed_by_ssm",
|
||||
"ec2_instance_older_than_specific_days",
|
||||
"ssm_managed_compliant_patching"
|
||||
],
|
||||
"ConfigRequirements": [
|
||||
{
|
||||
"Check": "config_recorder_all_regions_enabled",
|
||||
"ConfigKey": "mute_non_default_regions",
|
||||
"Operator": "eq",
|
||||
"Value": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-cna",
|
||||
"Name": "KSI-CNA: Cloud Native Architecture",
|
||||
"Description": "A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-cna",
|
||||
"Section": "Cloud Native Architecture",
|
||||
"Service": "aws"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"autoscaling_group_multiple_az",
|
||||
"autoscaling_group_multiple_instance_types",
|
||||
"autoscaling_group_capacity_rebalance_enabled",
|
||||
"dynamodb_tables_pitr_enabled",
|
||||
"dynamodb_table_deletion_protection_enabled",
|
||||
"ec2_instance_imdsv2_enabled",
|
||||
"ec2_networkacl_allow_ingress_any_port",
|
||||
"ec2_securitygroup_default_restrict_traffic",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"eks_cluster_network_policy_enabled",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_cluster_private_nodes_enabled",
|
||||
"eks_cluster_uses_a_supported_version",
|
||||
"elb_cross_zone_load_balancing_enabled",
|
||||
"elbv2_is_in_multiple_az",
|
||||
"elbv2_waf_acl_attached",
|
||||
"rds_instance_multi_az",
|
||||
"rds_cluster_multi_az",
|
||||
"vpc_subnet_no_public_ip_by_default",
|
||||
"vpc_peering_routing_tables_with_least_privilege",
|
||||
"ec2_confidential_workload_host_imdsv2_not_enforced"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-iam",
|
||||
"Name": "KSI-IAM: Identity and Access Management",
|
||||
"Description": "A secure cloud service offering will protect user data, control access, and apply zero trust principles",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-iam",
|
||||
"Section": "Identity and Access Management",
|
||||
"Service": "aws"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"iam_inline_policy_no_wildcard_marketplace_subscribe",
|
||||
"iam_policy_no_wildcard_marketplace_subscribe",
|
||||
"iam_administrator_access_with_mfa",
|
||||
"iam_aws_attached_policy_no_administrative_privileges",
|
||||
"iam_customer_attached_policy_no_administrative_privileges",
|
||||
"iam_inline_policy_no_administrative_privileges",
|
||||
"iam_no_custom_policy_permissive_role_assumption",
|
||||
"iam_no_root_access_key",
|
||||
"iam_password_policy_expires_passwords_within_90_days_or_less",
|
||||
"iam_password_policy_lowercase",
|
||||
"iam_password_policy_minimum_length_14",
|
||||
"iam_password_policy_number",
|
||||
"iam_password_policy_reuse_24",
|
||||
"iam_password_policy_symbol",
|
||||
"iam_password_policy_uppercase",
|
||||
"iam_policy_attached_only_to_group_or_roles",
|
||||
"iam_policy_no_full_access_to_cloudtrail",
|
||||
"iam_policy_no_full_access_to_kms",
|
||||
"iam_root_hardware_mfa_enabled",
|
||||
"iam_root_mfa_enabled",
|
||||
"iam_rotate_access_key_90_days",
|
||||
"iam_role_access_not_stale_to_bedrock",
|
||||
"iam_user_access_not_stale_to_bedrock",
|
||||
"iam_user_access_not_stale_to_sagemaker",
|
||||
"iam_user_accesskey_unused",
|
||||
"iam_user_console_access_unused",
|
||||
"iam_user_hardware_mfa_enabled",
|
||||
"iam_user_mfa_enabled_console_access",
|
||||
"iam_user_two_active_access_key",
|
||||
"organizations_scp_check_deny_regions",
|
||||
"organizations_opt_out_ai_services_policy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-inr",
|
||||
"Name": "KSI-INR: Incident Response",
|
||||
"Description": "A secure cloud service offering will respond to incidents according to FedRAMP requirements and cloud service provider policies",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-inr",
|
||||
"Section": "Incident Response",
|
||||
"Service": "aws"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"guardduty_centrally_managed",
|
||||
"guardduty_ec2_malware_protection_enabled",
|
||||
"guardduty_eks_audit_log_enabled",
|
||||
"guardduty_eks_runtime_monitoring_enabled",
|
||||
"guardduty_is_enabled",
|
||||
"guardduty_lambda_protection_enabled",
|
||||
"guardduty_no_high_severity_findings",
|
||||
"guardduty_rds_protection_enabled",
|
||||
"guardduty_s3_protection_enabled",
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist",
|
||||
"securityhub_enabled",
|
||||
"sns_topics_kms_encryption_at_rest_enabled"
|
||||
],
|
||||
"ConfigRequirements": [
|
||||
{
|
||||
"Check": "guardduty_is_enabled",
|
||||
"ConfigKey": "mute_non_default_regions",
|
||||
"Operator": "eq",
|
||||
"Value": false
|
||||
},
|
||||
{
|
||||
"Check": "securityhub_enabled",
|
||||
"ConfigKey": "mute_non_default_regions",
|
||||
"Operator": "eq",
|
||||
"Value": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-mla",
|
||||
"Name": "KSI-MLA: Monitoring, Logging, and Auditing",
|
||||
"Description": "A secure cloud service offering will monitor, log, and audit all important events, activity, and changes",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-mla",
|
||||
"Section": "Monitoring, Logging, and Auditing",
|
||||
"Service": "aws"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"apigateway_restapi_logging_enabled",
|
||||
"cloudtrail_cloudwatch_logging_enabled",
|
||||
"cloudtrail_kms_encryption_enabled",
|
||||
"cloudtrail_log_file_validation_enabled",
|
||||
"cloudtrail_multi_region_enabled",
|
||||
"cloudtrail_s3_dataevents_read_enabled",
|
||||
"cloudtrail_s3_dataevents_write_enabled",
|
||||
"cloudwatch_log_group_kms_encryption_enabled",
|
||||
"cloudwatch_log_group_retention_policy_specific_days_enabled",
|
||||
"ecs_cluster_container_insights_enabled",
|
||||
"eks_control_plane_logging_all_types_enabled",
|
||||
"elb_logging_enabled",
|
||||
"elbv2_logging_enabled",
|
||||
"inspector2_is_enabled",
|
||||
"opensearch_service_domains_cloudwatch_logging_enabled",
|
||||
"rds_instance_enhanced_monitoring_enabled",
|
||||
"rds_instance_integration_cloudwatch_logs",
|
||||
"redshift_cluster_audit_logging",
|
||||
"s3_bucket_server_access_logging_enabled",
|
||||
"vpc_flow_logs_enabled",
|
||||
"wafv2_webacl_logging_enabled",
|
||||
"kms_key_enclave_debug_attestation_detected",
|
||||
"kms_key_enclave_attestation_unknown_image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-piy",
|
||||
"Name": "KSI-PIY: Policy and Inventory",
|
||||
"Description": "A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-piy",
|
||||
"Section": "Policy and Inventory",
|
||||
"Service": "aws"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"config_recorder_all_regions_enabled",
|
||||
"config_recorder_using_aws_service_role",
|
||||
"ec2_instance_managed_by_ssm",
|
||||
"organizations_account_part_of_organizations",
|
||||
"organizations_delegated_administrators",
|
||||
"organizations_scp_check_deny_regions",
|
||||
"organizations_tags_policies_enabled_and_attached",
|
||||
"resourceexplorer2_indexes_found",
|
||||
"trustedadvisor_premium_support_plan_subscribed"
|
||||
],
|
||||
"ConfigRequirements": [
|
||||
{
|
||||
"Check": "config_recorder_all_regions_enabled",
|
||||
"ConfigKey": "mute_non_default_regions",
|
||||
"Operator": "eq",
|
||||
"Value": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-rpl",
|
||||
"Name": "KSI-RPL: Recovery Planning",
|
||||
"Description": "A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-rpl",
|
||||
"Section": "Recovery Planning",
|
||||
"Service": "aws"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"backup_plans_exist",
|
||||
"backup_reportplans_exist",
|
||||
"backup_vaults_exist",
|
||||
"backup_vaults_encrypted",
|
||||
"backup_recovery_point_encrypted",
|
||||
"dlm_ebs_snapshot_lifecycle_policy_exists",
|
||||
"dynamodb_tables_pitr_enabled",
|
||||
"dynamodb_table_deletion_protection_enabled",
|
||||
"efs_have_backup_enabled",
|
||||
"fsx_file_system_copy_tags_to_backups_enabled",
|
||||
"rds_instance_backup_enabled",
|
||||
"rds_instance_deletion_protection",
|
||||
"rds_cluster_deletion_protection",
|
||||
"rds_snapshots_encrypted",
|
||||
"redshift_cluster_automated_snapshot"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-svc",
|
||||
"Name": "KSI-SVC: Service Configuration",
|
||||
"Description": "A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-svc",
|
||||
"Section": "Service Configuration",
|
||||
"Service": "aws"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"acm_certificates_expiration_check",
|
||||
"apigateway_restapi_cache_encrypted",
|
||||
"cloudtrail_kms_encryption_enabled",
|
||||
"dynamodb_accelerator_cluster_encryption_enabled",
|
||||
"dynamodb_tables_kms_cmk_encryption_enabled",
|
||||
"ec2_ebs_volume_encryption",
|
||||
"ec2_ebs_default_encryption",
|
||||
"efs_encryption_at_rest_enabled",
|
||||
"eks_cluster_kms_cmk_encryption_in_secrets_enabled",
|
||||
"elasticache_redis_cluster_rest_encryption_enabled",
|
||||
"elasticache_redis_cluster_in_transit_encryption_enabled",
|
||||
"elbv2_ssl_listeners",
|
||||
"kinesis_stream_encrypted_at_rest",
|
||||
"kms_cmk_rotation_enabled",
|
||||
"kms_cmk_not_deleted_unintentionally",
|
||||
"kms_key_not_publicly_accessible",
|
||||
"rds_instance_storage_encrypted",
|
||||
"rds_cluster_storage_encrypted",
|
||||
"redshift_cluster_encrypted_at_rest",
|
||||
"redshift_cluster_in_transit_encryption_enabled",
|
||||
"s3_bucket_default_encryption",
|
||||
"s3_bucket_secure_transport_policy",
|
||||
"sagemaker_notebook_instance_encryption_enabled",
|
||||
"sns_topics_kms_encryption_at_rest_enabled",
|
||||
"sqs_queues_server_side_encryption_enabled",
|
||||
"kms_key_enclave_attestation_not_enforced"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-tpr",
|
||||
"Name": "KSI-TPR: Third-Party Information Resources",
|
||||
"Description": "A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-tpr",
|
||||
"Section": "Third-Party Information Resources",
|
||||
"Service": "aws"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"ecr_registry_scan_images_on_push_enabled",
|
||||
"ecr_repositories_lifecycle_policy_enabled",
|
||||
"ecr_repositories_not_publicly_accessible",
|
||||
"ecr_repositories_scan_images_on_push_enabled",
|
||||
"ecr_repositories_scan_vulnerabilities_in_latest_image",
|
||||
"ecr_repositories_tag_immutability",
|
||||
"inspector2_active_findings_exist",
|
||||
"inspector2_is_enabled",
|
||||
"awslambda_function_using_supported_runtimes",
|
||||
"ssm_managed_compliant_patching",
|
||||
"trustedadvisor_premium_support_plan_subscribed",
|
||||
"guardduty_no_high_severity_findings"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-iam-07",
|
||||
"Name": "KSI-IAM-07: Account Lifecycle Management",
|
||||
"Description": "Securely manage the lifecycle and privileges of all accounts, roles, and groups",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-iam-07",
|
||||
"Section": "Identity and Access Management",
|
||||
"Service": "aws"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"iam_no_root_access_key",
|
||||
"iam_policy_attached_only_to_group_or_roles",
|
||||
"iam_rotate_access_key_90_days",
|
||||
"iam_role_access_not_stale_to_bedrock",
|
||||
"iam_user_access_not_stale_to_bedrock",
|
||||
"iam_user_access_not_stale_to_sagemaker",
|
||||
"iam_user_accesskey_unused",
|
||||
"iam_user_console_access_unused",
|
||||
"organizations_delegated_administrators"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-mla-07",
|
||||
"Name": "KSI-MLA-07: Monitoring and Logging Inventory",
|
||||
"Description": "Maintain a list of information resources and event types that will be monitored, logged, and audited",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-mla-07",
|
||||
"Section": "Monitoring, Logging, and Auditing",
|
||||
"Service": "aws"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"cloudtrail_multi_region_enabled",
|
||||
"cloudwatch_log_group_retention_policy_specific_days_enabled",
|
||||
"config_recorder_all_regions_enabled",
|
||||
"inspector2_is_enabled",
|
||||
"resourceexplorer2_indexes_found"
|
||||
],
|
||||
"ConfigRequirements": [
|
||||
{
|
||||
"Check": "config_recorder_all_regions_enabled",
|
||||
"ConfigKey": "mute_non_default_regions",
|
||||
"Operator": "eq",
|
||||
"Value": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
{
|
||||
"Framework": "FedRAMP-20x-KSI-Low",
|
||||
"Name": "FedRAMP 20x Key Security Indicators (KSIs) - Low Impact Level v25.05C",
|
||||
"Version": "25.05C",
|
||||
"Provider": "Azure",
|
||||
"Description": "FedRAMP 20x Key Security Indicators (KSIs) Low Impact Level represent core security indicators for cloud service providers, focusing on automation, continuous monitoring, and cloud-native security principles per FedRAMP 20x Phase One pilot requirements for Low impact systems.",
|
||||
"Requirements": [
|
||||
{
|
||||
"Id": "ksi-cmt",
|
||||
"Name": "KSI-CMT: Change Management",
|
||||
"Description": "A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-cmt",
|
||||
"Section": "Change Management",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"monitor_alert_create_policy_assignment",
|
||||
"monitor_alert_create_update_sqlserver_fr",
|
||||
"monitor_alert_delete_sqlserver_fr",
|
||||
"monitor_alert_create_update_nsg",
|
||||
"monitor_alert_create_update_public_ip_address_rule",
|
||||
"monitor_alert_create_update_security_solution",
|
||||
"monitor_alert_delete_nsg",
|
||||
"monitor_alert_delete_policy_assignment",
|
||||
"monitor_alert_delete_public_ip_address_rule",
|
||||
"monitor_alert_delete_security_solution",
|
||||
"monitor_diagnostic_setting_with_appropriate_categories",
|
||||
"defender_assessments_vm_endpoint_protection_installed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-cna",
|
||||
"Name": "KSI-CNA: Cloud Native Architecture",
|
||||
"Description": "A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-cna",
|
||||
"Section": "Cloud Native Architecture",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"aks_clusters_created_with_private_nodes",
|
||||
"aks_clusters_public_access_disabled",
|
||||
"aks_network_policy_enabled",
|
||||
"app_function_vnet_integration_enabled",
|
||||
"app_function_not_publicly_accessible",
|
||||
"containerregistry_not_publicly_accessible",
|
||||
"containerregistry_uses_private_link",
|
||||
"cosmosdb_account_use_private_endpoints",
|
||||
"cosmosdb_account_firewall_use_selected_networks",
|
||||
"databricks_workspace_vnet_injection_enabled",
|
||||
"keyvault_access_only_through_private_endpoints",
|
||||
"keyvault_private_endpoints",
|
||||
"network_bastion_host_exists",
|
||||
"network_flow_log_captured_sent",
|
||||
"network_ssh_internet_access_restricted",
|
||||
"network_rdp_internet_access_restricted",
|
||||
"network_watcher_enabled",
|
||||
"storage_default_network_access_rule_is_denied"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-iam",
|
||||
"Name": "KSI-IAM: Identity and Access Management",
|
||||
"Description": "A secure cloud service offering will protect user data, control access, and apply zero trust principles",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-iam",
|
||||
"Section": "Identity and Access Management",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api",
|
||||
"entra_global_admin_in_less_than_five_users",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
"entra_policy_default_users_cannot_create_security_groups",
|
||||
"entra_policy_ensure_default_user_cannot_create_apps",
|
||||
"entra_policy_ensure_default_user_cannot_create_tenants",
|
||||
"entra_policy_guest_invite_only_for_admin_roles",
|
||||
"entra_policy_guest_users_access_restrictions",
|
||||
"entra_policy_restricts_user_consent_for_apps",
|
||||
"entra_policy_user_consent_for_verified_apps",
|
||||
"entra_privileged_user_has_mfa",
|
||||
"entra_security_defaults_enabled",
|
||||
"entra_trusted_named_locations_exists",
|
||||
"entra_user_with_vm_access_has_mfa",
|
||||
"entra_users_cannot_create_microsoft_365_groups",
|
||||
"iam_custom_role_has_permissions_to_administer_resource_locks",
|
||||
"iam_role_user_access_admin_restricted",
|
||||
"iam_subscription_roles_owner_custom_not_created",
|
||||
"keyvault_rbac_enabled",
|
||||
"app_function_identity_is_configured",
|
||||
"app_function_identity_without_admin_privileges",
|
||||
"app_ensure_auth_is_set_up",
|
||||
"app_register_with_identity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-inr",
|
||||
"Name": "KSI-INR: Incident Response",
|
||||
"Description": "A secure cloud service offering will respond to incidents according to FedRAMP requirements and cloud service provider policies",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-inr",
|
||||
"Section": "Incident Response",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"defender_attack_path_notifications_properly_configured",
|
||||
"defender_ensure_notify_alerts_severity_is_high",
|
||||
"defender_ensure_notify_emails_to_owners",
|
||||
"defender_additional_email_configured_with_a_security_contact",
|
||||
"defender_container_images_resolved_vulnerabilities",
|
||||
"defender_container_images_scan_enabled",
|
||||
"defender_ensure_defender_for_app_services_is_on",
|
||||
"defender_ensure_defender_for_arm_is_on",
|
||||
"defender_ensure_defender_for_azure_sql_databases_is_on",
|
||||
"defender_ensure_defender_for_containers_is_on",
|
||||
"defender_ensure_defender_for_cosmosdb_is_on",
|
||||
"defender_ensure_defender_for_databases_is_on",
|
||||
"defender_ensure_defender_for_dns_is_on",
|
||||
"defender_ensure_defender_for_keyvault_is_on",
|
||||
"defender_ensure_defender_for_os_relational_databases_is_on",
|
||||
"defender_ensure_defender_for_server_is_on",
|
||||
"defender_ensure_defender_for_sql_servers_is_on",
|
||||
"defender_ensure_defender_for_storage_is_on",
|
||||
"defender_ensure_iot_hub_defender_is_on",
|
||||
"defender_ensure_wdatp_is_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-mla",
|
||||
"Name": "KSI-MLA: Monitoring, Logging, and Auditing",
|
||||
"Description": "A secure cloud service offering will monitor, log, and audit all important events, activity, and changes",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-mla",
|
||||
"Section": "Monitoring, Logging, and Auditing",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"app_function_application_insights_enabled",
|
||||
"app_http_logs_enabled",
|
||||
"appinsights_ensure_is_configured",
|
||||
"defender_auto_provisioning_log_analytics_agent_vms_on",
|
||||
"defender_auto_provisioning_vulnerabilty_assessments_machines_on",
|
||||
"keyvault_logging_enabled",
|
||||
"monitor_diagnostic_setting_with_appropriate_categories",
|
||||
"monitor_diagnostic_settings_exists",
|
||||
"network_flow_log_captured_sent",
|
||||
"network_flow_log_more_than_90_days",
|
||||
"network_watcher_enabled",
|
||||
"postgresql_flexible_server_log_checkpoints_on",
|
||||
"postgresql_flexible_server_log_connections_on",
|
||||
"postgresql_flexible_server_log_disconnections_on",
|
||||
"sqlserver_auditing_enabled",
|
||||
"sqlserver_auditing_retention_90_days"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-piy",
|
||||
"Name": "KSI-PIY: Policy and Inventory",
|
||||
"Description": "A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-piy",
|
||||
"Section": "Policy and Inventory",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"defender_ensure_defender_for_containers_is_on",
|
||||
"defender_ensure_defender_for_app_services_is_on",
|
||||
"defender_ensure_defender_for_azure_sql_databases_is_on",
|
||||
"defender_ensure_defender_for_keyvault_is_on",
|
||||
"defender_ensure_defender_for_server_is_on",
|
||||
"defender_ensure_defender_for_sql_servers_is_on",
|
||||
"defender_ensure_defender_for_storage_is_on"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-rpl",
|
||||
"Name": "KSI-RPL: Recovery Planning",
|
||||
"Description": "A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-rpl",
|
||||
"Section": "Recovery Planning",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"mysql_flexible_server_geo_redundant_backup_enabled",
|
||||
"postgresql_flexible_server_geo_redundant_backup_enabled",
|
||||
"storage_geo_redundant_enabled",
|
||||
"storage_infrastructure_encryption_is_enabled",
|
||||
"storage_ensure_soft_delete_is_enabled",
|
||||
"vm_backup_enabled",
|
||||
"vm_sufficient_daily_backup_retention_period"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-svc",
|
||||
"Name": "KSI-SVC: Service Configuration",
|
||||
"Description": "A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-svc",
|
||||
"Section": "Service Configuration",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"app_client_certificates_on",
|
||||
"app_ensure_http_is_redirected_to_https",
|
||||
"app_minimum_tls_version_12",
|
||||
"containerregistry_admin_user_disabled",
|
||||
"cosmosdb_account_use_aad_and_rbac",
|
||||
"databricks_workspace_cmk_encryption_enabled",
|
||||
"keyvault_key_expiration_set_in_non_rbac",
|
||||
"keyvault_key_rotation_enabled",
|
||||
"keyvault_non_rbac_secret_expiration_set",
|
||||
"mysql_flexible_server_ssl_connection_enabled",
|
||||
"mysql_flexible_server_minimum_tls_version_12",
|
||||
"postgresql_flexible_server_enforce_ssl_enabled",
|
||||
"defender_ensure_defender_for_sql_servers_is_on",
|
||||
"sqlserver_tde_encrypted_with_cmk",
|
||||
"sqlserver_tde_encryption_enabled",
|
||||
"sqlserver_recommended_minimal_tls_version",
|
||||
"storage_secure_transfer_required_is_enabled",
|
||||
"storage_ensure_encryption_with_customer_managed_keys",
|
||||
"storage_infrastructure_encryption_is_enabled",
|
||||
"storage_ensure_minimum_tls_version_12",
|
||||
"vm_ensure_attached_disks_encrypted_with_cmk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-tpr",
|
||||
"Name": "KSI-TPR: Third-Party Information Resources",
|
||||
"Description": "A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-tpr",
|
||||
"Section": "Third-Party Information Resources",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"app_ensure_java_version_is_latest",
|
||||
"app_ensure_php_version_is_latest",
|
||||
"app_ensure_python_version_is_latest",
|
||||
"app_function_latest_runtime_version",
|
||||
"defender_container_images_resolved_vulnerabilities",
|
||||
"defender_container_images_scan_enabled",
|
||||
"defender_ensure_system_updates_are_applied",
|
||||
"defender_assessments_vm_endpoint_protection_installed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-iam-07",
|
||||
"Name": "KSI-IAM-07: Account Lifecycle Management",
|
||||
"Description": "Securely manage the lifecycle and privileges of all accounts, roles, and groups",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-iam-07",
|
||||
"Section": "Identity and Access Management",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
"entra_privileged_user_has_mfa",
|
||||
"entra_user_with_recent_sign_in",
|
||||
"entra_user_with_vm_access_has_mfa",
|
||||
"iam_custom_role_has_permissions_to_administer_resource_locks",
|
||||
"iam_role_user_access_admin_restricted",
|
||||
"app_function_identity_is_configured"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-mla-07",
|
||||
"Name": "KSI-MLA-07: Monitoring and Logging Inventory",
|
||||
"Description": "Maintain a list of information resources and event types that will be monitored, logged, and audited",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-mla-07",
|
||||
"Section": "Monitoring, Logging, and Auditing",
|
||||
"Service": "azure"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"monitor_diagnostic_setting_with_appropriate_categories",
|
||||
"monitor_diagnostic_settings_exists",
|
||||
"network_watcher_enabled"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,294 +0,0 @@
|
||||
{
|
||||
"Framework": "FedRAMP-20x-KSI-Low",
|
||||
"Name": "FedRAMP 20x Key Security Indicators (KSIs) - Low Impact Level v25.05C",
|
||||
"Version": "25.05C",
|
||||
"Provider": "GCP",
|
||||
"Description": "FedRAMP 20x Key Security Indicators (KSIs) Low Impact Level represent core security indicators for cloud service providers, focusing on automation, continuous monitoring, and cloud-native security principles per FedRAMP 20x Phase One pilot requirements for Low impact systems.",
|
||||
"Requirements": [
|
||||
{
|
||||
"Id": "ksi-cmt",
|
||||
"Name": "KSI-CMT: Change Management",
|
||||
"Description": "A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-cmt",
|
||||
"Section": "Change Management",
|
||||
"Service": "gcp"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"iam_audit_logs_enabled",
|
||||
"iam_cloud_asset_inventory_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_custom_role_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled",
|
||||
"compute_instance_serial_ports_in_use",
|
||||
"compute_project_os_login_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-cna",
|
||||
"Name": "KSI-CNA: Cloud Native Architecture",
|
||||
"Description": "A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-cna",
|
||||
"Section": "Cloud Native Architecture",
|
||||
"Service": "gcp"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"cloudsql_instance_private_ip_assignment",
|
||||
"cloudsql_instance_public_access",
|
||||
"cloudsql_instance_public_ip",
|
||||
"cloudstorage_bucket_uniform_bucket_level_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",
|
||||
"compute_instance_confidential_computing_enabled",
|
||||
"compute_instance_ip_forwarding_is_enabled",
|
||||
"compute_instance_public_ip",
|
||||
"compute_instance_shielded_vm_enabled",
|
||||
"compute_loadbalancer_logging_enabled",
|
||||
"compute_network_default_in_use",
|
||||
"compute_network_dns_logging_enabled",
|
||||
"compute_network_not_legacy",
|
||||
"compute_subnet_flow_logs_enabled",
|
||||
"gke_cluster_no_default_service_account"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-iam",
|
||||
"Name": "KSI-IAM: Identity and Access Management",
|
||||
"Description": "A secure cloud service offering will protect user data, control access, and apply zero trust principles",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-iam",
|
||||
"Section": "Identity and Access Management",
|
||||
"Service": "gcp"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"apikeys_api_restrictions_configured",
|
||||
"apikeys_key_exists",
|
||||
"apikeys_key_rotated_in_90_days",
|
||||
"compute_instance_default_service_account_in_use",
|
||||
"compute_instance_default_service_account_in_use_with_full_api_access",
|
||||
"iam_no_service_roles_at_project_level",
|
||||
"iam_role_kms_enforce_separation_of_duties",
|
||||
"iam_role_sa_enforce_separation_of_duties",
|
||||
"iam_sa_no_administrative_privileges",
|
||||
"iam_sa_no_user_managed_keys",
|
||||
"iam_sa_user_managed_key_rotate_90_days",
|
||||
"iam_sa_user_managed_key_unused",
|
||||
"iam_service_account_unused"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-inr",
|
||||
"Name": "KSI-INR: Incident Response",
|
||||
"Description": "A secure cloud service offering will respond to incidents according to FedRAMP requirements and cloud service provider policies",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-inr",
|
||||
"Section": "Incident Response",
|
||||
"Service": "gcp"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"iam_organization_essential_contacts_configured",
|
||||
"iam_account_access_approval_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_custom_role_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-mla",
|
||||
"Name": "KSI-MLA: Monitoring, Logging, and Auditing",
|
||||
"Description": "A secure cloud service offering will monitor, log, and audit all important events, activity, and changes",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-mla",
|
||||
"Section": "Monitoring, Logging, and Auditing",
|
||||
"Service": "gcp"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"cloudsql_instance_postgres_enable_pgaudit_flag",
|
||||
"cloudsql_instance_postgres_log_connections_flag",
|
||||
"cloudsql_instance_postgres_log_disconnections_flag",
|
||||
"cloudsql_instance_postgres_log_error_verbosity_flag",
|
||||
"cloudsql_instance_postgres_log_min_duration_statement_flag",
|
||||
"cloudsql_instance_postgres_log_min_error_statement_flag",
|
||||
"cloudsql_instance_postgres_log_min_messages_flag",
|
||||
"cloudsql_instance_postgres_log_statement_flag",
|
||||
"cloudsql_instance_sqlserver_trace_flag",
|
||||
"cloudstorage_bucket_log_retention_policy_lock",
|
||||
"compute_loadbalancer_logging_enabled",
|
||||
"compute_network_dns_logging_enabled",
|
||||
"compute_subnet_flow_logs_enabled",
|
||||
"iam_audit_logs_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_custom_role_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled",
|
||||
"logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled",
|
||||
"logging_sink_created"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-piy",
|
||||
"Name": "KSI-PIY: Policy and Inventory",
|
||||
"Description": "A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-piy",
|
||||
"Section": "Policy and Inventory",
|
||||
"Service": "gcp"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"iam_cloud_asset_inventory_enabled",
|
||||
"iam_organization_essential_contacts_configured",
|
||||
"iam_audit_logs_enabled",
|
||||
"compute_project_os_login_enabled",
|
||||
"compute_instance_serial_ports_in_use",
|
||||
"compute_instance_block_project_wide_ssh_keys_disabled",
|
||||
"logging_sink_created"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-rpl",
|
||||
"Name": "KSI-RPL: Recovery Planning",
|
||||
"Description": "A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-rpl",
|
||||
"Section": "Recovery Planning",
|
||||
"Service": "gcp"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"cloudsql_instance_automated_backups",
|
||||
"cloudstorage_bucket_log_retention_policy_lock",
|
||||
"cloudstorage_bucket_versioning_enabled",
|
||||
"cloudstorage_bucket_lifecycle_management_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-svc",
|
||||
"Name": "KSI-SVC: Service Configuration",
|
||||
"Description": "A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-svc",
|
||||
"Section": "Service Configuration",
|
||||
"Service": "gcp"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"bigquery_dataset_cmk_encryption",
|
||||
"bigquery_table_cmk_encryption",
|
||||
"cloudsql_instance_mysql_local_infile_flag",
|
||||
"cloudsql_instance_mysql_skip_show_database_flag",
|
||||
"cloudsql_instance_postgres_enable_pgaudit_flag",
|
||||
"cloudsql_instance_postgres_log_connections_flag",
|
||||
"cloudsql_instance_postgres_log_disconnections_flag",
|
||||
"cloudsql_instance_postgres_log_error_verbosity_flag",
|
||||
"cloudsql_instance_postgres_log_min_duration_statement_flag",
|
||||
"cloudsql_instance_postgres_log_min_error_statement_flag",
|
||||
"cloudsql_instance_postgres_log_min_messages_flag",
|
||||
"cloudsql_instance_postgres_log_statement_flag",
|
||||
"cloudsql_instance_sqlserver_contained_database_authentication_flag",
|
||||
"cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag",
|
||||
"cloudsql_instance_sqlserver_external_scripts_enabled_flag",
|
||||
"cloudsql_instance_sqlserver_remote_access_flag",
|
||||
"cloudsql_instance_sqlserver_trace_flag",
|
||||
"cloudsql_instance_sqlserver_user_connections_flag",
|
||||
"cloudsql_instance_sqlserver_user_options_flag",
|
||||
"cloudsql_instance_ssl_connections",
|
||||
"compute_instance_encryption_with_csek_enabled",
|
||||
"compute_instance_shielded_vm_enabled",
|
||||
"dataproc_encrypted_with_cmks_disabled",
|
||||
"dns_dnssec_disabled",
|
||||
"dns_rsasha1_in_use_to_key_sign_in_dnssec",
|
||||
"dns_rsasha1_in_use_to_zone_sign_in_dnssec",
|
||||
"kms_key_not_publicly_accessible",
|
||||
"kms_key_rotation_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-tpr",
|
||||
"Name": "KSI-TPR: Third-Party Information Resources",
|
||||
"Description": "A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-tpr",
|
||||
"Section": "Third-Party Information Resources",
|
||||
"Service": "gcp"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"artifacts_container_analysis_enabled",
|
||||
"gcr_container_scanning_enabled",
|
||||
"compute_public_address_shodan",
|
||||
"cloudsql_instance_automated_backups",
|
||||
"iam_sa_user_managed_key_rotate_90_days",
|
||||
"iam_service_account_unused",
|
||||
"gemini_api_disabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-iam-07",
|
||||
"Name": "KSI-IAM-07: Account Lifecycle Management",
|
||||
"Description": "Securely manage the lifecycle and privileges of all accounts, roles, and groups",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-iam-07",
|
||||
"Section": "Identity and Access Management",
|
||||
"Service": "gcp"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"apikeys_key_rotated_in_90_days",
|
||||
"iam_sa_user_managed_key_rotate_90_days",
|
||||
"iam_sa_user_managed_key_unused",
|
||||
"iam_service_account_unused",
|
||||
"compute_instance_default_service_account_in_use"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ksi-mla-07",
|
||||
"Name": "KSI-MLA-07: Monitoring and Logging Inventory",
|
||||
"Description": "Maintain a list of information resources and event types that will be monitored, logged, and audited",
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "ksi-mla-07",
|
||||
"Section": "Monitoring, Logging, and Auditing",
|
||||
"Service": "gcp"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"iam_audit_logs_enabled",
|
||||
"iam_cloud_asset_inventory_enabled",
|
||||
"logging_sink_created",
|
||||
"compute_subnet_flow_logs_enabled",
|
||||
"compute_network_dns_logging_enabled"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -190,6 +190,14 @@ aws:
|
||||
# MEDIUM
|
||||
ecr_repository_vulnerability_minimum_severity: "MEDIUM"
|
||||
|
||||
# AWS Inspector2
|
||||
# aws.inspector2_coverage_recently_scanned
|
||||
# Maximum days since Inspector2 last scanned an actively covered resource
|
||||
inspector2_max_days_since_last_scan: 3
|
||||
# aws.inspector2_active_findings_within_max_age
|
||||
# Maximum days an Inspector2 finding can stay active since it was first observed
|
||||
inspector2_active_finding_max_age_days: 192
|
||||
|
||||
# AWS Trusted Advisor
|
||||
# aws.trustedadvisor_premium_support_plan_subscribed
|
||||
verify_premium_support_plans: True
|
||||
|
||||
@@ -333,6 +333,20 @@ class AWSProviderConfig(ProviderConfigBase):
|
||||
description="Highest severity tolerated for ECR images.",
|
||||
)
|
||||
|
||||
# --- Inspector2 -------------------------------------------------------
|
||||
inspector2_max_days_since_last_scan: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=90,
|
||||
description="Days since Inspector2 last scanned a covered resource. Range: 1..90.",
|
||||
)
|
||||
inspector2_active_finding_max_age_days: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=365,
|
||||
description="Days an Inspector2 finding can stay active since first observed. Range: 1..365.",
|
||||
)
|
||||
|
||||
# --- Trusted Advisor --------------------------------------------------
|
||||
verify_premium_support_plans: Optional[bool] = None
|
||||
|
||||
|
||||
@@ -3,12 +3,19 @@ import pathlib
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
from re import fullmatch
|
||||
from typing import Optional
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from boto3.session import Session
|
||||
from botocore.config import Config
|
||||
from botocore.credentials import RefreshableCredentials
|
||||
from botocore.exceptions import ClientError, NoCredentialsError, ProfileNotFound
|
||||
from botocore.exceptions import (
|
||||
ClientError,
|
||||
ConnectTimeoutError,
|
||||
EndpointConnectionError,
|
||||
NoCredentialsError,
|
||||
ProfileNotFound,
|
||||
ReadTimeoutError,
|
||||
)
|
||||
from botocore.session import Session as BotocoreSession
|
||||
from colorama import Fore, Style
|
||||
from pytz import utc
|
||||
@@ -263,7 +270,10 @@ class AwsProvider(Provider):
|
||||
caller_identity = self.validate_credentials(
|
||||
session=self.session.current_session,
|
||||
aws_region=sts_region,
|
||||
excluded_regions=excluded_regions,
|
||||
)
|
||||
# Later STS calls go where validation got an answer, not where it timed out
|
||||
sts_region = caller_identity.region
|
||||
|
||||
logger.info("Credentials validated")
|
||||
########
|
||||
@@ -690,8 +700,6 @@ class AwsProvider(Provider):
|
||||
or session.region_name
|
||||
or AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
)
|
||||
sts_client = AwsProvider.create_sts_session(session, sts_region)
|
||||
|
||||
# TODO: pass values from the input
|
||||
mfa_info = AwsProvider.input_role_mfa_token_and_code()
|
||||
# TODO: validate MFA ARN here
|
||||
@@ -699,8 +707,12 @@ class AwsProvider(Provider):
|
||||
"SerialNumber": mfa_info.arn,
|
||||
"TokenCode": mfa_info.totp,
|
||||
}
|
||||
session_credentials = sts_client.get_session_token(
|
||||
**get_session_token_arguments
|
||||
_, session_credentials = AwsProvider.sts_call_with_partition_failover(
|
||||
session,
|
||||
sts_region,
|
||||
lambda sts_client: sts_client.get_session_token(
|
||||
**get_session_token_arguments
|
||||
),
|
||||
)
|
||||
mfa_session = Session(
|
||||
aws_access_key_id=session_credentials["Credentials"]["AccessKeyId"],
|
||||
@@ -1244,10 +1256,11 @@ class AwsProvider(Provider):
|
||||
mfa_info = AwsProvider.input_role_mfa_token_and_code()
|
||||
assume_role_arguments["SerialNumber"] = mfa_info.arn
|
||||
assume_role_arguments["TokenCode"] = mfa_info.totp
|
||||
sts_client = AwsProvider.create_sts_session(
|
||||
session, assumed_role_info.sts_region
|
||||
_, assumed_credentials = AwsProvider.sts_call_with_partition_failover(
|
||||
session,
|
||||
assumed_role_info.sts_region,
|
||||
lambda sts_client: sts_client.assume_role(**assume_role_arguments),
|
||||
)
|
||||
assumed_credentials = sts_client.assume_role(**assume_role_arguments)
|
||||
# Convert the UTC datetime object to your local timezone
|
||||
credentials_expiration_local_time = (
|
||||
assumed_credentials["Credentials"]["Expiration"]
|
||||
@@ -1326,30 +1339,98 @@ class AwsProvider(Provider):
|
||||
)
|
||||
raise error
|
||||
|
||||
@staticmethod
|
||||
def sts_call_with_partition_failover(
|
||||
session: Session,
|
||||
aws_region: str,
|
||||
operation: Callable[[Any], Any],
|
||||
excluded_regions: set[str] | None = None,
|
||||
) -> tuple[str, Any]:
|
||||
"""
|
||||
Run a bootstrap STS call, moving on when a region cannot be reached.
|
||||
|
||||
Bootstrap calls happen before anything is known about the credentials, so
|
||||
the region they go to is a guess whenever none was configured. On a network
|
||||
that routes to only one region of its partition that guess is fatal, and the
|
||||
remaining regions of the partition declared in PROWLER_AWS_PARTITION are the
|
||||
ones worth trying.
|
||||
|
||||
Args:
|
||||
session (Session): The AWS session object.
|
||||
aws_region (str): The region to try first.
|
||||
operation (Callable[[Any], Any]): Receives an STS client and performs
|
||||
the call.
|
||||
excluded_regions (set[str] | None): Regions excluded from the scan,
|
||||
tried after the rest of the partition.
|
||||
|
||||
Returns:
|
||||
tuple[str, Any]: The region that answered and whatever the operation
|
||||
returned.
|
||||
|
||||
Raises:
|
||||
Exception: Whatever the operation raises, or the last connection error
|
||||
when no region could be reached.
|
||||
"""
|
||||
*fallback_regions, last_region = get_partition_bootstrap_candidates(
|
||||
aws_region, session.region_name, excluded_regions
|
||||
)
|
||||
|
||||
for candidate_region in fallback_regions:
|
||||
try:
|
||||
sts_client = AwsProvider.create_sts_session(session, candidate_region)
|
||||
return candidate_region, operation(sts_client)
|
||||
# The credentials are not at fault, so the next region is worth trying
|
||||
except (
|
||||
EndpointConnectionError,
|
||||
ConnectTimeoutError,
|
||||
ReadTimeoutError,
|
||||
) as unreachable:
|
||||
logger.warning(
|
||||
f"{unreachable.__class__.__name__}[{unreachable.__traceback__.tb_lineno}]: {unreachable}"
|
||||
)
|
||||
|
||||
# Nothing is left to try after the last region, so its error is the answer
|
||||
sts_client = AwsProvider.create_sts_session(session, last_region)
|
||||
return last_region, operation(sts_client)
|
||||
|
||||
@staticmethod
|
||||
def validate_credentials(
|
||||
session: Session,
|
||||
aws_region: str,
|
||||
excluded_regions: set[str] | None = None,
|
||||
) -> AWSCallerIdentity:
|
||||
"""
|
||||
Validates the AWS credentials using the provided session and AWS region.
|
||||
|
||||
When the region cannot be reached, the remaining regions of the partition
|
||||
declared in PROWLER_AWS_PARTITION are tried before giving up. A credential
|
||||
error is returned from the first region instead, since it would be the same
|
||||
everywhere.
|
||||
|
||||
Args:
|
||||
session (Session): The AWS session object.
|
||||
aws_region (str): The AWS region to validate the credentials.
|
||||
excluded_regions (set[str] | None): Regions excluded from the scan,
|
||||
tried after the rest of the partition.
|
||||
Returns:
|
||||
AWSCallerIdentity: An object containing the caller identity information.
|
||||
AWSCallerIdentity: An object containing the caller identity information,
|
||||
including the region that answered.
|
||||
Raises:
|
||||
Exception: If an error occurs during the validation process.
|
||||
"""
|
||||
try:
|
||||
sts_client = AwsProvider.create_sts_session(session, aws_region)
|
||||
caller_identity = sts_client.get_caller_identity()
|
||||
sts_region, caller_identity = AwsProvider.sts_call_with_partition_failover(
|
||||
session,
|
||||
aws_region,
|
||||
lambda sts_client: sts_client.get_caller_identity(),
|
||||
excluded_regions,
|
||||
)
|
||||
# Include the region where the caller_identity has validated the credentials
|
||||
return AWSCallerIdentity(
|
||||
user_id=caller_identity.get("UserId"),
|
||||
account=caller_identity.get("Account"),
|
||||
arn=ARN(caller_identity.get("Arn")),
|
||||
region=aws_region,
|
||||
region=sts_region,
|
||||
)
|
||||
except ClientError as client_error:
|
||||
logger.error(
|
||||
@@ -1846,6 +1927,49 @@ def get_env_partition_bootstrap_region(
|
||||
return regions[0] if regions else None
|
||||
|
||||
|
||||
# An unreachable endpoint costs a connection timeout, so a partition with many
|
||||
# regions is not walked in full
|
||||
MAX_STS_BOOTSTRAP_ATTEMPTS = 3
|
||||
|
||||
|
||||
def get_partition_bootstrap_candidates(
|
||||
aws_region: str,
|
||||
session_region: Optional[str] = None,
|
||||
excluded_regions: set[str] | None = None,
|
||||
) -> list:
|
||||
"""
|
||||
Get the STS bootstrap regions to try, in order, starting with the chosen one.
|
||||
|
||||
A deployment reached only through its own region's endpoints has no route to
|
||||
the rest of its partition, and which region that is cannot be known from the
|
||||
environment alone: a container may carry a region belonging to no partition
|
||||
it scans. Offering the remaining regions of the declared partition lets the
|
||||
bootstrap succeed without anything having to declare the right one.
|
||||
|
||||
Args:
|
||||
aws_region (str): The region already chosen for the bootstrap call.
|
||||
session_region (Optional[str]): The region of the AWS session.
|
||||
excluded_regions (set[str] | None): Regions excluded from the scan. They
|
||||
go after the rest of the partition, so the bootstrap avoids them
|
||||
whenever another region answers and still has them as a last resort.
|
||||
|
||||
Returns:
|
||||
list: The regions to try, preferred first, capped at
|
||||
MAX_STS_BOOTSTRAP_ATTEMPTS.
|
||||
"""
|
||||
excluded_regions = set(excluded_regions or ())
|
||||
partition_regions = get_env_partition_regions(session_region) or []
|
||||
# sorted() is stable, so the partition order survives on each side of the split
|
||||
ordered_regions = sorted(
|
||||
partition_regions, key=lambda region: region in excluded_regions
|
||||
)
|
||||
candidates = [aws_region]
|
||||
for region in ordered_regions:
|
||||
if region not in candidates:
|
||||
candidates.append(region)
|
||||
return candidates[:MAX_STS_BOOTSTRAP_ATTEMPTS]
|
||||
|
||||
|
||||
# TODO: This can be moved to another class since it doesn't need self
|
||||
def get_aws_region_for_sts(
|
||||
session_region: str,
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "elbv2_listener_fips_tls_enabled",
|
||||
"CheckTitle": "ELBv2 HTTPS/TLS listeners use a FIPS TLS security policy",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "elbv2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "low",
|
||||
"ResourceType": "AwsElbv2LoadBalancer",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**ELBv2 HTTPS and TLS listeners** are assessed for use of a **FIPS** TLS security policy (`ELBSecurityPolicy-*-FIPS-*`). FIPS policies terminate TLS with the AWS-LC FIPS validated cryptographic module.",
|
||||
"Risk": "Listeners without a FIPS policy terminate TLS with cryptographic modules that are not FIPS 140 validated, which does not meet requirements to protect federal or regulated data with **NIST CMVP validated cryptography**.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/elasticloadbalancing/latest/application/describe-ssl-policies.html",
|
||||
"https://docs.aws.amazon.com/elasticloadbalancing/latest/network/describe-ssl-policies.html",
|
||||
"https://aws.amazon.com/compliance/fips/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws elbv2 modify-listener --listener-arn <listener_arn> --ssl-policy ELBSecurityPolicy-TLS13-1-2-FIPS-2023-04",
|
||||
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::ElasticLoadBalancingV2::Listener\n Properties:\n LoadBalancerArn: <example_resource_arn>\n Protocol: HTTPS\n Port: 443\n DefaultActions:\n - Type: forward\n TargetGroupArn: <example_resource_arn>\n Certificates:\n - CertificateArn: <example_certificate_arn>\n SslPolicy: ELBSecurityPolicy-TLS13-1-2-FIPS-2023-04 # FIX: uses a FIPS TLS policy\n```",
|
||||
"Other": "1. In the AWS Console, go to EC2 > Load Balancers\n2. Select the load balancer and open the Listeners tab\n3. Select each HTTPS/TLS listener and choose Edit\n4. Set Security policy to a FIPS policy such as ELBSecurityPolicy-TLS13-1-2-FIPS-2023-04\n5. Save changes",
|
||||
"Terraform": "```hcl\nresource \"aws_lb_listener\" \"<example_resource_name>\" {\n load_balancer_arn = \"<example_resource_arn>\"\n port = 443\n protocol = \"HTTPS\"\n ssl_policy = \"ELBSecurityPolicy-TLS13-1-2-FIPS-2023-04\" # FIX: FIPS TLS policy\n certificate_arn = \"<example_certificate_arn>\"\n\n default_action {\n type = \"forward\"\n target_group_arn = \"<example_resource_arn>\"\n }\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Use a **FIPS** TLS security policy, such as `ELBSecurityPolicy-TLS13-1-2-FIPS-2023-04`, on every HTTPS and TLS listener that carries federal or regulated data.",
|
||||
"Url": "https://hub.prowler.com/check/elbv2_listener_fips_tls_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"encryption"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"elbv2_insecure_ssl_ciphers",
|
||||
"elbv2_listener_pqc_tls_enabled"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.elbv2.elbv2_client import elbv2_client
|
||||
|
||||
|
||||
class elbv2_listener_fips_tls_enabled(Check):
|
||||
"""Ensure every ELBv2 HTTPS or TLS listener uses a FIPS TLS security policy."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report whether each load balancer terminates HTTPS/TLS with a FIPS policy."""
|
||||
findings = []
|
||||
for lb in elbv2_client.loadbalancersv2.values():
|
||||
if lb.listener_discovery_failed:
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=lb)
|
||||
tls_listeners = {
|
||||
listener_arn: listener
|
||||
for listener_arn, listener in lb.listeners.items()
|
||||
if listener.protocol in ("HTTPS", "TLS")
|
||||
}
|
||||
non_fips_listeners = [
|
||||
f"{listener.protocol}:{listener.port} ({listener_arn}) uses {listener.ssl_policy or '<none>'}"
|
||||
for listener_arn, listener in tls_listeners.items()
|
||||
if "FIPS" not in (listener.ssl_policy or "").split("-")
|
||||
]
|
||||
if not tls_listeners:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ELBv2 {lb.name} has no HTTPS/TLS listeners."
|
||||
elif non_fips_listeners:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"ELBv2 {lb.name} has HTTPS/TLS listeners without a FIPS TLS security policy: {', '.join(non_fips_listeners)}."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ELBv2 {lb.name} has all HTTPS/TLS listeners using a FIPS TLS security policy."
|
||||
findings.append(report)
|
||||
return findings
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "inspector2_active_findings_kev_within_due_date",
|
||||
"CheckTitle": "Inspector2 has no active findings for CISA Known Exploited Vulnerabilities past their remediation due date",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Vulnerabilities/CVE",
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "inspector2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "critical",
|
||||
"ResourceType": "Other",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "**Amazon Inspector** active findings for **CISA Known Exploited Vulnerabilities** are compared with the remediation due date (`dateDue`) that CISA assigns to each entry of the KEV catalog. Findings that are still active after that date are reported.\n\nThe result is reported per Region where Inspector is enabled.",
|
||||
"Risk": "CISA due dates reflect **active exploitation**. Missing them keeps exploited vulnerabilities open beyond the window CISA sets for federal agencies and shows that vulnerability response is not keeping pace with real threats.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
|
||||
"https://docs.aws.amazon.com/inspector/v2/APIReference/API_BatchGetFindingDetails.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In the AWS Console, open Amazon Inspector > Findings\n2. Filter by Vulnerability ID for each overdue CVE reported by this check\n3. Patch or upgrade the affected packages, rebuild container images or update Lambda runtimes\n4. If a fix is not available, apply the mitigations listed in the CISA catalog entry\n5. Confirm the findings move to Closed",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Track every KEV finding against its **CISA due date** and remediate before it passes. When no fix exists yet, apply the vendor or CISA mitigations and document the residual risk.",
|
||||
"Url": "https://hub.prowler.com/check/inspector2_active_findings_kev_within_due_date"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"vulnerabilities"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"inspector2_active_findings_no_known_exploited_vulnerabilities"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.inspector2.inspector2_client import (
|
||||
inspector2_client,
|
||||
)
|
||||
from prowler.providers.aws.services.inspector2.lib.vulnerabilities import (
|
||||
summarize_vulnerabilities,
|
||||
)
|
||||
|
||||
|
||||
class inspector2_active_findings_kev_within_due_date(Check):
|
||||
"""Ensure active Inspector2 findings for CISA KEVs are not past their CISA due date."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report, per Region, whether any CISA KEV finding is past its remediation due date."""
|
||||
findings = []
|
||||
now = datetime.now(timezone.utc)
|
||||
known_exploited = inspector2_client.known_exploited_vulnerabilities
|
||||
lookup_failed = inspector2_client.vulnerability_lookup_failed
|
||||
for inspector in inspector2_client.inspectors:
|
||||
if inspector.status != "ENABLED":
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=inspector)
|
||||
if inspector.findings is None:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 findings could not be retrieved in region {inspector.region}; "
|
||||
"verify the inspector2:ListFindings permission."
|
||||
)
|
||||
findings.append(report)
|
||||
continue
|
||||
vulnerability_ids = {
|
||||
finding.vulnerability_id
|
||||
for finding in inspector.findings
|
||||
if finding.vulnerability_id
|
||||
}
|
||||
overdue = sorted(
|
||||
f"{vulnerability_id} (due {known_exploited[vulnerability_id].date_due.date().isoformat()})"
|
||||
for vulnerability_id in known_exploited.keys() & vulnerability_ids
|
||||
if known_exploited[vulnerability_id].date_due
|
||||
and known_exploited[vulnerability_id].date_due < now
|
||||
)
|
||||
unverified_ids = sorted(vulnerability_ids & lookup_failed)
|
||||
if overdue:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 has active findings in region {inspector.region} for CISA "
|
||||
"Known Exploited Vulnerabilities past their remediation due date: "
|
||||
f"{summarize_vulnerabilities(overdue)}."
|
||||
)
|
||||
elif unverified_ids:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
"Inspector2 could not verify the CISA Known Exploited Vulnerabilities status of "
|
||||
f"{summarize_vulnerabilities(unverified_ids)} in region {inspector.region}; "
|
||||
"verify the inspector2:BatchGetFindingDetails permission."
|
||||
)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Inspector2 has no active findings in region {inspector.region} for CISA "
|
||||
"Known Exploited Vulnerabilities past their remediation due date."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "inspector2_active_findings_no_known_exploited_vulnerabilities",
|
||||
"CheckTitle": "Inspector2 has no active findings for CISA Known Exploited Vulnerabilities",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Vulnerabilities/CVE",
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "inspector2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "Other",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "**Amazon Inspector** active findings are cross-referenced with the **CISA Known Exploited Vulnerabilities (KEV)** catalog, using the CISA data that Inspector returns in the finding details (`BatchGetFindingDetails`) of each CVE.\n\nThe result is reported per Region where Inspector is enabled.",
|
||||
"Risk": "KEV entries are vulnerabilities **confirmed as exploited in the wild**. Workloads carrying them are prime targets for initial access and ransomware, enabling remote code execution, data exfiltration and lateral movement.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
|
||||
"https://docs.aws.amazon.com/inspector/v2/APIReference/API_BatchGetFindingDetails.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In the AWS Console, open Amazon Inspector > Findings\n2. Filter by Vulnerability ID for each CVE reported by this check\n3. Patch or upgrade the affected packages, rebuild container images or update Lambda runtimes\n4. Confirm the findings move to Closed",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Remediate KEV findings before any other vulnerability: patch or upgrade the affected packages, rebuild and redeploy container images, and stop deploying new resources that carry **known exploited vulnerabilities**.",
|
||||
"Url": "https://hub.prowler.com/check/inspector2_active_findings_no_known_exploited_vulnerabilities"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"vulnerabilities"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"inspector2_active_findings_exist",
|
||||
"inspector2_active_findings_kev_within_due_date"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.inspector2.inspector2_client import (
|
||||
inspector2_client,
|
||||
)
|
||||
from prowler.providers.aws.services.inspector2.lib.vulnerabilities import (
|
||||
summarize_vulnerabilities,
|
||||
)
|
||||
|
||||
|
||||
class inspector2_active_findings_no_known_exploited_vulnerabilities(Check):
|
||||
"""Ensure no active Inspector2 finding is a CISA Known Exploited Vulnerability."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report, per Region, whether any active finding is a CISA Known Exploited Vulnerability."""
|
||||
findings = []
|
||||
known_exploited = inspector2_client.known_exploited_vulnerabilities
|
||||
lookup_failed = inspector2_client.vulnerability_lookup_failed
|
||||
for inspector in inspector2_client.inspectors:
|
||||
if inspector.status != "ENABLED":
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=inspector)
|
||||
if inspector.findings is None:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 findings could not be retrieved in region {inspector.region}; "
|
||||
"verify the inspector2:ListFindings permission."
|
||||
)
|
||||
findings.append(report)
|
||||
continue
|
||||
vulnerability_ids = {
|
||||
finding.vulnerability_id
|
||||
for finding in inspector.findings
|
||||
if finding.vulnerability_id
|
||||
}
|
||||
kev_ids = sorted(known_exploited.keys() & vulnerability_ids)
|
||||
unverified_ids = sorted(vulnerability_ids & lookup_failed)
|
||||
if kev_ids:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 has active findings in region {inspector.region} for CISA "
|
||||
f"Known Exploited Vulnerabilities: {summarize_vulnerabilities(kev_ids)}."
|
||||
)
|
||||
elif unverified_ids:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
"Inspector2 could not verify the CISA Known Exploited Vulnerabilities status of "
|
||||
f"{summarize_vulnerabilities(unverified_ids)} in region {inspector.region}; "
|
||||
"verify the inspector2:BatchGetFindingDetails permission."
|
||||
)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Inspector2 has no active findings in region {inspector.region} for CISA "
|
||||
"Known Exploited Vulnerabilities."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "inspector2_active_findings_within_max_age",
|
||||
"CheckTitle": "Inspector2 has no active findings older than the configured maximum age",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Vulnerabilities/CVE",
|
||||
"Software and Configuration Checks/Patch Management",
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "inspector2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "**Amazon Inspector** active findings are evaluated against the configurable `inspector2_active_finding_max_age_days` threshold (192 days by default), using the time since each finding was first observed (`firstObservedAt`). Suppressed and closed findings are not active and are not evaluated.\n\nThe result is reported per Region where Inspector is enabled.",
|
||||
"Risk": "Findings left open for months show that **vulnerability response** is not keeping up. Long-lived vulnerabilities give attackers time to discover and exploit them, and a backlog that is neither fixed nor formally accepted hides real risk from decision makers.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/inspector/latest/user/findings-understanding.html",
|
||||
"https://docs.aws.amazon.com/inspector/latest/user/findings-managing-supression-rules.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In the Amazon Inspector console, open Findings and filter by Finding status = Active\n2. Remediate the findings first observed longest ago\n3. For vulnerabilities you formally accept, choose Suppression rules in the navigation pane and create a rule so they stop counting as active",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Remediate findings within your vulnerability response timeframes. Vulnerabilities you decide not to fix should be formally **accepted** and suppressed with a documented justification instead of staying active indefinitely.",
|
||||
"Url": "https://hub.prowler.com/check/inspector2_active_findings_within_max_age"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"vulnerabilities"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"inspector2_active_findings_exist"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.inspector2.inspector2_client import (
|
||||
inspector2_client,
|
||||
)
|
||||
|
||||
|
||||
class inspector2_active_findings_within_max_age(Check):
|
||||
"""Ensure no Inspector2 finding stays active longer than the configured days."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report, per Region, whether any active finding is older than the allowed days."""
|
||||
findings = []
|
||||
max_age_days = inspector2_client.audit_config.get(
|
||||
"inspector2_active_finding_max_age_days", 192
|
||||
)
|
||||
max_age = timedelta(days=max_age_days)
|
||||
now = datetime.now(timezone.utc)
|
||||
for inspector in inspector2_client.inspectors:
|
||||
if inspector.status != "ENABLED":
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=inspector)
|
||||
if inspector.findings is None:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 findings could not be retrieved in region {inspector.region}; "
|
||||
"verify the inspector2:ListFindings permission."
|
||||
)
|
||||
findings.append(report)
|
||||
continue
|
||||
stale_ages = [
|
||||
now - finding.first_observed_at
|
||||
for finding in inspector.findings
|
||||
if finding.first_observed_at
|
||||
and now - finding.first_observed_at > max_age
|
||||
]
|
||||
if stale_ages:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 has {len(stale_ages)} active findings in region {inspector.region} "
|
||||
f"first observed more than {max_age_days} days ago, the oldest {max(stale_ages).days} days ago."
|
||||
)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Inspector2 has no active findings in region {inspector.region} "
|
||||
f"first observed more than {max_age_days} days ago."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "inspector2_coverage_recently_scanned",
|
||||
"CheckTitle": "Inspector2 covered resource was scanned within the configured number of days",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Vulnerabilities/CVE",
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "inspector2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "**Amazon Inspector** coverage is evaluated for every actively monitored resource. The time since the resource was last scanned (`lastScannedAt`) is compared with the configurable `inspector2_max_days_since_last_scan` threshold (3 days by default).\n\nResources still pending their first scan are not evaluated.",
|
||||
"Risk": "Stale scans leave **newly published CVEs** and configuration **drift** undetected. A resource that has not been rescanned for weeks can keep running exploitable packages long after a fix is available.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/inspector/latest/user/assessing-coverage.html",
|
||||
"https://docs.aws.amazon.com/inspector/latest/user/scanning-ecr.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In the Amazon Inspector console, choose Account management and review the Last scanned at value in the Instances, Container images and Lambda functions tabs\n2. For EC2 instances, confirm the SSM Agent is healthy or, under General settings > EC2 scanning settings, set the scan mode to hybrid\n3. For ECR images, increase the Amazon ECR re-scan duration in the Amazon Inspector settings\n4. Confirm the resources are rescanned",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Keep continuous scanning healthy: use **hybrid** EC2 scanning so instances without a working SSM agent are still scanned, set a long ECR **rescan duration** for images in use, and investigate every resource whose last scan is older than the allowed window.",
|
||||
"Url": "https://hub.prowler.com/check/inspector2_coverage_recently_scanned"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"vulnerabilities"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_coverage_scan_status_active"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.inspector2.inspector2_client import (
|
||||
inspector2_client,
|
||||
)
|
||||
|
||||
PENDING_SCAN_REASONS = {
|
||||
"PENDING_INITIAL_SCAN",
|
||||
"PENDING_REVIVAL_SCAN",
|
||||
"SCAN_IN_PROGRESS",
|
||||
}
|
||||
|
||||
|
||||
class inspector2_coverage_recently_scanned(Check):
|
||||
"""Ensure Inspector2 scanned every actively covered resource within the configured days."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report whether each actively covered resource was scanned within the allowed days."""
|
||||
findings = []
|
||||
max_days = inspector2_client.audit_config.get(
|
||||
"inspector2_max_days_since_last_scan", 3
|
||||
)
|
||||
max_elapsed = timedelta(days=max_days)
|
||||
now = datetime.now(timezone.utc)
|
||||
for inspector in inspector2_client.inspectors:
|
||||
if inspector.status != "ENABLED":
|
||||
continue
|
||||
if inspector.coverage is None:
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=inspector)
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 coverage could not be retrieved in region {inspector.region}; "
|
||||
"verify the inspector2:ListCoverage permission."
|
||||
)
|
||||
findings.append(report)
|
||||
continue
|
||||
for resource in inspector.coverage:
|
||||
if resource.scan_status_code != "ACTIVE":
|
||||
continue
|
||||
if (
|
||||
resource.last_scanned_at is None
|
||||
and resource.scan_status_reason in PENDING_SCAN_REASONS
|
||||
):
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=resource)
|
||||
if resource.last_scanned_at is None:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{resource.resource_type} {resource.id} has no recorded Inspector2 scan."
|
||||
elif now - resource.last_scanned_at > max_elapsed:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{resource.resource_type} {resource.id} was last scanned by Inspector2 more than {max_days} days ago."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"{resource.resource_type} {resource.id} was last scanned by Inspector2 within the last {max_days} days."
|
||||
findings.append(report)
|
||||
return findings
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "inspector2_coverage_scan_status_active",
|
||||
"CheckTitle": "Inspector2 covered resource is actively scanned",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Vulnerabilities/CVE",
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "inspector2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "**Amazon Inspector** coverage is evaluated for every resource it tracks (EC2 instances, ECR images and repositories, Lambda functions). A resource whose scan status is `INACTIVE`, for example because of `UNMANAGED_EC2_INSTANCE`, `NO_INVENTORY`, `UNSUPPORTED_OS` or `ACCESS_DENIED`, is not being scanned for vulnerabilities.\n\nStopped, terminated, tag-excluded and aged-out resources are not evaluated.",
|
||||
"Risk": "Resources that Inspector cannot scan silently fall out of **vulnerability detection**. New CVEs affecting those workloads are never reported, so exploitable software can stay deployed while the account still appears covered.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/inspector/latest/user/assessing-coverage.html",
|
||||
"https://docs.aws.amazon.com/inspector/latest/user/scanning-ec2.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In the Amazon Inspector console, choose Account management\n2. Open the Instances, Container images or Lambda functions tab and review the resources that are not actively scanned\n3. Fix the reported cause: register EC2 instances with Systems Manager or set the EC2 scan mode to hybrid, use supported operating systems and runtimes, and grant access to the required encryption keys\n4. Confirm the resource is actively scanned",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Resolve the reason reported in each inactive resource's scan status so Inspector can scan every in-scope workload. Register EC2 instances with **Systems Manager** or enable **hybrid scanning**, keep operating systems and runtimes supported, and treat scanning gaps as vulnerabilities to track.",
|
||||
"Url": "https://hub.prowler.com/check/inspector2_coverage_scan_status_active"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"vulnerabilities"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"inspector2_is_enabled"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.inspector2.inspector2_client import (
|
||||
inspector2_client,
|
||||
)
|
||||
|
||||
NOT_APPLICABLE_SCAN_REASONS = {
|
||||
"EC2_INSTANCE_STOPPED",
|
||||
"EXCLUDED_BY_TAG",
|
||||
"NO_RESOURCES_FOUND",
|
||||
"PENDING_DISABLE",
|
||||
"RESOURCE_TERMINATED",
|
||||
"SCAN_ELIGIBILITY_EXPIRED",
|
||||
}
|
||||
|
||||
|
||||
class inspector2_coverage_scan_status_active(Check):
|
||||
"""Ensure Inspector2 is actively scanning every covered resource."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report whether Inspector2 is actively scanning each covered resource."""
|
||||
findings = []
|
||||
for inspector in inspector2_client.inspectors:
|
||||
if inspector.status != "ENABLED":
|
||||
continue
|
||||
if inspector.coverage is None:
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=inspector)
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"Inspector2 coverage could not be retrieved in region {inspector.region}; "
|
||||
"verify the inspector2:ListCoverage permission."
|
||||
)
|
||||
findings.append(report)
|
||||
continue
|
||||
for resource in inspector.coverage:
|
||||
if resource.scan_status_reason in NOT_APPLICABLE_SCAN_REASONS:
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=resource)
|
||||
if resource.scan_status_code == "ACTIVE":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Inspector2 is actively scanning {resource.resource_type} {resource.id}."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
reason = resource.scan_status_reason or "no reason reported"
|
||||
report.status_extended = f"Inspector2 is not scanning {resource.resource_type} {resource.id}: {reason}."
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -1,16 +1,33 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
|
||||
from prowler.providers.aws.lib.service.service import AWSService
|
||||
|
||||
FINDING_DETAILS_BATCH_SIZE = 10
|
||||
|
||||
|
||||
class Inspector2(AWSService):
|
||||
def __init__(self, provider):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.inspectors = []
|
||||
self.known_exploited_vulnerabilities = {}
|
||||
self.vulnerability_lookup_failed = set()
|
||||
self.__threading_call__(self._batch_get_account_status)
|
||||
self.__threading_call__(self._list_active_findings, self.inspectors)
|
||||
enabled_inspectors = [
|
||||
inspector for inspector in self.inspectors if inspector.status == "ENABLED"
|
||||
]
|
||||
self.__threading_call__(self._list_findings, enabled_inspectors)
|
||||
self.__threading_call__(self._list_coverage, enabled_inspectors)
|
||||
self.__threading_call__(
|
||||
self._batch_get_finding_details,
|
||||
self._get_finding_detail_batches(enabled_inspectors),
|
||||
)
|
||||
|
||||
def _batch_get_account_status(self, regional_client):
|
||||
# We use this function to check if inspector2 is enabled
|
||||
@@ -59,6 +76,188 @@ class Inspector2(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_findings(self, inspector):
|
||||
"""Store the active findings of the audited account for an enabled Region."""
|
||||
logger.info("Inspector2 - Listing active findings details...")
|
||||
try:
|
||||
paginator = self.regional_clients[inspector.region].get_paginator(
|
||||
"list_findings"
|
||||
)
|
||||
findings = []
|
||||
for page in paginator.paginate(
|
||||
filterCriteria={
|
||||
"awsAccountId": [
|
||||
{"comparison": "EQUALS", "value": self.audited_account},
|
||||
],
|
||||
"findingStatus": [{"comparison": "EQUALS", "value": "ACTIVE"}],
|
||||
},
|
||||
PaginationConfig={"PageSize": 100},
|
||||
):
|
||||
for finding in page.get("findings", []):
|
||||
findings.append(
|
||||
Finding(
|
||||
arn=finding.get("findingArn", ""),
|
||||
type=finding.get("type", ""),
|
||||
severity=finding.get("severity", ""),
|
||||
first_observed_at=finding.get("firstObservedAt"),
|
||||
vulnerability_id=finding.get(
|
||||
"packageVulnerabilityDetails", {}
|
||||
).get("vulnerabilityId"),
|
||||
resource_ids=[
|
||||
resource["id"]
|
||||
for resource in finding.get("resources", [])
|
||||
if resource.get("id")
|
||||
],
|
||||
)
|
||||
)
|
||||
inspector.findings = findings
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{inspector.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_coverage(self, inspector):
|
||||
"""Store the resources Inspector2 covers in an enabled Region, respecting audit resources."""
|
||||
logger.info("Inspector2 - Listing coverage...")
|
||||
try:
|
||||
paginator = self.regional_clients[inspector.region].get_paginator(
|
||||
"list_coverage"
|
||||
)
|
||||
coverage = []
|
||||
for page in paginator.paginate(
|
||||
filterCriteria={
|
||||
"accountId": [
|
||||
{"comparison": "EQUALS", "value": self.audited_account},
|
||||
],
|
||||
},
|
||||
PaginationConfig={"PageSize": 200},
|
||||
):
|
||||
for covered_resource in page.get("coveredResources", []):
|
||||
resource_id = covered_resource.get("resourceId", "")
|
||||
resource_type = covered_resource.get("resourceType", "")
|
||||
scan_status = covered_resource.get("scanStatus", {})
|
||||
arn = self._get_covered_resource_arn(
|
||||
resource_type, resource_id, inspector.region
|
||||
)
|
||||
if self.audit_resources and not is_resource_filtered(
|
||||
arn, self.audit_resources
|
||||
):
|
||||
continue
|
||||
coverage.append(
|
||||
CoveredResource(
|
||||
id=resource_id,
|
||||
arn=arn,
|
||||
region=inspector.region,
|
||||
resource_type=resource_type,
|
||||
scan_type=covered_resource.get("scanType", ""),
|
||||
scan_status_code=scan_status.get("statusCode", ""),
|
||||
scan_status_reason=scan_status.get("reason", ""),
|
||||
last_scanned_at=covered_resource.get("lastScannedAt"),
|
||||
)
|
||||
)
|
||||
inspector.coverage = coverage
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{inspector.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_covered_resource_arn(self, resource_type, resource_id, region):
|
||||
"""Return the ARN of a covered resource, building it for EC2 instance IDs."""
|
||||
if resource_type == "AWS_EC2_INSTANCE" and not resource_id.startswith("arn:"):
|
||||
return f"arn:{self.audited_partition}:ec2:{region}:{self.audited_account}:instance/{resource_id}"
|
||||
return resource_id
|
||||
|
||||
@staticmethod
|
||||
def _get_finding_detail_batches(inspectors):
|
||||
"""Group one active finding per CVE into finding details batches per Region."""
|
||||
representatives = {}
|
||||
for inspector in inspectors:
|
||||
for finding in inspector.findings or []:
|
||||
if finding.vulnerability_id and finding.vulnerability_id.startswith(
|
||||
"CVE-"
|
||||
):
|
||||
representatives.setdefault(
|
||||
finding.vulnerability_id, (inspector.region, finding.arn)
|
||||
)
|
||||
findings_by_region = {}
|
||||
for vulnerability_id, (region, finding_arn) in representatives.items():
|
||||
findings_by_region.setdefault(region, []).append(
|
||||
(finding_arn, vulnerability_id)
|
||||
)
|
||||
return [
|
||||
(region, findings[index : index + FINDING_DETAILS_BATCH_SIZE])
|
||||
for region, findings in findings_by_region.items()
|
||||
for index in range(0, len(findings), FINDING_DETAILS_BATCH_SIZE)
|
||||
]
|
||||
|
||||
def _batch_get_finding_details(self, batch):
|
||||
"""Record the CISA KEV data of the CVEs in a batch, flagging failed lookups."""
|
||||
region, findings = batch
|
||||
vulnerability_ids = dict(findings)
|
||||
logger.info("Inspector2 - Getting finding details...")
|
||||
try:
|
||||
response = self.regional_clients[region].batch_get_finding_details(
|
||||
findingArns=list(vulnerability_ids)
|
||||
)
|
||||
for detail in response.get("findingDetails", []):
|
||||
vulnerability_id = vulnerability_ids.get(detail.get("findingArn"))
|
||||
cisa_data = detail.get("cisaData")
|
||||
if vulnerability_id and cisa_data:
|
||||
self.known_exploited_vulnerabilities[vulnerability_id] = (
|
||||
KnownExploitedVulnerability(
|
||||
id=vulnerability_id,
|
||||
date_added=cisa_data.get("dateAdded"),
|
||||
date_due=cisa_data.get("dateDue"),
|
||||
)
|
||||
)
|
||||
for detail_error in response.get("errors", []):
|
||||
# Inspector has no intelligence for the CVE, so it cannot be a KEV
|
||||
if detail_error.get("errorCode") == "FINDING_DETAILS_NOT_FOUND":
|
||||
continue
|
||||
vulnerability_id = vulnerability_ids.get(detail_error.get("findingArn"))
|
||||
if vulnerability_id:
|
||||
self.vulnerability_lookup_failed.add(vulnerability_id)
|
||||
logger.error(
|
||||
f"{region} -- {detail_error.get('errorCode')} getting finding details for {vulnerability_id}: {detail_error.get('errorMessage')}"
|
||||
)
|
||||
except Exception as error:
|
||||
self.vulnerability_lookup_failed.update(vulnerability_ids.values())
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class Finding(BaseModel):
|
||||
"""Active Inspector2 finding."""
|
||||
|
||||
arn: str
|
||||
type: str
|
||||
severity: str
|
||||
first_observed_at: Optional[datetime]
|
||||
vulnerability_id: Optional[str]
|
||||
resource_ids: list[str] = []
|
||||
|
||||
|
||||
class CoveredResource(BaseModel):
|
||||
"""Resource tracked by Inspector2 coverage."""
|
||||
|
||||
id: str
|
||||
arn: str
|
||||
region: str
|
||||
resource_type: str
|
||||
scan_type: str
|
||||
scan_status_code: str
|
||||
scan_status_reason: str
|
||||
last_scanned_at: Optional[datetime]
|
||||
|
||||
|
||||
class KnownExploitedVulnerability(BaseModel):
|
||||
"""CISA Known Exploited Vulnerability data of a CVE."""
|
||||
|
||||
id: str
|
||||
date_added: Optional[datetime]
|
||||
date_due: Optional[datetime]
|
||||
|
||||
|
||||
class Inspector(BaseModel):
|
||||
id: str
|
||||
@@ -70,3 +269,5 @@ class Inspector(BaseModel):
|
||||
lambda_status: str
|
||||
lambda_code_status: str
|
||||
active_findings: bool = None
|
||||
findings: Optional[list[Finding]] = None
|
||||
coverage: Optional[list[CoveredResource]] = None
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
MAX_LISTED_VULNERABILITIES = 10
|
||||
|
||||
|
||||
def summarize_vulnerabilities(vulnerabilities: list[str]) -> str:
|
||||
"""Join vulnerability identifiers, truncating long lists."""
|
||||
listed = ", ".join(vulnerabilities[:MAX_LISTED_VULNERABILITIES])
|
||||
remaining = len(vulnerabilities) - MAX_LISTED_VULNERABILITIES
|
||||
return f"{listed} and {remaining} more" if remaining > 0 else listed
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "transfer_server_fips_security_policy_enabled",
|
||||
"CheckTitle": "AWS Transfer Family server uses a FIPS security policy",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "transfer",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "low",
|
||||
"ResourceType": "AwsTransferServer",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**AWS Transfer Family servers** (SFTP, FTPS, AS2) are assessed for use of a **FIPS** security policy (`TransferSecurityPolicy-FIPS-*`, flagged `Fips: true` by AWS), which limits file-transfer sessions to the FIPS-enabled set of SSH and TLS algorithms.",
|
||||
"Risk": "Servers without a FIPS security policy can negotiate algorithms outside the FIPS-enabled set, which does not meet requirements to protect federal or regulated files and credentials with **NIST CMVP validated cryptography**.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/transfer/latest/userguide/security-policies.html",
|
||||
"https://aws.amazon.com/compliance/fips/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws transfer update-server --server-id <server_id> --security-policy-name TransferSecurityPolicy-FIPS-2025-03",
|
||||
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::Transfer::Server\n Properties:\n Protocols:\n - SFTP\n SecurityPolicyName: TransferSecurityPolicy-FIPS-2025-03 # FIX: FIPS security policy\n```",
|
||||
"Other": "1. In the AWS Console, go to AWS Transfer Family > Servers\n2. Select the server and choose Edit on the Additional details panel\n3. Set Cryptographic algorithm options (Security policy) to a FIPS policy such as TransferSecurityPolicy-FIPS-2025-03\n4. Save the changes",
|
||||
"Terraform": "```hcl\nresource \"aws_transfer_server\" \"<example_resource_name>\" {\n protocols = [\"SFTP\"]\n security_policy_name = \"TransferSecurityPolicy-FIPS-2025-03\" # FIX: FIPS security policy\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Use a **FIPS** security policy, such as `TransferSecurityPolicy-FIPS-2025-03`, on every Transfer Family server that exchanges federal or regulated data.",
|
||||
"Url": "https://hub.prowler.com/check/transfer_server_fips_security_policy_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"encryption"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"transfer_server_in_transit_encryption_enabled",
|
||||
"transfer_server_pqc_ssh_kex_enabled"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.transfer.transfer_client import transfer_client
|
||||
|
||||
|
||||
class transfer_server_fips_security_policy_enabled(Check):
|
||||
"""Ensure every AWS Transfer Family server uses a FIPS security policy."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
"""Report whether each Transfer Family server uses a FIPS security policy."""
|
||||
findings = []
|
||||
unretrieved_servers = []
|
||||
for server in transfer_client.servers.values():
|
||||
policy = server.security_policy_name
|
||||
if not policy:
|
||||
unretrieved_servers.append(server.id)
|
||||
continue
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=server)
|
||||
if "FIPS" in policy.split("-"):
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Transfer Server {server.id} uses FIPS security policy {policy}."
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Transfer Server {server.id} uses security policy {policy}, which is not a FIPS security policy."
|
||||
findings.append(report)
|
||||
if unretrieved_servers:
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource={})
|
||||
report.resource_id = transfer_client.audited_account
|
||||
report.resource_arn = transfer_client.audited_account_arn
|
||||
report.region = transfer_client.region
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
"Transfer Server security policies could not be retrieved for "
|
||||
f"{', '.join(unretrieved_servers)}; verify the transfer:DescribeServer permission."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -27,6 +27,7 @@ class AzureService:
|
||||
)
|
||||
|
||||
self.subscriptions = provider.identity.subscriptions
|
||||
self.region_config = provider.region_config
|
||||
self.resource_groups = provider.resource_groups
|
||||
self.locations = provider.locations
|
||||
self.audit_config = provider.audit_config
|
||||
|
||||
@@ -18,8 +18,8 @@ class AzureIdentityInfo(BaseModel):
|
||||
class AzureRegionConfig(BaseModel):
|
||||
name: str = ""
|
||||
authority: Optional[str] = None
|
||||
base_url: str = ""
|
||||
credential_scopes: list = []
|
||||
base_url: str = "https://management.azure.com"
|
||||
credential_scopes: list = ["https://management.azure.com/.default"]
|
||||
graph_host: str = "https://graph.microsoft.com"
|
||||
graph_scope: str = "https://graph.microsoft.com/.default"
|
||||
logs_endpoint: str = "https://api.loganalytics.io"
|
||||
|
||||
@@ -12,7 +12,16 @@ from prowler.providers.azure.lib.service.service import AzureService
|
||||
|
||||
|
||||
class Defender(AzureService):
|
||||
"""Microsoft Defender for Cloud service: pricings, settings, assessments,
|
||||
security contacts, IoT solutions and JIT policies per subscription."""
|
||||
|
||||
def __init__(self, provider: AzureProvider):
|
||||
"""Collect the Defender configuration of every audited subscription.
|
||||
|
||||
Args:
|
||||
provider: Azure provider supplying the session, subscriptions and
|
||||
the region config whose endpoints are used for every call.
|
||||
"""
|
||||
super().__init__(SecurityCenter, provider)
|
||||
|
||||
self.pricings = self._get_pricings()
|
||||
@@ -21,7 +30,7 @@ class Defender(AzureService):
|
||||
self.settings = self._get_settings()
|
||||
self.security_contact_configurations = self._get_security_contacts(
|
||||
token=provider.session.get_token(
|
||||
"https://management.azure.com/.default"
|
||||
*self.region_config.credential_scopes
|
||||
).token
|
||||
)
|
||||
self.iot_security_solutions = self._get_iot_security_solutions()
|
||||
@@ -168,7 +177,7 @@ class Defender(AzureService):
|
||||
security_contacts = {}
|
||||
for subscription_id, display_name in self.subscriptions.items():
|
||||
try:
|
||||
url = f"https://management.azure.com/subscriptions/{subscription_id}/providers/Microsoft.Security/securityContacts?api-version=2023-12-01-preview"
|
||||
url = f"{self.region_config.base_url}/subscriptions/{subscription_id}/providers/Microsoft.Security/securityContacts?api-version=2023-12-01-preview"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -83,6 +83,7 @@ class KeyVault(AzureService):
|
||||
subscription,
|
||||
resource_group,
|
||||
keyvault_name,
|
||||
getattr(keyvault_properties, "vault_uri", ""),
|
||||
provider,
|
||||
)
|
||||
secrets_future = executor.submit(
|
||||
@@ -150,7 +151,22 @@ class KeyVault(AzureService):
|
||||
)
|
||||
return None
|
||||
|
||||
def _get_keys(self, subscription, resource_group, keyvault_name, provider):
|
||||
def _get_keys(
|
||||
self, subscription, resource_group, keyvault_name, vault_uri, provider
|
||||
):
|
||||
"""Get the keys of a Key Vault, enriched with their rotation policies.
|
||||
|
||||
Args:
|
||||
subscription: Subscription ID the vault belongs to.
|
||||
resource_group: Resource group name of the vault.
|
||||
keyvault_name: Vault name, used for the management API and logs.
|
||||
vault_uri: Data-plane URI of the vault as returned by ARM, valid in
|
||||
any Azure cloud. When empty, rotation policies are skipped.
|
||||
provider: Azure provider whose session authenticates the KeyClient.
|
||||
|
||||
Returns:
|
||||
A list of Key objects; rotation_policy is set when it could be read.
|
||||
"""
|
||||
logger.info(f"KeyVault - Getting keys for {keyvault_name}...")
|
||||
keys = []
|
||||
keys_dict = {}
|
||||
@@ -179,10 +195,15 @@ class KeyVault(AzureService):
|
||||
f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
if not vault_uri:
|
||||
logger.warning(
|
||||
f"KeyVault {keyvault_name} in {subscription} -- has no vault URI, skipping key rotation policies"
|
||||
)
|
||||
return keys
|
||||
|
||||
try:
|
||||
key_client = KeyClient(
|
||||
vault_url=f"https://{keyvault_name}.vault.azure.net/",
|
||||
# TODO: review the following line
|
||||
vault_url=vault_uri,
|
||||
credential=provider.session,
|
||||
)
|
||||
properties = list(key_client.list_properties_of_keys())
|
||||
|
||||
+1
-16
@@ -8,7 +8,6 @@ from prowler.providers.googleworkspace.services.security.lib.durations import (
|
||||
parse_duration_seconds,
|
||||
)
|
||||
from prowler.providers.googleworkspace.services.security.lib.scope import (
|
||||
failures_shadowed_by_overrides,
|
||||
override_caveat,
|
||||
unevaluable_reason,
|
||||
)
|
||||
@@ -145,23 +144,9 @@ class security_2sv_enforced(Check):
|
||||
)
|
||||
)
|
||||
|
||||
failing_settings = frozenset(setting for setting, _ in issues)
|
||||
reasons = "; ".join(text for _, text in issues)
|
||||
|
||||
if issues and failures_shadowed_by_overrides(policies, failing_settings):
|
||||
# The audited scope (e.g. the admin group of 4.1.1.1) may get
|
||||
# the overriding value, which the Policy API does not expose,
|
||||
# so the domain-wide failure cannot be confirmed for it.
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"2-Step Verification is not enforced as required in the "
|
||||
f"domain-wide policy of {domain}: {reasons}. However, every "
|
||||
f"failing setting is also overridden for at least one group "
|
||||
f"or organizational unit, so the audited scope may be "
|
||||
f"configured correctly. Review those overrides in the Admin "
|
||||
f"console."
|
||||
)
|
||||
elif issues:
|
||||
if issues:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"2-Step Verification is not enforced as required in domain "
|
||||
|
||||
@@ -384,6 +384,19 @@ class HuaweiCloudSession:
|
||||
.build()
|
||||
)
|
||||
|
||||
elif service == "smn":
|
||||
from huaweicloudsdksmn.v2 import SmnClient
|
||||
from huaweicloudsdksmn.v2.region.smn_region import SmnRegion
|
||||
|
||||
client_region = region or self._region
|
||||
return (
|
||||
SmnClient.new_builder()
|
||||
.with_credentials(self._get_basic_credentials(client_region))
|
||||
.with_http_config(self._http_config())
|
||||
.with_region(_aligned_region(SmnRegion, client_region))
|
||||
.build()
|
||||
)
|
||||
|
||||
else:
|
||||
raise HuaweiCloudServiceError(
|
||||
message=f"Huawei Cloud service '{service}' is not supported"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from prowler.providers.huaweicloud.services.smn.smn_service import SMN
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
smn_client = SMN(Provider.get_global_provider())
|
||||
@@ -0,0 +1,116 @@
|
||||
from typing import List
|
||||
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.huaweicloud.lib.service.service import HuaweiCloudService
|
||||
|
||||
SMN_PAGE_SIZE = 100
|
||||
|
||||
|
||||
class SMN(HuaweiCloudService):
|
||||
"""
|
||||
SMN (Simple Message Notification) service class for Huawei Cloud.
|
||||
|
||||
This class provides methods to interact with Huawei Cloud SMN service
|
||||
to retrieve notification topics and their subscription counts.
|
||||
"""
|
||||
|
||||
def __init__(self, provider):
|
||||
super().__init__(__class__.__name__, provider)
|
||||
|
||||
self.topics: List[SMNTopic] = []
|
||||
self._list_topics()
|
||||
|
||||
def _list_topics(self):
|
||||
"""List all SMN topics across regions and get their subscription counts."""
|
||||
if not self.regional_clients:
|
||||
return
|
||||
|
||||
for region, client in self.regional_clients.items():
|
||||
logger.info(f"SMN - Listing Topics in {region}...")
|
||||
|
||||
try:
|
||||
from huaweicloudsdksmn.v2 import (
|
||||
ListSubscriptionsByTopicRequest,
|
||||
ListTopicsRequest,
|
||||
)
|
||||
|
||||
offset = 0
|
||||
while True:
|
||||
request = ListTopicsRequest(offset=offset, limit=SMN_PAGE_SIZE)
|
||||
response = self._call_with_retries(client.list_topics, request)
|
||||
topics = getattr(response, "topics", None) or []
|
||||
|
||||
for topic in topics:
|
||||
topic_urn = getattr(topic, "topic_urn", "") or ""
|
||||
topic_id = getattr(topic, "topic_id", "") or ""
|
||||
name = getattr(topic, "name", "") or ""
|
||||
display_name = getattr(topic, "display_name", "") or ""
|
||||
push_policy = getattr(topic, "push_policy", None)
|
||||
|
||||
try:
|
||||
confirmed_subscription_count = 0
|
||||
subscription_offset = 0
|
||||
while True:
|
||||
sub_request = ListSubscriptionsByTopicRequest(
|
||||
topic_urn=topic_urn,
|
||||
offset=subscription_offset,
|
||||
limit=SMN_PAGE_SIZE,
|
||||
)
|
||||
sub_response = self._call_with_retries(
|
||||
client.list_subscriptions_by_topic, sub_request
|
||||
)
|
||||
subscriptions = (
|
||||
getattr(sub_response, "subscriptions", None) or []
|
||||
)
|
||||
confirmed_subscription_count += sum(
|
||||
getattr(subscription, "status", None) == 1
|
||||
for subscription in subscriptions
|
||||
)
|
||||
subscription_count = (
|
||||
getattr(sub_response, "subscription_count", 0) or 0
|
||||
)
|
||||
subscription_offset += SMN_PAGE_SIZE
|
||||
if subscription_offset >= subscription_count:
|
||||
break
|
||||
except Exception as sub_error:
|
||||
logger.error(
|
||||
f"{region} -- {sub_error.__class__.__name__}"
|
||||
f"[{sub_error.__traceback__.tb_lineno}]: {sub_error}"
|
||||
)
|
||||
continue
|
||||
|
||||
self.topics.append(
|
||||
SMNTopic(
|
||||
topic_urn=topic_urn,
|
||||
topic_id=topic_id,
|
||||
name=name,
|
||||
display_name=display_name,
|
||||
push_policy=push_policy,
|
||||
confirmed_subscription_count=confirmed_subscription_count,
|
||||
region=region,
|
||||
)
|
||||
)
|
||||
|
||||
offset += SMN_PAGE_SIZE
|
||||
topic_count = getattr(response, "topic_count", 0) or 0
|
||||
if offset >= topic_count:
|
||||
break
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class SMNTopic(BaseModel):
|
||||
"""SMN topic model."""
|
||||
|
||||
topic_urn: str
|
||||
topic_id: str = ""
|
||||
name: str = ""
|
||||
display_name: str = ""
|
||||
push_policy: int = None
|
||||
confirmed_subscription_count: int = 0
|
||||
region: str = ""
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"Provider": "huaweicloud",
|
||||
"CheckID": "smn_topic_subscriptions",
|
||||
"CheckTitle": "SMN topics have at least one confirmed subscription",
|
||||
"CheckType": [],
|
||||
"ServiceName": "smn",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "low",
|
||||
"ResourceType": "HUAWEICLOUD::SMN::Topic",
|
||||
"ResourceGroup": "messaging",
|
||||
"Description": "Ensure that SMN notification topics have at least one confirmed subscription so alerts can be delivered to recipients.",
|
||||
"Risk": "Topics without confirmed subscriptions cannot deliver notifications, meaning critical alerts may go unnoticed by operations and security teams.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://support.huaweicloud.com/intl/en-us/api-smn/ListSubscriptionsByTopic.html",
|
||||
"https://support.huaweicloud.com/intl/en-us/api-smn/AddSubscription.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "hcloud SMN AddSubscription --topic_urn=<topic_urn> --protocol=<protocol> --endpoint=<endpoint>",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Log on to the Huawei Cloud console.\n2. Navigate to Simple Message Notification (SMN).\n3. Select the topic without subscriptions.\n4. Click Add Subscription.\n5. Configure the subscription protocol and endpoint.\n6. Confirm the subscription.",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Add and confirm at least one subscription for each SMN topic so notifications can be delivered.",
|
||||
"Url": "https://hub.prowler.com/check/smn_topic_subscriptions"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "Only confirmed subscriptions (status 1) satisfy this check. Unconfirmed or canceled subscriptions cannot provide notification coverage."
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
from prowler.lib.check.models import Check, CheckReportHuaweiCloud
|
||||
from prowler.providers.huaweicloud.services.smn.smn_client import smn_client
|
||||
|
||||
|
||||
class smn_topic_subscriptions(Check):
|
||||
"""Check if SMN topics have at least one subscription configured."""
|
||||
|
||||
def execute(self) -> list[CheckReportHuaweiCloud]:
|
||||
findings = []
|
||||
|
||||
for topic in smn_client.topics:
|
||||
report = CheckReportHuaweiCloud(
|
||||
metadata=self.metadata(),
|
||||
resource=topic,
|
||||
)
|
||||
report.region = topic.region
|
||||
report.resource_id = topic.topic_id
|
||||
report.resource_name = topic.name
|
||||
report.resource_arn = topic.topic_urn
|
||||
|
||||
if topic.confirmed_subscription_count > 0:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"SMN topic '{topic.name}' ({topic.topic_id}) has "
|
||||
f"{topic.confirmed_subscription_count} confirmed subscription(s)."
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"SMN topic '{topic.name}' ({topic.topic_id}) has no confirmed "
|
||||
"subscriptions. Notifications will not be delivered."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+6
@@ -55,6 +55,12 @@ class defender_antiphishing_policy_configured(Check):
|
||||
policy_name,
|
||||
policy,
|
||||
) in defender_client.antiphishing_policies.items():
|
||||
# Preset security policies are scoped by protection policy rules, not filter rules
|
||||
if (
|
||||
not policy.default
|
||||
and policy.name not in defender_client.antiphishing_rules
|
||||
):
|
||||
continue
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
|
||||
+6
@@ -51,6 +51,12 @@ class defender_antispam_policy_inbound_no_allowed_domains(Check):
|
||||
default_policy_well_configured = False
|
||||
|
||||
for policy in defender_client.inbound_spam_policies:
|
||||
# Preset security policies are scoped by protection policy rules, not filter rules
|
||||
if (
|
||||
not policy.default
|
||||
and policy.identity not in defender_client.inbound_spam_rules
|
||||
):
|
||||
continue
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
|
||||
+6
@@ -51,6 +51,12 @@ class defender_malware_policy_common_attachments_filter_enabled(Check):
|
||||
default_policy_well_configured = False
|
||||
|
||||
for policy in defender_client.malware_policies:
|
||||
# Preset security policies are scoped by protection policy rules, not filter rules
|
||||
if (
|
||||
not policy.is_default
|
||||
and policy.identity not in defender_client.malware_rules
|
||||
):
|
||||
continue
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
|
||||
+6
@@ -114,6 +114,12 @@ class defender_malware_policy_comprehensive_attachments_filter_applied(Check):
|
||||
default_policy_well_configured = False
|
||||
|
||||
for policy in defender_client.malware_policies:
|
||||
# Preset security policies are scoped by protection policy rules, not filter rules
|
||||
if (
|
||||
not policy.is_default
|
||||
and policy.identity not in defender_client.malware_rules
|
||||
):
|
||||
continue
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
|
||||
+6
@@ -51,6 +51,12 @@ class defender_malware_policy_notifications_internal_users_malware_enabled(Check
|
||||
default_policy_well_configured = False
|
||||
|
||||
for policy in defender_client.malware_policies:
|
||||
# Preset security policies are scoped by protection policy rules, not filter rules
|
||||
if (
|
||||
not policy.is_default
|
||||
and policy.identity not in defender_client.malware_rules
|
||||
):
|
||||
continue
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=policy,
|
||||
|
||||
@@ -133,6 +133,7 @@ dependencies = [
|
||||
"huaweicloudsdkkms==3.1.204",
|
||||
"huaweicloudsdkobs==3.1.204",
|
||||
"huaweicloudsdkrds==3.1.204",
|
||||
"huaweicloudsdksmn==3.1.204",
|
||||
"huaweicloudsdkvpc==3.1.204",
|
||||
"huaweicloudsdkwaf==3.1.204",
|
||||
"zstandard==0.25.0"
|
||||
@@ -271,6 +272,7 @@ constraint-dependencies = [
|
||||
"huaweicloudsdkkms==3.1.204",
|
||||
"huaweicloudsdkobs==3.1.204",
|
||||
"huaweicloudsdkrds==3.1.204",
|
||||
"huaweicloudsdksmn==3.1.204",
|
||||
"huaweicloudsdkvpc==3.1.204",
|
||||
"huaweicloudsdkwaf==3.1.204",
|
||||
"hyperframe==6.1.0",
|
||||
|
||||
@@ -138,6 +138,8 @@ config_aws = {
|
||||
"organizations_enabled_regions": [],
|
||||
"organizations_trusted_delegated_administrators": [],
|
||||
"ecr_repository_vulnerability_minimum_severity": "MEDIUM",
|
||||
"inspector2_max_days_since_last_scan": 3,
|
||||
"inspector2_active_finding_max_age_days": 192,
|
||||
"verify_premium_support_plans": True,
|
||||
"threat_detection_privilege_escalation_threshold": 0.2,
|
||||
"threat_detection_privilege_escalation_minutes": 1440,
|
||||
|
||||
@@ -139,6 +139,14 @@ aws:
|
||||
# MEDIUM
|
||||
ecr_repository_vulnerability_minimum_severity: "MEDIUM"
|
||||
|
||||
# AWS Inspector2
|
||||
# aws.inspector2_coverage_recently_scanned
|
||||
# Maximum days since Inspector2 last scanned an actively covered resource
|
||||
inspector2_max_days_since_last_scan: 3
|
||||
# aws.inspector2_active_findings_within_max_age
|
||||
# Maximum days an Inspector2 finding can stay active since it was first observed
|
||||
inspector2_active_finding_max_age_days: 192
|
||||
|
||||
# AWS Trusted Advisor
|
||||
# aws.trustedadvisor_premium_support_plan_subscribed
|
||||
verify_premium_support_plans: True
|
||||
|
||||
@@ -17,10 +17,12 @@ from pytest import raises
|
||||
from tzlocal import get_localzone
|
||||
|
||||
from prowler.providers.aws.aws_provider import (
|
||||
MAX_STS_BOOTSTRAP_ATTEMPTS,
|
||||
AwsProvider,
|
||||
get_aws_region_for_sts,
|
||||
get_env_partition_bootstrap_region,
|
||||
get_env_partition_regions,
|
||||
get_partition_bootstrap_candidates,
|
||||
)
|
||||
from prowler.providers.aws.config import (
|
||||
AWS_STS_GLOBAL_ENDPOINT_REGION,
|
||||
@@ -32,6 +34,7 @@ from prowler.providers.aws.config import (
|
||||
get_default_session_config,
|
||||
)
|
||||
from prowler.providers.aws.exceptions.exceptions import (
|
||||
AWSAccessKeyIDInvalidError,
|
||||
AWSArgumentTypeValidationError,
|
||||
AWSIAMRoleARNInvalidResourceTypeError,
|
||||
AWSInvalidBoto3TimeoutError,
|
||||
@@ -1583,6 +1586,411 @@ aws:
|
||||
assert get_caller_identity.arn.resource == "test-user"
|
||||
assert get_caller_identity.arn.resource_type == "user"
|
||||
|
||||
def test_get_partition_bootstrap_candidates_adds_the_rest_of_the_partition(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION)
|
||||
|
||||
assert get_partition_bootstrap_candidates(
|
||||
AWS_REGION_GOV_CLOUD_US_EAST_1, AWS_REGION_US_EAST_1
|
||||
) == [AWS_REGION_GOV_CLOUD_US_EAST_1, AWS_REGION_GOV_CLOUD_US_WEST_1]
|
||||
|
||||
def test_get_partition_bootstrap_candidates_without_partition_offers_one_region(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.delenv("PROWLER_AWS_PARTITION", raising=False)
|
||||
|
||||
assert get_partition_bootstrap_candidates(AWS_REGION_EU_WEST_1) == [
|
||||
AWS_REGION_EU_WEST_1
|
||||
]
|
||||
|
||||
def test_get_partition_bootstrap_candidates_is_capped(self, monkeypatch):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_COMMERCIAL_PARTITION)
|
||||
|
||||
candidates = get_partition_bootstrap_candidates(
|
||||
AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_1
|
||||
)
|
||||
|
||||
assert len(candidates) == MAX_STS_BOOTSTRAP_ATTEMPTS
|
||||
assert candidates[0] == AWS_REGION_EU_WEST_1
|
||||
|
||||
def test_get_partition_bootstrap_candidates_tries_excluded_regions_last(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_COMMERCIAL_PARTITION)
|
||||
partition_regions = get_env_partition_regions(AWS_REGION_EU_WEST_1)
|
||||
excluded_regions = set(partition_regions[1:3])
|
||||
|
||||
candidates = get_partition_bootstrap_candidates(
|
||||
AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_1, excluded_regions
|
||||
)
|
||||
|
||||
assert candidates == [AWS_REGION_EU_WEST_1, *partition_regions[3:5]]
|
||||
|
||||
def test_get_partition_bootstrap_candidates_keeps_excluded_regions_as_a_last_resort(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION)
|
||||
|
||||
assert get_partition_bootstrap_candidates(
|
||||
AWS_REGION_GOV_CLOUD_US_EAST_1,
|
||||
AWS_REGION_US_EAST_1,
|
||||
{AWS_REGION_GOV_CLOUD_US_EAST_1, AWS_REGION_GOV_CLOUD_US_WEST_1},
|
||||
) == [AWS_REGION_GOV_CLOUD_US_EAST_1, AWS_REGION_GOV_CLOUD_US_WEST_1]
|
||||
|
||||
def test_validate_credentials_falls_back_to_the_next_partition_region(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION)
|
||||
# A container may carry a region that belongs to no partition it scans
|
||||
current_session = session.Session(region_name=AWS_REGION_US_EAST_1)
|
||||
attempted_regions = []
|
||||
|
||||
def create_sts_session(session, aws_region):
|
||||
attempted_regions.append(aws_region)
|
||||
if aws_region == AWS_REGION_GOV_CLOUD_US_EAST_1:
|
||||
raise botocore.exceptions.EndpointConnectionError(
|
||||
endpoint_url=f"https://sts.{aws_region}.amazonaws.com"
|
||||
)
|
||||
sts_client = mock.MagicMock()
|
||||
sts_client.get_caller_identity.return_value = {
|
||||
"UserId": "test-user-id",
|
||||
"Account": AWS_ACCOUNT_NUMBER,
|
||||
"Arn": AWS_GOV_CLOUD_ACCOUNT_ARN,
|
||||
}
|
||||
return sts_client
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.create_sts_session",
|
||||
new=create_sts_session,
|
||||
):
|
||||
caller_identity = AwsProvider.validate_credentials(
|
||||
session=current_session, aws_region=AWS_REGION_GOV_CLOUD_US_EAST_1
|
||||
)
|
||||
|
||||
assert attempted_regions == [
|
||||
AWS_REGION_GOV_CLOUD_US_EAST_1,
|
||||
AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
]
|
||||
assert caller_identity.region == AWS_REGION_GOV_CLOUD_US_WEST_1
|
||||
|
||||
def test_validate_credentials_falls_back_when_a_region_does_not_answer(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION)
|
||||
current_session = session.Session(region_name=AWS_REGION_US_EAST_1)
|
||||
attempted_regions = []
|
||||
|
||||
# The connection is accepted but nothing comes back before the read timeout
|
||||
def create_sts_session(session, aws_region):
|
||||
attempted_regions.append(aws_region)
|
||||
if aws_region == AWS_REGION_GOV_CLOUD_US_EAST_1:
|
||||
raise botocore.exceptions.ReadTimeoutError(
|
||||
endpoint_url=f"https://sts.{aws_region}.amazonaws.com"
|
||||
)
|
||||
sts_client = mock.MagicMock()
|
||||
sts_client.get_caller_identity.return_value = {
|
||||
"UserId": "test-user-id",
|
||||
"Account": AWS_ACCOUNT_NUMBER,
|
||||
"Arn": AWS_GOV_CLOUD_ACCOUNT_ARN,
|
||||
}
|
||||
return sts_client
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.create_sts_session",
|
||||
new=create_sts_session,
|
||||
):
|
||||
caller_identity = AwsProvider.validate_credentials(
|
||||
session=current_session, aws_region=AWS_REGION_GOV_CLOUD_US_EAST_1
|
||||
)
|
||||
|
||||
assert attempted_regions == [
|
||||
AWS_REGION_GOV_CLOUD_US_EAST_1,
|
||||
AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
]
|
||||
assert caller_identity.region == AWS_REGION_GOV_CLOUD_US_WEST_1
|
||||
|
||||
def test_validate_credentials_raises_when_no_partition_region_answers(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION)
|
||||
current_session = session.Session(region_name=AWS_REGION_US_EAST_1)
|
||||
attempted_regions = []
|
||||
|
||||
def create_sts_session(session, aws_region):
|
||||
attempted_regions.append(aws_region)
|
||||
raise botocore.exceptions.EndpointConnectionError(
|
||||
endpoint_url=f"https://sts.{aws_region}.amazonaws.com"
|
||||
)
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.create_sts_session",
|
||||
new=create_sts_session,
|
||||
):
|
||||
with raises(botocore.exceptions.EndpointConnectionError):
|
||||
AwsProvider.validate_credentials(
|
||||
session=current_session, aws_region=AWS_REGION_GOV_CLOUD_US_EAST_1
|
||||
)
|
||||
|
||||
assert attempted_regions == [
|
||||
AWS_REGION_GOV_CLOUD_US_EAST_1,
|
||||
AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
]
|
||||
|
||||
def test_validate_credentials_avoids_an_excluded_region_when_failing_over(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_COMMERCIAL_PARTITION)
|
||||
current_session = session.Session(region_name=AWS_REGION_EU_WEST_1)
|
||||
partition_regions = get_env_partition_regions(AWS_REGION_EU_WEST_1)
|
||||
excluded_region, answering_region = partition_regions[1:3]
|
||||
attempted_regions = []
|
||||
|
||||
def create_sts_session(session, aws_region):
|
||||
attempted_regions.append(aws_region)
|
||||
if aws_region == AWS_REGION_EU_WEST_1:
|
||||
raise botocore.exceptions.EndpointConnectionError(
|
||||
endpoint_url=f"https://sts.{aws_region}.amazonaws.com"
|
||||
)
|
||||
sts_client = mock.MagicMock()
|
||||
sts_client.get_caller_identity.return_value = {
|
||||
"UserId": "test-user-id",
|
||||
"Account": AWS_ACCOUNT_NUMBER,
|
||||
"Arn": AWS_ACCOUNT_ARN,
|
||||
}
|
||||
return sts_client
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.create_sts_session",
|
||||
new=create_sts_session,
|
||||
):
|
||||
caller_identity = AwsProvider.validate_credentials(
|
||||
session=current_session,
|
||||
aws_region=AWS_REGION_EU_WEST_1,
|
||||
excluded_regions={excluded_region},
|
||||
)
|
||||
|
||||
assert attempted_regions == [AWS_REGION_EU_WEST_1, answering_region]
|
||||
assert caller_identity.region == answering_region
|
||||
|
||||
def test_validate_credentials_does_not_retry_a_credential_error(self, monkeypatch):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION)
|
||||
current_session = session.Session(region_name=AWS_REGION_US_EAST_1)
|
||||
attempted_regions = []
|
||||
|
||||
def create_sts_session(session, aws_region):
|
||||
attempted_regions.append(aws_region)
|
||||
sts_client = mock.MagicMock()
|
||||
sts_client.get_caller_identity.side_effect = (
|
||||
botocore.exceptions.ClientError(
|
||||
{"Error": {"Code": "InvalidClientTokenId", "Message": "invalid"}},
|
||||
"GetCallerIdentity",
|
||||
)
|
||||
)
|
||||
return sts_client
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.create_sts_session",
|
||||
new=create_sts_session,
|
||||
):
|
||||
with raises(AWSAccessKeyIDInvalidError):
|
||||
AwsProvider.validate_credentials(
|
||||
session=current_session, aws_region=AWS_REGION_GOV_CLOUD_US_EAST_1
|
||||
)
|
||||
|
||||
assert attempted_regions == [AWS_REGION_GOV_CLOUD_US_EAST_1]
|
||||
|
||||
def test_assume_role_falls_back_to_the_next_partition_region(self, monkeypatch):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION)
|
||||
current_session = session.Session(region_name=AWS_REGION_US_EAST_1)
|
||||
attempted_regions = []
|
||||
|
||||
def create_sts_session(session, aws_region):
|
||||
attempted_regions.append(aws_region)
|
||||
if aws_region == AWS_REGION_GOV_CLOUD_US_EAST_1:
|
||||
raise botocore.exceptions.EndpointConnectionError(
|
||||
endpoint_url=f"https://sts.{aws_region}.amazonaws.com"
|
||||
)
|
||||
sts_client = mock.MagicMock()
|
||||
sts_client.assume_role.return_value = {
|
||||
"Credentials": {
|
||||
"AccessKeyId": "AKIAIOSFODNN7EXAMPLE",
|
||||
"SecretAccessKey": "secret",
|
||||
"SessionToken": "token",
|
||||
"Expiration": datetime.now() + timedelta(seconds=3600),
|
||||
}
|
||||
}
|
||||
return sts_client
|
||||
|
||||
assumed_role_info = AWSAssumeRoleInfo(
|
||||
role_arn=ARN(
|
||||
arn=f"arn:{AWS_GOV_CLOUD_PARTITION}:iam::{AWS_ACCOUNT_NUMBER}:role/test-role"
|
||||
),
|
||||
session_duration=3600,
|
||||
external_id=None,
|
||||
mfa_enabled=False,
|
||||
role_session_name=ROLE_SESSION_NAME,
|
||||
sts_region=AWS_REGION_GOV_CLOUD_US_EAST_1,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.create_sts_session",
|
||||
new=create_sts_session,
|
||||
):
|
||||
credentials = AwsProvider.assume_role(current_session, assumed_role_info)
|
||||
|
||||
assert attempted_regions == [
|
||||
AWS_REGION_GOV_CLOUD_US_EAST_1,
|
||||
AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
]
|
||||
assert isinstance(credentials, AWSCredentials)
|
||||
assert credentials.aws_access_key_id == "AKIAIOSFODNN7EXAMPLE"
|
||||
|
||||
def test_setup_session_mfa_falls_back_to_the_next_partition_region(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION)
|
||||
monkeypatch.setenv("AWS_DEFAULT_REGION", AWS_REGION_US_EAST_1)
|
||||
attempted_regions = []
|
||||
|
||||
def create_sts_session(session, aws_region):
|
||||
attempted_regions.append(aws_region)
|
||||
if aws_region == AWS_REGION_GOV_CLOUD_US_EAST_1:
|
||||
raise botocore.exceptions.EndpointConnectionError(
|
||||
endpoint_url=f"https://sts.{aws_region}.amazonaws.com"
|
||||
)
|
||||
sts_client = mock.MagicMock()
|
||||
sts_client.get_session_token.return_value = {
|
||||
"Credentials": {
|
||||
"AccessKeyId": "AKIAIOSFODNN7EXAMPLE",
|
||||
"SecretAccessKey": "secret",
|
||||
"SessionToken": "token",
|
||||
}
|
||||
}
|
||||
return sts_client
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.input_role_mfa_token_and_code",
|
||||
return_value=AWSMFAInfo(
|
||||
arn=f"arn:{AWS_GOV_CLOUD_PARTITION}:iam::{AWS_ACCOUNT_NUMBER}:mfa/test",
|
||||
totp="123456",
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.create_sts_session",
|
||||
new=create_sts_session,
|
||||
),
|
||||
):
|
||||
mfa_session = AwsProvider.setup_session(
|
||||
mfa=True,
|
||||
aws_access_key_id="test-access-key",
|
||||
aws_secret_access_key="test-secret-key",
|
||||
)
|
||||
|
||||
assert attempted_regions == [
|
||||
AWS_REGION_GOV_CLOUD_US_EAST_1,
|
||||
AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
]
|
||||
assert mfa_session.get_credentials().access_key == "AKIAIOSFODNN7EXAMPLE"
|
||||
assert mfa_session.get_credentials().token == "token"
|
||||
|
||||
@mock_aws
|
||||
def test_aws_provider_hands_excluded_regions_to_credential_validation(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION)
|
||||
monkeypatch.setenv("AWS_DEFAULT_REGION", AWS_REGION_US_EAST_1)
|
||||
handed = []
|
||||
|
||||
class Validated(Exception):
|
||||
pass
|
||||
|
||||
# Stops at the validation: what it was handed is all this checks
|
||||
def validate_credentials(session, aws_region, excluded_regions=None):
|
||||
handed.append((aws_region, set(excluded_regions or ())))
|
||||
raise Validated
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.validate_credentials",
|
||||
side_effect=validate_credentials,
|
||||
):
|
||||
with raises(Validated):
|
||||
AwsProvider(excluded_regions={AWS_REGION_GOV_CLOUD_US_EAST_1})
|
||||
|
||||
assert handed == [
|
||||
(AWS_REGION_GOV_CLOUD_US_WEST_1, {AWS_REGION_GOV_CLOUD_US_EAST_1})
|
||||
]
|
||||
|
||||
@mock_aws
|
||||
def test_aws_provider_assumes_the_role_where_validation_got_an_answer(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION)
|
||||
# Out of the partition, so the first candidate is botocore's, not this one
|
||||
monkeypatch.setenv("AWS_DEFAULT_REGION", AWS_REGION_US_EAST_1)
|
||||
role_arn = (
|
||||
f"arn:{AWS_GOV_CLOUD_PARTITION}:iam::{AWS_ACCOUNT_NUMBER}:role/test-role"
|
||||
)
|
||||
answered = AWSCallerIdentity(
|
||||
user_id="test-user-id",
|
||||
account=AWS_ACCOUNT_NUMBER,
|
||||
arn=ARN(AWS_GOV_CLOUD_ACCOUNT_ARN),
|
||||
region=AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.validate_credentials",
|
||||
return_value=answered,
|
||||
):
|
||||
aws_provider = AwsProvider(role_arn=role_arn, session_duration=900)
|
||||
|
||||
assert (
|
||||
aws_provider._assumed_role_configuration.info.sts_region
|
||||
== AWS_REGION_GOV_CLOUD_US_WEST_1
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_aws_provider_assumes_the_organizations_role_where_validation_got_an_answer(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION)
|
||||
monkeypatch.setenv("AWS_DEFAULT_REGION", AWS_REGION_US_EAST_1)
|
||||
organizations_role_arn = f"arn:{AWS_GOV_CLOUD_PARTITION}:iam::{AWS_ACCOUNT_NUMBER}:role/organizations-role"
|
||||
answered = AWSCallerIdentity(
|
||||
user_id="test-user-id",
|
||||
account=AWS_ACCOUNT_NUMBER,
|
||||
arn=ARN(AWS_GOV_CLOUD_ACCOUNT_ARN),
|
||||
region=AWS_REGION_GOV_CLOUD_US_WEST_1,
|
||||
)
|
||||
sts_regions = []
|
||||
|
||||
class RoleAssumed(Exception):
|
||||
pass
|
||||
|
||||
# Stops at the assumption: the region it was handed is all this checks
|
||||
def assume_role(session, assumed_role_info):
|
||||
sts_regions.append(assumed_role_info.sts_region)
|
||||
raise RoleAssumed
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.validate_credentials",
|
||||
return_value=answered,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.assume_role",
|
||||
side_effect=assume_role,
|
||||
),
|
||||
):
|
||||
with raises(RoleAssumed):
|
||||
AwsProvider(
|
||||
organizations_role_arn=organizations_role_arn,
|
||||
session_duration=900,
|
||||
)
|
||||
|
||||
assert sts_regions == [AWS_REGION_GOV_CLOUD_US_WEST_1]
|
||||
|
||||
@mock_aws
|
||||
def test_test_connection_with_env_credentials(self, monkeypatch):
|
||||
# Create a mock IAM user
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user