Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49104f39df | ||
|
|
4d13e8432e | ||
|
|
5f24bec9fe | ||
|
|
afefb8f333 | ||
|
|
2b81fdcc04 | ||
|
|
db298c1d48 | ||
|
|
2877c3d6c0 | ||
|
|
ee64c17108 | ||
|
|
a610314eba | ||
|
|
301edea7ce | ||
|
|
c89d900aae | ||
|
|
4cfb4eeb96 | ||
|
|
f19478f2f6 | ||
|
|
2f11b16299 | ||
|
|
654d2c9f17 | ||
|
|
2721d42594 | ||
|
|
bd1956446d | ||
|
|
d26d7cf91a | ||
|
|
4c20bf1fac | ||
|
|
9dc53ffc60 | ||
|
|
95642fb220 | ||
|
|
6449f3a592 | ||
|
|
91e6cb798d | ||
|
|
bcd37988b5 | ||
|
|
7a64e1a1d1 | ||
|
|
39c85ffb77 | ||
|
|
301ca50541 | ||
|
|
f299e1d9ac | ||
|
|
465e35bf54 | ||
|
|
829af2e3f7 | ||
|
|
51c65bdc69 | ||
|
|
d0c088dd0a | ||
|
|
411d112165 | ||
|
|
8a4cc8780d | ||
|
|
cd4d2a27e3 | ||
|
|
9898cf7364 | ||
|
|
83d8cfa829 | ||
|
|
5202a68cf0 | ||
|
|
e3a3acc799 | ||
|
|
3e000faa31 | ||
|
|
f39c92b8f8 | ||
|
|
823efc5ab1 | ||
|
|
75d7fa5006 | ||
|
|
db25484ccb | ||
|
|
3da4209ee7 | ||
|
|
acb6ff0425 | ||
|
|
ba564af4f4 | ||
|
|
0b9791ffdc | ||
|
|
f6defefb58 | ||
|
|
f3224d0988 | ||
|
|
450e6ba553 | ||
|
|
2cd93fe119 | ||
|
|
f807b22ea6 | ||
|
|
b6e9967da6 | ||
|
|
16e62f7514 | ||
|
|
13ce9436b3 | ||
|
|
d3ced63397 | ||
|
|
758b696ca5 | ||
|
|
0d3ce45374 | ||
|
|
f35666ff0a | ||
|
|
0758c3585d | ||
|
|
dd882c70e7 | ||
|
|
d05c9fbb31 | ||
|
|
7bde42ffb9 | ||
|
|
0d3df0fd0b | ||
|
|
ab996417e6 |
@@ -158,7 +158,7 @@ SENTRY_RELEASE=local
|
||||
# REO_DEV_CLIENT_ID=
|
||||
|
||||
#### Prowler release version ####
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.39.0
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.40.0
|
||||
|
||||
# Social login credentials
|
||||
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
|
||||
|
||||
@@ -64,7 +64,7 @@ runs:
|
||||
scanners: 'vuln'
|
||||
ignore-unfixed: 'true' # A finding with no available fix is not actionable, so it must not gate
|
||||
timeout: '5m'
|
||||
version: 'v0.72.0'
|
||||
version: 'v0.74.0'
|
||||
# Not trivyignores: that input drops the .yaml extension Trivy parses by.
|
||||
env:
|
||||
TRIVY_IGNOREFILE: '.trivyignore.yaml'
|
||||
@@ -81,7 +81,7 @@ runs:
|
||||
scanners: 'vuln'
|
||||
ignore-unfixed: 'true' # A finding with no available fix is not actionable, so it must not gate
|
||||
timeout: '5m'
|
||||
version: 'v0.72.0'
|
||||
version: 'v0.74.0'
|
||||
# Not trivyignores: that input drops the .yaml extension Trivy parses by.
|
||||
env:
|
||||
TRIVY_IGNOREFILE: '.trivyignore.yaml'
|
||||
|
||||
@@ -199,7 +199,7 @@ You MUST structure your response using this EXACT format. Do NOT include anythin
|
||||
|
||||
### For Check Logic Bug
|
||||
|
||||
```
|
||||
```markdown
|
||||
### AI Assessment [Experimental]: Check Logic Bug
|
||||
|
||||
**Component**: {component from issue template}
|
||||
@@ -297,7 +297,7 @@ Write tests FIRST (TDD). The skills contain all testing conventions and patterns
|
||||
|
||||
### For Bug (non-check)
|
||||
|
||||
```
|
||||
```markdown
|
||||
### AI Assessment [Experimental]: Bug
|
||||
|
||||
**Component**: {CLI/SDK | API | UI | Dashboard | MCP Server | Other}
|
||||
@@ -378,7 +378,7 @@ Write tests FIRST (TDD). The skills contain all testing conventions and patterns
|
||||
|
||||
### For Already Fixed
|
||||
|
||||
```
|
||||
```markdown
|
||||
### AI Assessment [Experimental]: Already Fixed
|
||||
|
||||
**Component**: {component}
|
||||
@@ -401,7 +401,7 @@ Upgrade to the latest version. Close the issue as resolved.
|
||||
|
||||
### For Feature Request
|
||||
|
||||
```
|
||||
```markdown
|
||||
### AI Assessment [Experimental]: Feature Request
|
||||
|
||||
**Component**: {component}
|
||||
@@ -419,7 +419,7 @@ Upgrade to the latest version. Close the issue as resolved.
|
||||
|
||||
### For Not a Bug
|
||||
|
||||
```
|
||||
```markdown
|
||||
### AI Assessment [Experimental]: Not a Bug
|
||||
|
||||
**Component**: {component}
|
||||
@@ -440,7 +440,7 @@ Upgrade to the latest version. Close the issue as resolved.
|
||||
|
||||
### For Needs More Information
|
||||
|
||||
```
|
||||
```markdown
|
||||
### AI Assessment [Experimental]: Needs More Information
|
||||
|
||||
**Component**: {component or "Unknown"}
|
||||
|
||||
@@ -52,6 +52,16 @@ provider/alibabacloud:
|
||||
- any-glob-to-any-file: "prowler/providers/alibabacloud/**"
|
||||
- any-glob-to-any-file: "tests/providers/alibabacloud/**"
|
||||
|
||||
provider/huaweicloud:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/huaweicloud/**"
|
||||
- any-glob-to-any-file: "tests/providers/huaweicloud/**"
|
||||
|
||||
provider/image:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/image/**"
|
||||
- any-glob-to-any-file: "tests/providers/image/**"
|
||||
|
||||
provider/cloudflare:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/cloudflare/**"
|
||||
@@ -82,6 +92,11 @@ provider/linode:
|
||||
- any-glob-to-any-file: "prowler/providers/linode/**"
|
||||
- any-glob-to-any-file: "tests/providers/linode/**"
|
||||
|
||||
provider/stackit:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/stackit/**"
|
||||
- any-glob-to-any-file: "tests/providers/stackit/**"
|
||||
|
||||
github_actions:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ".github/workflows/*"
|
||||
|
||||
@@ -8,11 +8,11 @@ These JSON templates are used with the `slackapi/slack-github-action` using the
|
||||
|
||||
### Available Templates
|
||||
|
||||
**Container Releases**
|
||||
#### Container Releases
|
||||
- `container-release-started.json`: Simple one-line notification when container push starts
|
||||
- `container-release-completed.json`: Simple one-line notification when container release completes
|
||||
|
||||
**Deployments**
|
||||
#### Deployments
|
||||
- `deployment-started.json`: Deployment start notification with Block Kit formatting
|
||||
- `deployment-completed.json`: Deployment completion notification (updates the start message)
|
||||
|
||||
@@ -416,17 +416,17 @@ For deployments that start with one message and update it with the final status:
|
||||
### Container Release (Simple One-Line)
|
||||
|
||||
**Start message:**
|
||||
```
|
||||
```text
|
||||
API container release 4.5.0 push started... View run
|
||||
```
|
||||
|
||||
**Completion message (success):**
|
||||
```
|
||||
```text
|
||||
[✓] API container release 4.5.0 push completed successfully! View run
|
||||
```
|
||||
|
||||
**Completion message (failure):**
|
||||
```
|
||||
```text
|
||||
[✗] API container release 4.5.0 push failed View run
|
||||
```
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ jobs:
|
||||
api.github.com:443
|
||||
github.com:443
|
||||
objects.githubusercontent.com:443
|
||||
release-assets.githubusercontent.com:443
|
||||
pypi.org:443
|
||||
files.pythonhosted.org:443
|
||||
|
||||
|
||||
@@ -55,6 +55,10 @@ jobs:
|
||||
# Pin must match .pre-commit-config.yaml so prek and CI behave identically.
|
||||
# pnpm dlx doesn't accept --ignore-scripts as a flag; the env var
|
||||
# disables postinstall scripts on transitives the same way.
|
||||
#
|
||||
# Files come from `git ls-files` because markdownlint doesn't traverse
|
||||
# dot-directories, so `.github/**/*.md` went unlinted.
|
||||
# `.markdownlintignore` still applies to the listed paths.
|
||||
env:
|
||||
pnpm_config_ignore_scripts: 'true'
|
||||
run: pnpm dlx markdownlint-cli@0.45.0 '**/*.md'
|
||||
run: git ls-files -z '*.md' | xargs -0 -r pnpm dlx markdownlint-cli@0.45.0 --
|
||||
|
||||
@@ -114,6 +114,8 @@ jobs:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
auth.docker.io:443
|
||||
dl-cdn.alpinelinux.org:443
|
||||
dualstack.j.sni.global.fastly.net:443
|
||||
files.pythonhosted.org:443
|
||||
ghcr.io:443
|
||||
github.com:443
|
||||
|
||||
@@ -81,6 +81,8 @@ jobs:
|
||||
pkg-containers.githubusercontent.com:443
|
||||
files.pythonhosted.org:443
|
||||
pypi.org:443
|
||||
dl-cdn.alpinelinux.org:443
|
||||
dualstack.j.sni.global.fastly.net:443
|
||||
api.github.com:443
|
||||
mirror.gcr.io:443
|
||||
check.trivy.dev:443
|
||||
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
|
||||
- name: Publish prowler-mcp package to PyPI
|
||||
if: steps.pypi-check.outputs.skip != 'true'
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
||||
with:
|
||||
packages-dir: ${{ env.WORKING_DIRECTORY }}/dist/
|
||||
print-hash: true
|
||||
|
||||
@@ -35,6 +35,7 @@ jobs:
|
||||
api.github.com:443
|
||||
github.com:443
|
||||
objects.githubusercontent.com:443
|
||||
release-assets.githubusercontent.com:443
|
||||
pypi.org:443
|
||||
files.pythonhosted.org:443
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
name: 'SDK: Package Checks'
|
||||
|
||||
# Rehearses the PyPI release on every packaging change and once a week, from the
|
||||
# consumer's side. Two incidents this guards against:
|
||||
#
|
||||
# - 5.38.0 shipped an unsatisfiable Requires-Dist (cryptography==50.0.0 while
|
||||
# alibabacloud-tea-openapi and pyopenssl cap it below 49). A [tool.uv] override hid
|
||||
# the conflict inside the repo; pip could not install the wheel and silently
|
||||
# resolved `pip install prowler` to 5.37.1 for a week.
|
||||
# - 5.39.0 never published: an unpinned build backend started emitting core metadata
|
||||
# 2.5 and the twine bundled in the publish action rejected it.
|
||||
#
|
||||
# Both were only detectable at release time because nothing built and installed the
|
||||
# artifact earlier. The weekly run also catches releases yanked from PyPI after we
|
||||
# pinned them (zstd 1.5.7.3, "buggy - not thread safe", sat in uv.lock for months).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v5.*'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v5.*'
|
||||
schedule:
|
||||
# Monday 06:00 UTC. Yanks and upstream releases happen without a commit here.
|
||||
- cron: '0 6 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
# Must equal the twine bundled in the pypa/gh-action-pypi-publish pin used by
|
||||
# sdk-pypi-release.yml (requirements/runtime.txt in that repo at the pinned tag).
|
||||
# A metadata check that passes here must pass there.
|
||||
TWINE_VERSION: '7.0.0'
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
# Scheduled and manual runs always execute; pushes and PRs only when a packaging
|
||||
# input changed. Jobs skipped this way still report success to branch protection.
|
||||
run: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || steps.filter.outputs.any_changed == 'true' }}
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
github.com:443
|
||||
api.github.com:443
|
||||
|
||||
- name: Checkout repository
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# zizmor: ignore[artipacked]
|
||||
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
|
||||
|
||||
- name: Detect packaging changes
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
id: filter
|
||||
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||
with:
|
||||
files: |
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
README.md
|
||||
util/replicate_pypi_package.py
|
||||
util/check_yanked_pins.py
|
||||
api/pyproject.toml
|
||||
api/uv.lock
|
||||
mcp_server/pyproject.toml
|
||||
mcp_server/uv.lock
|
||||
.github/workflows/sdk-package-checks.yml
|
||||
.github/workflows/sdk-pypi-release.yml
|
||||
.github/actions/setup-python-uv/**
|
||||
|
||||
install-from-wheel:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.run == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version:
|
||||
- '3.10'
|
||||
- '3.11'
|
||||
- '3.12'
|
||||
- '3.13'
|
||||
package:
|
||||
- 'prowler'
|
||||
include:
|
||||
# prowler-cloud is the same tree renamed by util/replicate_pypi_package.py;
|
||||
# one Python is enough to prove the rename and its build still work.
|
||||
- python-version: '3.12'
|
||||
package: 'prowler-cloud'
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
github.com:443
|
||||
api.github.com:443
|
||||
release-assets.githubusercontent.com:443
|
||||
pypi.org:443
|
||||
files.pythonhosted.org:443
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Python with uv
|
||||
uses: ./.github/actions/setup-python-uv
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
install-dependencies: 'false'
|
||||
|
||||
- name: Rename package to prowler-cloud
|
||||
if: matrix.package == 'prowler-cloud'
|
||||
run: |
|
||||
pip install --no-cache-dir toml
|
||||
python util/replicate_pypi_package.py
|
||||
|
||||
- name: Build sdist and wheel
|
||||
run: uv build
|
||||
|
||||
- name: Check metadata with the release workflow's twine
|
||||
run: uvx --from "twine==${TWINE_VERSION}" twine check --strict dist/*
|
||||
|
||||
- name: Install the wheel with pip into a clean virtualenv
|
||||
# Plain pip, --isolated, from outside the repo: consumers never see [tool.uv]
|
||||
# override-dependencies or constraint-dependencies, so neither does this step.
|
||||
run: |
|
||||
python -m venv "${RUNNER_TEMP}/consumer"
|
||||
"${RUNNER_TEMP}/consumer/bin/python" -m pip install --quiet --upgrade pip
|
||||
cd "${RUNNER_TEMP}"
|
||||
"${RUNNER_TEMP}/consumer/bin/python" -m pip install --isolated --no-cache-dir "${GITHUB_WORKSPACE}"/dist/*.whl
|
||||
|
||||
- name: Smoke test the installed CLI
|
||||
run: |
|
||||
cd "${RUNNER_TEMP}"
|
||||
"${RUNNER_TEMP}/consumer/bin/prowler" --version
|
||||
# Loads every AWS check module from the installed wheel: catches files missing
|
||||
# from the package. grep fails the step if the summary line never appears.
|
||||
"${RUNNER_TEMP}/consumer/bin/prowler" aws --list-checks | grep 'available checks'
|
||||
|
||||
pinned-releases-not-yanked:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.run == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
github.com:443
|
||||
api.github.com:443
|
||||
release-assets.githubusercontent.com:443
|
||||
pypi.org:443
|
||||
files.pythonhosted.org:443
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Check every pinned and locked release against PyPI
|
||||
run: python util/check_yanked_pins.py . api mcp_server
|
||||
@@ -84,8 +84,18 @@ jobs:
|
||||
- name: Build Prowler package
|
||||
run: uv build
|
||||
|
||||
- name: Verify the wheel installs with pip
|
||||
# Same check as "SDK: Package Checks", repeated on the exact artifact about to be
|
||||
# published. Plain pip, --isolated, from outside the repo: an unsatisfiable
|
||||
# Requires-Dist fails here instead of on users' machines (5.38.0 shipped one).
|
||||
run: |
|
||||
python -m venv "${RUNNER_TEMP}/consumer"
|
||||
"${RUNNER_TEMP}/consumer/bin/python" -m pip install --quiet --upgrade pip
|
||||
cd "${RUNNER_TEMP}"
|
||||
"${RUNNER_TEMP}/consumer/bin/python" -m pip install --isolated --no-cache-dir --dry-run "${GITHUB_WORKSPACE}"/dist/*.whl
|
||||
|
||||
- name: Publish Prowler package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
||||
with:
|
||||
print-hash: true
|
||||
|
||||
@@ -128,7 +138,17 @@ jobs:
|
||||
- name: Build prowler-cloud package
|
||||
run: uv build
|
||||
|
||||
- name: Verify the wheel installs with pip
|
||||
# Same check as "SDK: Package Checks", repeated on the exact artifact about to be
|
||||
# published. Plain pip, --isolated, from outside the repo: an unsatisfiable
|
||||
# Requires-Dist fails here instead of on users' machines (5.38.0 shipped one).
|
||||
run: |
|
||||
python -m venv "${RUNNER_TEMP}/consumer"
|
||||
"${RUNNER_TEMP}/consumer/bin/python" -m pip install --quiet --upgrade pip
|
||||
cd "${RUNNER_TEMP}"
|
||||
"${RUNNER_TEMP}/consumer/bin/python" -m pip install --isolated --no-cache-dir --dry-run "${GITHUB_WORKSPACE}"/dist/*.whl
|
||||
|
||||
- name: Publish prowler-cloud package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
||||
with:
|
||||
print-hash: true
|
||||
|
||||
@@ -57,6 +57,7 @@ jobs:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
github.com:443
|
||||
release-assets.githubusercontent.com:443
|
||||
pypi.org:443
|
||||
files.pythonhosted.org:443
|
||||
|
||||
|
||||
@@ -135,8 +135,8 @@ vulnerabilities:
|
||||
# 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.72.0 contains go-git 5.19.1, and even the latest published
|
||||
# Trivy release, 0.73.0, still pins that vulnerable version:
|
||||
# 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
|
||||
@@ -164,7 +164,3 @@ vulnerabilities:
|
||||
purls:
|
||||
- "pkg:golang/oras.land/oras-go/v2"
|
||||
expired_at: 2026-12-31
|
||||
- id: CVE-2026-39822
|
||||
purls:
|
||||
- "pkg:golang/stdlib"
|
||||
expired_at: 2026-12-31
|
||||
|
||||
@@ -8,24 +8,34 @@ ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
|
||||
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
|
||||
ENV POWERSHELL_TELEMETRY_OPTOUT=1
|
||||
|
||||
ARG TRIVY_VERSION=0.72.0
|
||||
ARG TRIVY_VERSION=0.74.0
|
||||
ENV TRIVY_VERSION=${TRIVY_VERSION}
|
||||
|
||||
ARG ZIZMOR_VERSION=1.24.1
|
||||
ENV ZIZMOR_VERSION=${ZIZMOR_VERSION}
|
||||
|
||||
# Pinned here, not fetched with the artefact: a compromised release ships its own checksum.
|
||||
ARG TRIVY_SHA256_AMD64=bbb64b9695866ce4a7a8f5c9592002c5961cab378577fa3f8a040df362b9b2ea
|
||||
ARG TRIVY_SHA256_ARM64=2ca2c023109c2db6b2b77366b6717291452d4531167377d95c79547f0c8e3467
|
||||
ARG TRIVY_SHA256_AMD64=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
|
||||
ARG TRIVY_SHA256_ARM64=b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5
|
||||
ARG POWERSHELL_SHA256_AMD64=492ff26bb958336bf61e597ce19e07648b4003bd2a08659e02f0e3e0446ebfe0
|
||||
ARG POWERSHELL_SHA256_ARM64=2503b71da3e83635592b092df59a0aca4c3606b4d9b068217bb00be989cb0d56
|
||||
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:
|
||||
# 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)
|
||||
# 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.
|
||||
# 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 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install PowerShell
|
||||
|
||||
@@ -4,6 +4,36 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [1.41.0] (Prowler v5.40.0)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- `FINDINGS_TABLE_PARTITION_MAX_AGE_MONTHS` is now applied in months instead of days, and negative values are rejected [(#12580)](https://github.com/prowler-cloud/prowler/pull/12580)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- `sqlparse` upgraded to 0.6.0, patching CVE-2026-54284, CVE-2026-59893, and CVE-2026-71491 [(#12509)](https://github.com/prowler-cloud/prowler/pull/12509)
|
||||
- `openssl`, `libssl3t64` and `openssl-provider-legacy` upgraded to 3.5.7-1~deb13u2 in the API container image, patching ten high OpenSSL CVEs [(#12549)](https://github.com/prowler-cloud/prowler/pull/12549)
|
||||
|
||||
---
|
||||
|
||||
## [1.40.1] (Prowler v5.39.1)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Bump alibabacloud-tea-openapi to 0.4.6, oci to 2.184.1 and pyopenssl to 26.4.0 to match the SDK; the cryptography override now names its actual blockers (azure-cli-core pins msal below 1.37, workos 8.3.0 requires cryptography 48) [(#12477)](https://github.com/prowler-cloud/prowler/pull/12477)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Pin zstd to 1.5.7.2; 1.5.7.3 was yanked from PyPI as not thread safe [(#12477)](https://github.com/prowler-cloud/prowler/pull/12477)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- Trivy from v0.72.0 to v0.73.0 in the container image, fixing HIGH CVE-2026-46600 in the bundled `golang.org/x/net` [(#12445)](https://github.com/prowler-cloud/prowler/pull/12445)
|
||||
- Trivy v0.74.0 and Debian util-linux 2.41.5-0+deb13u1 in the API container image, patching Go standard library vulnerabilities and CVE-2026-53615 [(#12470)](https://github.com/prowler-cloud/prowler/pull/12470)
|
||||
|
||||
---
|
||||
|
||||
## [1.40.0] (Prowler v5.39.0)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
@@ -7,20 +7,28 @@ ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
|
||||
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
|
||||
ENV POWERSHELL_TELEMETRY_OPTOUT=1
|
||||
|
||||
ARG TRIVY_VERSION=0.72.0
|
||||
ARG TRIVY_VERSION=0.74.0
|
||||
ENV TRIVY_VERSION=${TRIVY_VERSION}
|
||||
|
||||
ARG ZIZMOR_VERSION=1.24.1
|
||||
ENV ZIZMOR_VERSION=${ZIZMOR_VERSION}
|
||||
|
||||
# Pinned here, not fetched with the artefact: a compromised release ships its own checksum.
|
||||
ARG TRIVY_SHA256_AMD64=bbb64b9695866ce4a7a8f5c9592002c5961cab378577fa3f8a040df362b9b2ea
|
||||
ARG TRIVY_SHA256_ARM64=2ca2c023109c2db6b2b77366b6717291452d4531167377d95c79547f0c8e3467
|
||||
ARG TRIVY_SHA256_AMD64=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
|
||||
ARG TRIVY_SHA256_ARM64=b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5
|
||||
ARG POWERSHELL_SHA256_AMD64=492ff26bb958336bf61e597ce19e07648b4003bd2a08659e02f0e3e0446ebfe0
|
||||
ARG POWERSHELL_SHA256_ARM64=2503b71da3e83635592b092df59a0aca4c3606b4d9b068217bb00be989cb0d56
|
||||
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:
|
||||
# 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)
|
||||
# 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.
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
wget \
|
||||
@@ -36,6 +44,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libtool \
|
||||
libxslt1-dev \
|
||||
python3-dev \
|
||||
&& apt-get install -y --no-install-recommends --only-upgrade \
|
||||
util-linux libssl3t64 openssl openssl-provider-legacy \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install PowerShell
|
||||
|
||||
@@ -45,7 +45,7 @@ dependencies = [
|
||||
"gunicorn==26.0.0",
|
||||
"uvloop==0.22.1",
|
||||
"lxml==6.1.0",
|
||||
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
|
||||
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.40",
|
||||
"psycopg2-binary==2.9.9",
|
||||
"pytest-celery[redis] (==1.3.0)",
|
||||
"sentry-sdk[django] (==2.56.0)",
|
||||
@@ -61,7 +61,7 @@ dependencies = [
|
||||
"cartography (==0.138.1)",
|
||||
"gevent (==25.9.1)",
|
||||
"werkzeug (==3.1.7)",
|
||||
"sqlparse (==0.5.5)",
|
||||
"sqlparse (==0.6.0)",
|
||||
"fonttools (==4.62.1)",
|
||||
"uvicorn-worker (==0.4.0)"
|
||||
]
|
||||
@@ -71,7 +71,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.40.0"
|
||||
version = "1.41.0"
|
||||
|
||||
# Shared ruff baseline (kept in sync with mcp_server/pyproject.toml).
|
||||
# target-version tracks this project's lowest supported Python.
|
||||
@@ -92,8 +92,7 @@ extend-select = [
|
||||
|
||||
[tool.uv]
|
||||
# Transitive pins matching master to avoid silent drift; bump deliberately.
|
||||
# workos and pyopenssl run ahead of master: the versions master pins cap cryptography
|
||||
# below 48, so both were bumped to versions that allow it (PROWLER-2310).
|
||||
# workos is api-only; pyopenssl matches master (PROWLER-2310).
|
||||
constraint-dependencies = [
|
||||
"about-time==4.2.1",
|
||||
"adal==1.2.7",
|
||||
@@ -130,7 +129,7 @@ constraint-dependencies = [
|
||||
"alibabacloud-sls20201230==5.9.0",
|
||||
"alibabacloud-sts20150401==1.1.6",
|
||||
"alibabacloud-tea==0.4.3",
|
||||
"alibabacloud-tea-openapi==0.4.5",
|
||||
"alibabacloud-tea-openapi==0.4.6",
|
||||
"alibabacloud-tea-util==0.3.14",
|
||||
"alibabacloud-tea-xml==0.0.3",
|
||||
"alibabacloud-vpc20160428==6.13.0",
|
||||
@@ -339,7 +338,7 @@ constraint-dependencies = [
|
||||
"nltk==3.9.4",
|
||||
"numpy==2.2.6",
|
||||
"oauthlib==3.3.1",
|
||||
"oci==2.183.0",
|
||||
"oci==2.184.1",
|
||||
"openai==1.109.1",
|
||||
"openstacksdk==4.2.0",
|
||||
"opentelemetry-api==1.39.1",
|
||||
@@ -380,7 +379,7 @@ constraint-dependencies = [
|
||||
"pylint==3.2.5",
|
||||
"pymsalruntime==0.18.1",
|
||||
"pynacl==1.6.2",
|
||||
"pyopenssl==26.2.0",
|
||||
"pyopenssl==26.4.0",
|
||||
"pyparsing==3.3.2",
|
||||
"pyreadline3==3.5.4",
|
||||
"pysocks==1.7.1",
|
||||
@@ -424,7 +423,7 @@ constraint-dependencies = [
|
||||
"six==1.17.0",
|
||||
"slack-sdk==3.39.0",
|
||||
"sniffio==1.3.1",
|
||||
"sqlparse==0.5.5",
|
||||
"sqlparse==0.6.0",
|
||||
"statsd==4.0.1",
|
||||
"std-uritemplate==2.0.8",
|
||||
"stevedore==5.6.0",
|
||||
@@ -458,7 +457,7 @@ constraint-dependencies = [
|
||||
"zipp==3.23.0",
|
||||
"zope-event==6.1",
|
||||
"zope-interface==8.2",
|
||||
"zstd==1.5.7.3"
|
||||
"zstd==1.5.7.2"
|
||||
]
|
||||
# prowler@master needs okta==3.4.2, but cartography 0.138.1 requires okta<1.0.0.
|
||||
# Attack Paths does not ingest Okta today, so override the Cartography
|
||||
@@ -485,7 +484,12 @@ constraint-dependencies = [
|
||||
# that request pyjwt[crypto] and leave cryptography (needed for RS256) only transitive.
|
||||
override-dependencies = [
|
||||
"okta==3.4.2",
|
||||
# alibabacloud-tea-openapi 0.4.5 caps cryptography below 49 and is the latest release.
|
||||
# prowler requires cryptography==50.0.0. Two api-only dependencies still cap it below
|
||||
# 49 and cannot move yet: msal, pinned exactly by azure-cli-core (2.83.0 -> 1.35.0b1,
|
||||
# 2.89.1 -> 1.36.0, both <49; cartography needs azure-cli-core), and workos 8.3.0
|
||||
# (~=48.0; workos 10.1.1+ needs ~=50.0 and is a separate SDK upgrade). This api is
|
||||
# deployed from this lock with `uv sync --locked`, so the override applies to what runs.
|
||||
# Remove when azure-cli-core pins msal>=1.37.0 and workos is on 10.x.
|
||||
"cryptography==50.0.0",
|
||||
"azure-mgmt-containerservice==34.1.0",
|
||||
"microsoft-kiota-abstractions==1.9.10",
|
||||
|
||||
@@ -6,6 +6,7 @@ from api.rls import RowLevelSecurityConstraint
|
||||
from api.uuid_utils import datetime_to_uuid7
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from psqlextra.partitioning import (
|
||||
PostgresPartitioningError,
|
||||
PostgresPartitioningManager,
|
||||
@@ -153,10 +154,17 @@ class PostgresUUIDv7PartitioningStrategy(PostgresRangePartitioningStrategy):
|
||||
)
|
||||
|
||||
|
||||
def relative_days_or_none(value):
|
||||
if value is None:
|
||||
def relative_months_or_none(value):
|
||||
# A negative value would set the cutoff in the future and delete every
|
||||
# partition, so it is rejected rather than silently ignored.
|
||||
if value is not None and value < 0:
|
||||
raise ImproperlyConfigured(
|
||||
"FINDINGS_TABLE_PARTITION_MAX_AGE_MONTHS must not be negative; "
|
||||
"leave it unset or use 0 to keep partitions indefinitely"
|
||||
)
|
||||
if not value:
|
||||
return None
|
||||
return relativedelta(days=value)
|
||||
return relativedelta(months=value)
|
||||
|
||||
|
||||
#
|
||||
@@ -173,7 +181,7 @@ manager = PostgresPartitioningManager(
|
||||
months=settings.FINDINGS_TABLE_PARTITION_MONTHS
|
||||
),
|
||||
count=settings.FINDINGS_TABLE_PARTITION_COUNT,
|
||||
max_age=relative_days_or_none(
|
||||
max_age=relative_months_or_none(
|
||||
settings.FINDINGS_TABLE_PARTITION_MAX_AGE_MONTHS
|
||||
),
|
||||
name_format="%Y_%b",
|
||||
@@ -189,7 +197,7 @@ manager = PostgresPartitioningManager(
|
||||
months=settings.FINDINGS_TABLE_PARTITION_MONTHS
|
||||
),
|
||||
count=settings.FINDINGS_TABLE_PARTITION_COUNT,
|
||||
max_age=relative_days_or_none(
|
||||
max_age=relative_months_or_none(
|
||||
settings.FINDINGS_TABLE_PARTITION_MAX_AGE_MONTHS
|
||||
),
|
||||
name_format="%Y_%b",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.40.0
|
||||
version: 1.41.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
from datetime import UTC, datetime
|
||||
from itertools import islice
|
||||
|
||||
import pytest
|
||||
from api.partitions import (
|
||||
PostgresUUIDv7PartitioningStrategy,
|
||||
relative_months_or_none,
|
||||
)
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from psqlextra.partitioning import PostgresTimePartitionSize
|
||||
|
||||
|
||||
def build_strategy(max_age):
|
||||
return PostgresUUIDv7PartitioningStrategy(
|
||||
size=PostgresTimePartitionSize(months=1),
|
||||
count=1,
|
||||
start_date=datetime.now(UTC),
|
||||
max_age=max_age,
|
||||
name_format="%Y_%b",
|
||||
)
|
||||
|
||||
|
||||
class TestRelativeMonthsOrNone:
|
||||
@pytest.mark.parametrize("value", [None, 0])
|
||||
def test_unset_or_zero_keeps_partitions_indefinitely(self, value):
|
||||
assert relative_months_or_none(value) is None
|
||||
|
||||
@pytest.mark.parametrize("months", [1, 3, 12])
|
||||
def test_value_is_interpreted_as_months(self, months):
|
||||
assert relative_months_or_none(months) == relativedelta(months=months)
|
||||
|
||||
def test_value_is_not_interpreted_as_days(self):
|
||||
assert relative_months_or_none(12) != relativedelta(days=12)
|
||||
|
||||
def test_negative_is_rejected(self):
|
||||
with pytest.raises(ImproperlyConfigured):
|
||||
relative_months_or_none(-12)
|
||||
|
||||
|
||||
class TestToDelete:
|
||||
@pytest.mark.parametrize("max_age", [None, relative_months_or_none(0)])
|
||||
def test_nothing_is_deleted_without_max_age(self, max_age):
|
||||
strategy = build_strategy(max_age)
|
||||
|
||||
assert list(islice(strategy.to_delete(), 5)) == []
|
||||
|
||||
def test_first_deleted_partition_is_max_age_old(self):
|
||||
months = 3
|
||||
strategy = build_strategy(relative_months_or_none(months))
|
||||
|
||||
first = next(strategy.to_delete())
|
||||
|
||||
expected = strategy.get_start_datetime() - relativedelta(months=months)
|
||||
assert first.name() == expected.strftime("%Y_%b").lower()
|
||||
|
||||
def test_deleted_partitions_go_further_back_in_time(self):
|
||||
strategy = build_strategy(relative_months_or_none(3))
|
||||
|
||||
names = [p.name() for p in islice(strategy.to_delete(), 3)]
|
||||
starts = [datetime.strptime(n, "%Y_%b") for n in names]
|
||||
|
||||
assert starts == sorted(starts, reverse=True)
|
||||
@@ -45,7 +45,7 @@ constraints = [
|
||||
{ name = "alibabacloud-sls20201230", specifier = "==5.9.0" },
|
||||
{ name = "alibabacloud-sts20150401", specifier = "==1.1.6" },
|
||||
{ name = "alibabacloud-tea", specifier = "==0.4.3" },
|
||||
{ name = "alibabacloud-tea-openapi", specifier = "==0.4.5" },
|
||||
{ name = "alibabacloud-tea-openapi", specifier = "==0.4.6" },
|
||||
{ name = "alibabacloud-tea-util", specifier = "==0.3.14" },
|
||||
{ name = "alibabacloud-tea-xml", specifier = "==0.0.3" },
|
||||
{ name = "alibabacloud-vpc20160428", specifier = "==6.13.0" },
|
||||
@@ -254,7 +254,7 @@ constraints = [
|
||||
{ name = "nltk", specifier = "==3.9.4" },
|
||||
{ name = "numpy", specifier = "==2.2.6" },
|
||||
{ name = "oauthlib", specifier = "==3.3.1" },
|
||||
{ name = "oci", specifier = "==2.183.0" },
|
||||
{ name = "oci", specifier = "==2.184.1" },
|
||||
{ name = "openai", specifier = "==1.109.1" },
|
||||
{ name = "openstacksdk", specifier = "==4.2.0" },
|
||||
{ name = "opentelemetry-api", specifier = "==1.39.1" },
|
||||
@@ -295,7 +295,7 @@ constraints = [
|
||||
{ name = "pylint", specifier = "==3.2.5" },
|
||||
{ name = "pymsalruntime", specifier = "==0.18.1" },
|
||||
{ name = "pynacl", specifier = "==1.6.2" },
|
||||
{ name = "pyopenssl", specifier = "==26.2.0" },
|
||||
{ name = "pyopenssl", specifier = "==26.4.0" },
|
||||
{ name = "pyparsing", specifier = "==3.3.2" },
|
||||
{ name = "pyreadline3", specifier = "==3.5.4" },
|
||||
{ name = "pysocks", specifier = "==1.7.1" },
|
||||
@@ -339,7 +339,7 @@ constraints = [
|
||||
{ name = "six", specifier = "==1.17.0" },
|
||||
{ name = "slack-sdk", specifier = "==3.39.0" },
|
||||
{ name = "sniffio", specifier = "==1.3.1" },
|
||||
{ name = "sqlparse", specifier = "==0.5.5" },
|
||||
{ name = "sqlparse", specifier = "==0.6.0" },
|
||||
{ name = "statsd", specifier = "==4.0.1" },
|
||||
{ name = "std-uritemplate", specifier = "==2.0.8" },
|
||||
{ name = "stevedore", specifier = "==5.6.0" },
|
||||
@@ -373,7 +373,7 @@ constraints = [
|
||||
{ name = "zipp", specifier = "==3.23.0" },
|
||||
{ name = "zope-event", specifier = "==6.1" },
|
||||
{ name = "zope-interface", specifier = "==8.2" },
|
||||
{ name = "zstd", specifier = "==1.5.7.3" },
|
||||
{ name = "zstd", specifier = "==1.5.7.2" },
|
||||
]
|
||||
overrides = [
|
||||
{ name = "azure-mgmt-containerservice", specifier = "==34.1.0" },
|
||||
@@ -860,7 +860,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/9a/7d/b22cb9a0d4f396ee0
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-tea-openapi"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "alibabacloud-credentials" },
|
||||
@@ -869,9 +869,9 @@ dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "darabonba-core" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/73/fb0c4d44759791ecdf269fc715c1e810fa1aba3981bfaaf8a01f61899296/alibabacloud_tea_openapi-0.4.5.tar.gz", hash = "sha256:75fa1f4360a46e41f5bf5f8d4917e52efb6f64885839bc1328c35590670c97b9", size = 26616, upload-time = "2026-07-14T13:15:39.364Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/34/1918a2d780676494365c7f945bfab397ecddb988054d78025bd26f438977/alibabacloud_tea_openapi-0.4.6.tar.gz", hash = "sha256:dafc32401712f5b21c12dc3d05ba887a91ad156d9b49a7662279f9fd90526fb2", size = 26742, upload-time = "2026-08-17T08:34:11.55Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/ec/6b368a10e9c2e8b1b394c69b96ac213ae66e8c4895e0baa1ffaf7178fd32/alibabacloud_tea_openapi-0.4.5-py3-none-any.whl", hash = "sha256:338979095c7beda80a5b413c31262892cafdc12069dde4ce4fc2e4f7ce0fc609", size = 33333, upload-time = "2026-07-14T13:15:38.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/00/2f534f5884e5f299d9cb3a1e8be2def8071bc6a6e2a192ba4ff2a8cd5e02/alibabacloud_tea_openapi-0.4.6-py3-none-any.whl", hash = "sha256:c9e1727b9fb2936f487d050fc3590c99f9f2065256dc3a927e5b61f414674ed6", size = 33448, upload-time = "2026-08-17T08:34:10.472Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4426,7 +4426,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "oci"
|
||||
version = "2.183.0"
|
||||
version = "2.184.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
@@ -4439,9 +4439,9 @@ dependencies = [
|
||||
{ name = "pytz" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/2a/77bd6cbf1c69b2f368fe3d6462d84369b0cba15e37ce713cdc08d459b95a/oci-2.183.0.tar.gz", hash = "sha256:ff572ef5f2030a788796bb509d257e6a41c6510ef9b4b6a75a079efd06e533ce", size = 17759723, upload-time = "2026-07-28T06:02:29.76Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/2d/fa5368cfabb868f4111c6978e8b5f66aa3a55076c40c1a59ac3081b0227b/oci-2.184.1.tar.gz", hash = "sha256:617dad69caf8dd6e521d224dbc3e8a8bc289906943a0214fd2c3419094e26435", size = 17990631, upload-time = "2026-08-11T11:01:26.194Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/de/8574b3e527996a099d196e87794a4652d91a0c3185fcc7fdbb5649b75a8a/oci-2.183.0-py3-none-any.whl", hash = "sha256:bd789c98a94d7c5ea08c20d11dcf68c9cd1ad479b134727d80a930b84387070b", size = 36133501, upload-time = "2026-07-28T06:02:18.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/63/5ae22e42aaf96a5da74dc2b9de449c78b4d7418cce621d5da723b3e49f32/oci-2.184.1-py3-none-any.whl", hash = "sha256:bd814e38a70da2190e721937455a08689ab13c0750bd2ef8dd0c98b2dc5a38ea", size = 36628063, upload-time = "2026-08-11T11:01:18.178Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4835,8 +4835,8 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler"
|
||||
version = "5.38.0"
|
||||
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#b3d174d0c1eb202ed7cb9a9daf0500683f4443be" }
|
||||
version = "5.40.0"
|
||||
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=v5.40#4d13e8432e57289a188c2a12200dcd8437f35543" }
|
||||
dependencies = [
|
||||
{ name = "alibabacloud-actiontrail20200706" },
|
||||
{ name = "alibabacloud-credentials" },
|
||||
@@ -4928,14 +4928,17 @@ dependencies = [
|
||||
{ name = "stackit-iaas" },
|
||||
{ name = "stackit-objectstorage" },
|
||||
{ name = "stackit-resourcemanager" },
|
||||
{ name = "stackit-ske" },
|
||||
{ name = "tabulate" },
|
||||
{ name = "truststore" },
|
||||
{ name = "tzlocal" },
|
||||
{ name = "uuid6" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prowler-api"
|
||||
version = "1.40.0"
|
||||
version = "1.41.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "cartography" },
|
||||
@@ -5035,12 +5038,12 @@ requires-dist = [
|
||||
{ name = "matplotlib", specifier = "==3.10.8" },
|
||||
{ name = "neo4j", specifier = "==6.1.0" },
|
||||
{ name = "openai", specifier = "==1.109.1" },
|
||||
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=master" },
|
||||
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=v5.40" },
|
||||
{ name = "psycopg2-binary", specifier = "==2.9.9" },
|
||||
{ name = "pytest-celery", extras = ["redis"], specifier = "==1.3.0" },
|
||||
{ name = "reportlab", specifier = "==4.4.10" },
|
||||
{ name = "sentry-sdk", extras = ["django"], specifier = "==2.56.0" },
|
||||
{ name = "sqlparse", specifier = "==0.5.5" },
|
||||
{ name = "sqlparse", specifier = "==0.6.0" },
|
||||
{ name = "uuid6", specifier = "==2024.7.10" },
|
||||
{ name = "uvicorn-worker", specifier = "==0.4.0" },
|
||||
{ name = "uvloop", specifier = "==0.22.1" },
|
||||
@@ -5426,15 +5429,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pyopenssl"
|
||||
version = "26.2.0"
|
||||
version = "26.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/e8/7325d258199b159eb2c03fe32107533e2832e70e63f4fb88a6aa00023201/pyopenssl-26.4.0.tar.gz", hash = "sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7", size = 182046, upload-time = "2026-08-01T19:50:50.512Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ad/2cf6d3fa2fae5c79e1ed9960c0d42badd0f94d81dd12b50604cdc839e648/pyopenssl-26.4.0-py3-none-any.whl", hash = "sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c", size = 56026, upload-time = "2026-08-01T19:50:48.94Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6049,11 +6052,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.5.5"
|
||||
version = "0.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5f/d3/3f06a1006f2261d1342aefb3c71eed02f5d4ca5bdbecd86ebc12ad38306e/sqlparse-0.6.0.tar.gz", hash = "sha256:113c35c75365ab9cc9c7231d68c6428fb11c085fc8e9eb1ad659b7ddbf6cd2b9", size = 178477, upload-time = "2026-08-13T19:16:06.396Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/50/f00935da0ec7cbf325f8dc4f772ae46fbc7b672dd62876e73f0a94adda57/sqlparse-0.6.0-py3-none-any.whl", hash = "sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f", size = 50070, upload-time = "2026-08-13T19:16:04.062Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6117,6 +6120,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/9c/38a74d0f7a89b4320f6d2366fb660638bda8860daa08748b12c713d84381/stackit_resourcemanager-0.8.0-py3-none-any.whl", hash = "sha256:dd04bb8353d041a137c4dcba190beabded7acfaff1bc98b218fce20a99389ebc", size = 81288, upload-time = "2026-05-13T09:43:07.81Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stackit-ske"
|
||||
version = "1.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "requests" },
|
||||
{ name = "stackit-core" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/9e/df3ad585cb96d028354f4253568e9879d81bb9395d5ebfa268fa9350e2df/stackit_ske-1.12.0.tar.gz", hash = "sha256:62814279f3b7fb2387648f92d14453a8905ad60115c07579f2741ddb7d1fcc94", size = 37239, upload-time = "2026-06-30T11:18:49.39Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/37/dc54fb7185a2d4da37308322ea1a7b992312030b2e37262de4eb4003f5c7/stackit_ske-1.12.0-py3-none-any.whl", hash = "sha256:45bd8084d87f14f818b3d7e824450248c8784ed204ca1b2dc108f491dcbdb1a3", size = 93142, upload-time = "2026-06-30T11:18:48.233Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "statsd"
|
||||
version = "4.0.1"
|
||||
@@ -6225,6 +6243,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truststore"
|
||||
version = "0.10.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.21.1"
|
||||
@@ -6622,40 +6649,70 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "1.5.7.3"
|
||||
name = "zstandard"
|
||||
version = "0.25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/62/b9c075ad664e7c4cbb3d8d2be7c246506abe1bc7f778eb58d260ef9538c8/zstd-1.5.7.3.tar.gz", hash = "sha256:403e5205f4ac04b92e6b0cda654be2f51de268228a0db0067bc087faacf2f495", size = 672559, upload-time = "2026-01-08T16:24:43.361Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/75/0d/8c89c0d010b58c21a7865a239790bb1c6822029c053b1ded858d6b573e3a/zstd-1.5.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a3c1781a24e2ced2c0ddee11d45b1f04018b03615eeb622a62eca4d56d3358a", size = 267641, upload-time = "2026-01-08T16:30:50.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/6d/155d8c344d96eca2a5a003a5ddd63373a5f13591fd5cf2b9490250d6805a/zstd-1.5.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a6c7c81056362b60a04baa34632e713d596662a860ec34efd8e9b109c10e6ec7", size = 230962, upload-time = "2026-01-08T16:30:49.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c7/ab93916a26eb58cd501ad701974c31b4bc67a7f6abd6c24bef8fe4d7649b/zstd-1.5.7.3-cp311-cp311-manylinux_2_14_x86_64.whl", hash = "sha256:e564f34a55effc7d654eb293468edc80b64d476b0f899f82760ecd8323223ff5", size = 304166, upload-time = "2026-01-10T11:17:45.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/54/27a7040a360019a4602343e3c98c0c0a140f382186002c01e1992fd21837/zstd-1.5.7.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:fbc49a57188184931d5e3c9f1133cad7eea5a370a9e9418fb8122d58c14340a5", size = 1540288, upload-time = "2026-01-08T17:50:26.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/93/4a4d4edd1b2e809e0ebbb16000404bdcc9a09743c04ee1661442c9581b75/zstd-1.5.7.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d121d3e63722819e1fe5effbcd9628d8a7cfea0cddabcc5bb37ea861a6a83424", size = 1619134, upload-time = "2026-01-08T17:50:32.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/6b/cd6f0a7f4f0d98e4110aa77763cf3e85f594d983ea9ca3d64cc0cee10684/zstd-1.5.7.3-cp311-cp311-manylinux_2_4_i686.whl", hash = "sha256:621f2e7ca8e9eb52a83eb9c91ec3cd283d87591bf75cc658de486b65f44742c7", size = 300166, upload-time = "2026-01-10T11:12:27.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/3f/c717e0d15127d04b7fa58ba9b4c56e8b88b803048b9766cd9d158dbb22ea/zstd-1.5.7.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:c1950fcae690ba32d0f31702b335c548fb42547821565925e48576afdad774a5", size = 1525776, upload-time = "2026-01-08T17:50:35.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/a2/1813cd787d1a2f9ab8e8a90d28dcbc8e8098997dd04de38897ea8e75dd08/zstd-1.5.7.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bac4f0d03da69115878bedbfa03c4a3f64364e8396b432028c4ce0f05141a0fb", size = 2096057, upload-time = "2026-01-08T17:50:33.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/ce/f5a3c7c12de458dd9ce15c484d627fe5412b60c155da23dacb5fcf08d9d5/zstd-1.5.7.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:da0ab134b7fd28023dedf013751ca850de300a090eb11f689d2a1c178c87d9dc", size = 2132659, upload-time = "2026-01-08T17:50:29.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/66/151f9546498bfd8971a0b6ad67d87c26d7a0df17d57f724da674f3778666/zstd-1.5.7.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b9923175842ee8f7602ec9cc578f5fc396896f0e8460d3ac9a5adc3cea77244e", size = 2124811, upload-time = "2026-01-08T17:50:37.612Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/34/4d2dbb36cb2373d3f115c047cb901b64f89de0703d10779da39de9453812/zstd-1.5.7.3-cp311-cp311-win32.whl", hash = "sha256:0612b604948d7b58aecc6788c7ceb53c5f21d94a155bb6ea9bd0f54ffa43725d", size = 150363, upload-time = "2026-01-08T17:11:02.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/de/f53687e0dd8c0d0ebfaed9ae88f6a96a1a0388ae7424b469e74bb17ac57d/zstd-1.5.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:5b7f8c81b2bd3b62c0345242247d484cafa4b518d59d18619813d9225af5c5c3", size = 167577, upload-time = "2026-01-08T17:11:03.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/58/d4a6a902e229e953ed273fe9b78587ed31f57567aa68d3e34af6056e42af/zstd-1.5.7.3-cp311-cp311-win_arm64.whl", hash = "sha256:ea112e3acd9e1765adca35df7b54ac75b36194290f64ea03a3a59664209c8527", size = 157238, upload-time = "2026-01-08T16:36:06.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/ed/5a3bf2e29dc56d4cc7619929bb51f0c758de6d02967cc73c5d8755a862c0/zstd-1.5.7.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01a39efb0eeab7cc45cb308618233b624b0840d5e16dcf85456b6cca0592f203", size = 268124, upload-time = "2026-01-08T16:29:57.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/1d/efc2074ac90af938e78f2ed4004639fe24f294d9086c5280f8d9a02b9897/zstd-1.5.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7a8e8838cf35fa3987bfe1958584cc22e1797efce8e155a63544b4144fc671f8", size = 230988, upload-time = "2026-01-08T16:29:55.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/52/178393b8d70e23fba67f42dfce4663e4e8a30867110168beb490a36d4639/zstd-1.5.7.3-cp312-cp312-manylinux_2_14_i686.whl", hash = "sha256:f3920ac1d1cc7e9f252f3e29f217fe3cd36f2191bb3dbcae826c29e189b7ad54", size = 300207, upload-time = "2026-01-10T11:26:58.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/7a/8dcd86a2efb2ed3f9dae39545a05d3c7ed26c7678330786ce4a44cd8b099/zstd-1.5.7.3-cp312-cp312-manylinux_2_14_x86_64.whl", hash = "sha256:143f9062953fb5590cbd47c1040d357336742c79696bf90b6d5b835279a68304", size = 304154, upload-time = "2026-01-10T11:17:40.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/ce/0c96905ab01ffe0e53a3cec8132123b82db26bd583a71608029bcc789ebc/zstd-1.5.7.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d1fd8647e47e1f21b345e192f1a279e925678c23dad8236b547d04456cd699", size = 2162222, upload-time = "2026-01-08T18:02:22.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c4/db4807d6a68b4628c74fd379de7e3c67ec34f19a2a80ac246b3837cde6cb/zstd-1.5.7.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1538db419afa62773cf534fc7f3009ff59ecf55ecee4e889587ac2ef0010ed8", size = 2201732, upload-time = "2026-01-08T18:02:20.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/99/c19a3c0f5580ff9c33a74f06d98d6060ed1fa6bd09b55aed9be852ec191f/zstd-1.5.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5efd16adb092e2a547a7d51cfdaf6fd5680528227684c5bafc7669ab4a55f41", size = 2096459, upload-time = "2026-01-08T18:02:25.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/fd/02eac30419475dbe50212c119043a2d0698a0cbc756da85fd3fd9abddf42/zstd-1.5.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:39b3438e64637d80a5b1860526903b92020acb9bae9ceb5adffd9838c1441328", size = 2125442, upload-time = "2026-01-08T18:02:17.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/43/3a16ff0a8c913bb9825379db1bd533c75c57c2d2f31dd9111aa9b53711f4/zstd-1.5.7.3-cp312-cp312-win32.whl", hash = "sha256:cbf48c53461e224ffc2490cfe5120a1ff40d14c84d2b512c6d6d99fc91685cf3", size = 150367, upload-time = "2026-01-08T17:03:40.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/83/b85875d7428e63dfa9247e41d17fac611443c774f7892f8643bd4164a6b2/zstd-1.5.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:943a189910f2fea997462e3e4d7fbf727a06d231ef801ebee557b1c87568981c", size = 167604, upload-time = "2026-01-08T17:03:41.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/42/cf291e26804de2f55500cdac93f5e9fa6267cf315def8aa402529bae3a87/zstd-1.5.7.3-cp312-cp312-win_arm64.whl", hash = "sha256:85c4d508f8109afa7c51c4960626c3325af2cf1e442c6c36ebfea15d04757e3f", size = 157241, upload-time = "2026-01-08T16:47:34.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/b8/d13d584867d5eb1bc607877a870858e02a256d4706a4274e475413a000aa/zstd-1.5.7.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:76c49ea969bc08389ea59155cea7c5dea224522ffc62f443f3c0a915f5fd184d", size = 260025, upload-time = "2026-01-08T16:57:45.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/a1/1e5faf75bedfd2bfccfb83e18736b115bed6e348504bd21800cd8f30dcea/zstd-1.5.7.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b1a638ff3dfce8f4cb1203c662fb5606dd99b4a62c5ddc4c406d2d1326bcfdd", size = 221038, upload-time = "2026-01-08T17:16:32.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/2c/0fe74d8b2029eef8000bc71aac5b3e5b55d00581238711cf627814183ea3/zstd-1.5.7.3-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5e96a5cb100a0edc162935227f2d9784b1031ce4a8a83e96e66eae2673c10143", size = 326792, upload-time = "2026-01-08T16:57:35.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/e0/2c7f081f3524f872128ff31bea2acb6b21cb1dacccef920eb6a1a77a87c6/zstd-1.5.7.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bda0bbf3a9553720cd33f1f85940a259656c7ffba4be717ff82b7f062052188", size = 322283, upload-time = "2026-01-08T16:57:36.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/a7/3bebfcc18d66b90bc7b506a61b2ff4af5ee1b0b16e784ea644afa06241c5/zstd-1.5.7.3-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac36e4022422f6e49b3f07bdbb8a964fd348223d3dc9c82ad5398a4f0432a719", size = 311553, upload-time = "2026-01-08T16:57:38.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/75/8a791cae2c98e5e44a158e15db50d21b7ec0b37aeaffa68d151bc8ffb6d6/zstd-1.5.7.3-pp311-pypy311_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:fa4d760a220541b18ce732a3a2cf7547ea05afc76d05b3b39edebfeb721f6079", size = 317071, upload-time = "2026-01-08T16:36:07.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/25/b6624e6b08d515242154436c9d06fb20b790d300ac82e84f3c4c133e25e1/zstd-1.5.7.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a69e60146bf8aaa6a0e6c9a94a7c5f3133d68091e2e5c5a3c5ababf71fd5ec7a", size = 167654, upload-time = "2026-01-08T17:00:56.667Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "1.5.7.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/78/9a476e09c825304df47b98be80d1ffe223733b03550af71325415028f615/zstd-1.5.7.2.tar.gz", hash = "sha256:6d8684c69009be49e1b18ec251a5eb0d7e24f93624990a8a124a1da66a92fc8a", size = 670481, upload-time = "2025-06-23T12:36:08.131Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/2a/0885f6f1921ec1ef4a8f8ab29ab0a335cc867abe4c7aaa4e5031435a32a5/zstd-1.5.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f799c1e9900ad77e7a3d994b9b5146d7cfd1cbd1b61c3db53a697bf21ffcc57b", size = 269702, upload-time = "2025-06-23T12:50:11.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/e6/629cf6b77e47fc7149f5724fb4853c48edcdeb10d8c64e391d7026cb10e1/zstd-1.5.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ff4c667f29101566a7b71f06bbd677a63192818396003354131f586383db042", size = 228145, upload-time = "2025-06-23T12:50:10.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/b8/9ddefd4670bfe9328ca6657ad335eb8d9c657466247e234a579818b6b0b9/zstd-1.5.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8526a32fa9f67b07fd09e62474e345f8ca1daf3e37a41137643d45bd1bc90773", size = 1536530, upload-time = "2025-06-23T13:51:38.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/6a/1bb836c18760dc1e28ca7a9706016e482ebdea633b980d8505dbb65e18f8/zstd-1.5.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:2cec2472760d48a7a3445beaba509d3f7850e200fed65db15a1a66e315baec6a", size = 1616141, upload-time = "2025-06-23T13:51:34.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/7a/bb6c6e2cb2a066e347dc27d45d5205058b69d6c8b8d4ae2ee7d6b91c64a5/zstd-1.5.7.2-cp311-cp311-manylinux_2_4_i686.whl", hash = "sha256:a200c479ee1bb661bc45518e016a1fdc215a1d8f7e4bf6c7de0af254976cfdf6", size = 322188, upload-time = "2025-06-23T13:01:48.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/4f/cf0669c8a89fdcc91814bf92bd05cc363d5d12a79b656418c0add6f2d266/zstd-1.5.7.2-cp311-cp311-manylinux_2_4_x86_64.whl", hash = "sha256:f5d159e57a13147aa8293c0f14803a75e9039fd8afdf6cf1c8c2289fb4d2333a", size = 302736, upload-time = "2025-06-23T13:05:33.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/bc/e5f8b7f61826323e39e099db1eb5c0e09b18315df1b1ff778f7ae9aadcac/zstd-1.5.7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:7206934a2bd390080e972a1fed5a897e184dfd71dbb54e978dc11c6b295e1806", size = 1522687, upload-time = "2025-06-23T13:51:35.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/8c/7660a949a020ac9d02b3166a25dd1c12144572d77b11ae92a31d341016da/zstd-1.5.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e0027b20f296d1c9a8e85b8436834cf46560240a29d623aa8eaa8911832eb58", size = 2098794, upload-time = "2025-06-23T13:51:37.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/b2/730c811a78d670104d40c7f08cc8092577cdff870cba42b3158f20fceb57/zstd-1.5.7.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d6b17e5581dd1a13437079bd62838d2635db8eb8aca9c0e9251faa5d4d40a6d7", size = 2112266, upload-time = "2025-06-23T13:51:31.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/74/2c16e1632094db36c8920d4c13b8e2e843024d548ae26888c2d22af6a676/zstd-1.5.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b13285c99cc710f60dd270785ec75233018870a1831f5655d862745470a0ca29", size = 2109465, upload-time = "2025-06-23T13:51:32.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/6e/b9c9a834769d96cab2122da1be8c8c700d3f76be796d2b7516e85d2eca0e/zstd-1.5.7.2-cp311-cp311-win32.whl", hash = "sha256:cdb5ec80da299f63f8aeccec0bff3247e96252d4c8442876363ff1b438d8049b", size = 149448, upload-time = "2025-06-23T13:06:21.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/b7/fc22ad6292a32d7676ab815de3a23573beac3679e8abd9914288d1496ceb/zstd-1.5.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:4f6861c8edceb25fda37cdaf422fc5f15dcc88ced37c6a5b3c9011eda51aa218", size = 166592, upload-time = "2025-06-23T13:06:22.126Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/14/096bb77f3e5ef525b452cd6294da33de7f8a8c9647ba78293378fbb0a7ce/zstd-1.5.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ebe3e60dbace52525fa7aa604479e231dc3e4fcc76d0b4c54d8abce5e58734", size = 269408, upload-time = "2025-06-23T13:11:46.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/b8/2bc2590a34c733ea0570f366e6ad7d889d05c7825bd3ccab01f36ece71c6/zstd-1.5.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ef201b6f7d3a6751d85cc52f9e6198d4d870e83d490172016b64a6dd654a9583", size = 228188, upload-time = "2025-06-23T13:11:47.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/80/6252de3a70cfd7767718ad476893f1c7dc129f942cc7ed0322e3137c03d9/zstd-1.5.7.2-cp312-cp312-manylinux_2_14_x86_64.whl", hash = "sha256:ac7bdfedda51b1fcdcf0ab69267d01256fc97ddf666ce894fde0fae9f3630eac", size = 302720, upload-time = "2025-06-23T12:40:11.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/b6/af908387814b99172d3aea6aeb24b19583aadfa45f6021e5e2a0d6d8e99a/zstd-1.5.7.2-cp312-cp312-manylinux_2_4_i686.whl", hash = "sha256:b835405cc4080b378e45029f2fe500e408d1eaedfba7dd7402aba27af16955f9", size = 322237, upload-time = "2025-06-23T13:17:35.482Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/d7/ab9142e002a7eaa451cb4bb37a74c390c489ba8ae75ade543840496eda04/zstd-1.5.7.2-cp312-cp312-win32.whl", hash = "sha256:e4cf97bb97ed6dbb62d139d68fd42fa1af51fd26fd178c501f7b62040e897c50", size = 149453, upload-time = "2025-06-23T13:13:02.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/c7/c182ea7bc283f591e3f3c5f0f239e7a92c9bc1f626642ae2c4dfbe51d6f2/zstd-1.5.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:55e2edc4560a5cf8ee9908595e90a15b1f47536ea9aad4b2889f0e6165890a38", size = 166628, upload-time = "2025-06-23T13:13:03.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/c9/a6495a7bf168a78f0a0c01d61d830ebfb401315a64fd1ae8d725c458114c/zstd-1.5.7.2-pp311-pypy311_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:5fb2ff5718fe89181223c23ce7308bd0b4a427239379e2566294da805d8df68a", size = 315542, upload-time = "2025-06-23T12:39:27.598Z" },
|
||||
]
|
||||
|
||||
@@ -4,6 +4,117 @@ description: "New features and improvements in each Prowler release"
|
||||
rss: true
|
||||
---
|
||||
|
||||
<Update label="v5.39.0" description="August 13, 2026">
|
||||
### 🤖 Lighthouse AI — Finding Skills
|
||||
|
||||
<Note>
|
||||
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
|
||||
</Note>
|
||||
|
||||
Lighthouse AI now embeds a Skills menu on every finding, answering the questions an analyst actually asks. **Contextual Fix** produces the fix for the finding, **Triage Decision** judges whether it is real and closes it out when it is not, and **Systemic Scope** determines whether the problem is a one-off or everywhere. A free-form "Ask Lighthouse anything" prompt sits in the same menu, and each run shows its progress and offers follow-up actions such as creating a Jira issue or muting the finding.
|
||||
|
||||

|
||||
|
||||
Read more in the [Lighthouse AI documentation](https://docs.prowler.com/getting-started/products/prowler-cloud-lighthouse).
|
||||
|
||||
### ☁️ Azure Management Group Onboarding
|
||||
|
||||
<Note>
|
||||
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
|
||||
</Note>
|
||||
|
||||
Azure subscriptions no longer onboard one at a time. Choose "Add Multiple Subscriptions With Azure Management Group" in the add-provider wizard, enter the Microsoft Entra tenant ID, and authenticate once with a single tenant-wide service principal: Prowler discovers the entire management-group hierarchy under the tenant root, lets you select the subscriptions to onboard, and creates their providers with the management-group structure preserved. Azure now matches the one-step onboarding that AWS Organizations and GCP organizations already have.
|
||||
|
||||

|
||||
|
||||
Read more in the [Azure Management Groups documentation](https://docs.prowler.com/user-guide/tutorials/prowler-cloud-azure-management-groups).
|
||||
|
||||
### ✅ Findings Triage — Verify MANUAL Findings as PASS
|
||||
|
||||
<Note>
|
||||
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
|
||||
</Note>
|
||||
|
||||
Checks that require human judgment report `MANUAL` findings. For these findings, and only for them, the triage status selector now offers **Resolved**: choosing it asks for the required written evidence and verifies the finding as passing. The finding then reports an effective `PASS` while preserving the raw `MANUAL` scan result, across findings, finding groups, compliance reports, and scans, with the attestation's author, evidence, and validity always visible. Attestations expire automatically after 90 days, or as soon as a new scan reports a real failure, returning the finding to the review queue.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Read more in the [Findings Triage documentation](https://docs.prowler.com/user-guide/tutorials/prowler-app-findings-triage#verify-a-manual-finding-as-pass).
|
||||
|
||||
### ☁️ Prowler Cloud MCP — Organizations Management and Grouped Jira Dispatch
|
||||
|
||||
<Note>
|
||||
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
|
||||
</Note>
|
||||
|
||||
The hosted Prowler Cloud MCP server adds eight organization tools, so an agent can onboard and manage entire cloud organizations end to end: create the organization, discover its accounts, subscriptions, and projects, apply the selection, and manage the resulting providers. The tools cover AWS Organizations, GCP organizations, and Azure tenant root management groups, and they are available to Lighthouse AI.
|
||||
|
||||
`prowler_send_findings_to_jira` also gains Cloud-only dispatch capabilities: select failed findings by check IDs against the latest completed scan, and send them in grouped mode, one Jira work item per check listing up to 50 affected resources, with per-group error reporting.
|
||||
|
||||
Read more in the [Prowler MCP tools documentation](https://docs.prowler.com/getting-started/basic-usage/prowler-mcp-tools) and its [Jira operations reference](https://docs.prowler.com/getting-started/basic-usage/prowler-mcp-tools#jira-operations).
|
||||
|
||||
### 🕸️ Attack Paths — Grouped Graph with Outcome Destinations
|
||||
|
||||
<Note>
|
||||
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
|
||||
</Note>
|
||||
|
||||
The Attack Paths graph now reads from source to destination. Resources of the same class collapse into a single expandable node with a count, clicking reveals its members, and every path terminates in an explicit outcome node naming the destination impact: code execution, privilege escalation, public exposure, or resource inventory. The per-account hub node is gone, and the clicked resource stays highlighted while its findings are expanded.
|
||||
|
||||

|
||||
|
||||
Explore the full Attack Paths query catalog at [Prowler Hub](https://hub.prowler.com/attack-paths).
|
||||
|
||||
Read more in the [Attack Paths documentation](https://docs.prowler.com/user-guide/tutorials/prowler-app-attack-paths).
|
||||
|
||||
### 📚 New Compliance Framework — CMMC 2.0
|
||||
|
||||
The Cybersecurity Maturity Model Certification (CMMC) is the certification the US Department of Defense requires from contractors and suppliers that handle federal contract data. Prowler now includes CMMC 2.0 as a universal framework with all 149 requirements defined by the CMMC Program rule (32 CFR Part 170), organized in its three levels:
|
||||
|
||||
- **Level 1 (Foundational):** 15 requirements for the basic safeguarding of Federal Contract Information, from FAR 52.204-21.
|
||||
- **Level 2 (Advanced):** 110 requirements from NIST SP 800-171 Rev 2, protecting Controlled Unclassified Information.
|
||||
- **Level 3 (Expert):** 24 enhanced requirements from NIST SP 800-172 for the most sensitive programs.
|
||||
|
||||
Requirements map to Prowler checks across AWS, Azure, GCP, Alibaba Cloud, Oracle Cloud, and Microsoft 365, so one framework reports the compliance posture of the whole estate.
|
||||
|
||||
Read more in the [Compliance documentation](https://docs.prowler.com/user-guide/compliance/tutorials/compliance).
|
||||
|
||||
### 🔍 Checks
|
||||
|
||||
#### Microsoft 365
|
||||
|
||||
Twenty new Entra ID checks expand the coverage of CIS Microsoft 365 Foundations Benchmark v7.0.0:
|
||||
|
||||
- **Password protection:** custom banned password list, on-premises enforcement, and lockout threshold and duration.
|
||||
- **Default user permissions:** security group and Microsoft 365 group creation restricted, and guest invitations limited to allowed domains.
|
||||
- **Conditional Access:** high and medium sign-in risk blocked, authentication transfer blocked, untrusted locations blocked, trusted named locations defined, sign-in frequency enforced, and token protection enforced.
|
||||
- **Sessions and authentication methods:** idle session timeout configured, email one-time passcodes disabled, and Microsoft Authenticator context shown.
|
||||
- **PIM and access reviews:** approval required to activate the Global Administrator and Privileged Role Administrator roles, and access reviews configured for guest users and privileged roles.
|
||||
|
||||
Explore all Microsoft 365 checks at [Prowler Hub](https://hub.prowler.com/check?provider=m365).
|
||||
|
||||
#### AWS
|
||||
|
||||
Two new checks detect hardcoded secrets:
|
||||
|
||||
- `batch_job_definition_no_secrets` scans Batch job definition environment variables and command parameters. Thanks to @praneetrajv!
|
||||
- `awslambda_layer_no_secrets_in_content` scans Lambda layer package content. Thanks to @ganiganesh25!
|
||||
|
||||
Explore all AWS checks at [Prowler Hub](https://hub.prowler.com/check?provider=aws).
|
||||
|
||||
### 🙌 External Contributors
|
||||
|
||||
Thank you to our community contributors for this release!
|
||||
|
||||
- @praneetrajv: AWS `batch_job_definition_no_secrets` check ([#12117](https://github.com/prowler-cloud/prowler/pull/12117))
|
||||
- @ganiganesh25: AWS `awslambda_layer_no_secrets_in_content` check ([#12233](https://github.com/prowler-cloud/prowler/pull/12233))
|
||||
- @andoniaf: GitHub `organization_repository_creation_limited` now reports low severity when repository creation is limited to private or internal visibility ([#12164](https://github.com/prowler-cloud/prowler/pull/12164))
|
||||
|
||||
See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.39.0) for the complete list of changes.
|
||||
</Update>
|
||||
|
||||
<Update label="v5.38.0" description="August 6, 2026">
|
||||
### 📌 Compliance Watchlist
|
||||
|
||||
|
||||
@@ -224,9 +224,9 @@ Each check **must** populate the report with a unique identifier for the audited
|
||||
- `resource_name`: Description of the configuration (e.g., "SharePoint Settings")
|
||||
- GitHub
|
||||
- Resource ID — `report.resource_id`.
|
||||
- The ID of the Github resource. This is a system-generated integer that uniquely identifies the resource within the Github platform.
|
||||
- The ID of the GitHub resource. This is a system-generated integer that uniquely identifies the resource within the GitHub platform.
|
||||
- Resource Name — `report.resource_name`.
|
||||
- The name of the Github resource. In the case of a repository, this is just the repository name. For full repository names use the resource `full_name`.
|
||||
- The name of the GitHub resource. In the case of a repository, this is just the repository name. For full repository names use the resource `full_name`.
|
||||
|
||||
### Configurable Checks in Prowler
|
||||
|
||||
|
||||
@@ -120,14 +120,14 @@ class NewFeatureTools(BaseTool):
|
||||
|
||||
Returns complete feature details including configuration and metadata.
|
||||
"""
|
||||
try:
|
||||
response = await self.api_client.get(f"/api/v1/features/{feature_id}")
|
||||
return DetailedFeature.from_api_response(response["data"]).model_dump()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to get feature {feature_id}: {e}")
|
||||
return {"error": str(e), "status": "failed"}
|
||||
response = await self.api_client.get(f"/api/v1/features/{feature_id}")
|
||||
return DetailedFeature.from_api_response(response["data"]).model_dump()
|
||||
```
|
||||
|
||||
There is no `try`/`except` here on purpose. A failed request raises, and
|
||||
[Error Handling](#error-handling) explains what turns that raise into a message
|
||||
the agent can act on.
|
||||
|
||||
### Step 2: Create the Models
|
||||
|
||||
Create corresponding models in `prowler_app/models/`:
|
||||
@@ -369,18 +369,62 @@ async def search_items(self, status: str = Field(...)) -> dict:
|
||||
|
||||
### Error Handling
|
||||
|
||||
Return structured error responses instead of raising exceptions:
|
||||
**Raise, never return.** A returned `{"error": ...}` dict is reported to the
|
||||
client as `isError: false` -- a *successful* tool call whose payload happens to
|
||||
mention a failure. Clients and models read that as success. A raised exception
|
||||
becomes a spec-correct tool execution error instead.
|
||||
|
||||
The common case therefore needs no handler at all:
|
||||
|
||||
```python
|
||||
async def get_item(self, item_id: str) -> dict:
|
||||
try:
|
||||
response = await self.api_client.get(f"/api/v1/items/{item_id}")
|
||||
return DetailedItem.from_api_response(response["data"]).model_dump()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to get item {item_id}: {e}")
|
||||
return {"error": str(e), "status": "failed"}
|
||||
response = await self.api_client.get(f"/api/v1/items/{item_id}")
|
||||
return DetailedItem.from_api_response(response["data"]).model_dump()
|
||||
```
|
||||
|
||||
`prowler_mcp_server/lib/errors.py` classifies the failures every tool shares --
|
||||
a rejected credential, a missing permission, a rate limit, an outage, an
|
||||
unreachable API, a bad argument -- and gives each one a message that says what
|
||||
went wrong and what to do about it. Anything it does not recognise is masked,
|
||||
because `mask_error_details=True` is set on every sub-server and upstream
|
||||
response bodies must never be replayed into a model's context.
|
||||
|
||||
Three ways to raise, in the order to reach for them:
|
||||
|
||||
```python
|
||||
from fastmcp.exceptions import ToolError
|
||||
|
||||
from prowler_mcp_server.lib.errors import InvalidArgument
|
||||
|
||||
# 1. An argument this server rejected before any request went out. The message
|
||||
# is repeated to the agent verbatim, so write it for one to read.
|
||||
if not 1 <= page_size <= 1000:
|
||||
raise InvalidArgument("page_size must be between 1 and 1000.")
|
||||
|
||||
# 2. A request the API answered or never answered: let it propagate untouched.
|
||||
# `ProwlerAPIError` and `ProwlerAPIUnreachable` are what the classifier keys
|
||||
# on, and the second one is what stops a retry from duplicating a write.
|
||||
response = await self.api_client.get(f"/api/v1/items/{item_id}")
|
||||
data = response["data"]
|
||||
|
||||
# 3. A sentence the classifier cannot know -- a resource name, a precondition,
|
||||
# the next tool to call. NOTE the absent `from` clause: it is what marks the
|
||||
# message as already final. With `from e` the classifier would replace it.
|
||||
if not data:
|
||||
raise ToolError(
|
||||
f"No item with the ID {item_id!r} exists. Use prowler_list_items to "
|
||||
"find a valid one."
|
||||
)
|
||||
```
|
||||
|
||||
The one thing that still *returns* rather than raises is a write whose outcome is
|
||||
genuinely unknown. `prowler_send_findings_to_jira` is the worked example: work
|
||||
items are created one at a time and Prowler cannot delete them, so a dispatch
|
||||
that stopped halfway answers with a result object carrying
|
||||
`safe_to_retry: false`. "This may have been applied" is a fact about the world,
|
||||
not an error, and squashing it into one loses the only thing that stops a retry
|
||||
from duplicating the write.
|
||||
|
||||
### Parameter Descriptions
|
||||
|
||||
Use Pydantic `Field()` with clear descriptions. This also helps LLMs understand
|
||||
|
||||
@@ -107,7 +107,7 @@ Once you have decided the provider you want or need to add to Prowler, the next
|
||||
- **SDK Providers**: Low complexity. You have mature examples like AWS, Azure, GCP, Kubernetes, etc. that you can leverage to implement your provider.
|
||||
- **API Providers**: Medium complexity. You need to implement the authentication and session management, and the API calls to the provider. You now have NHN and MongoDB Atlas as example to follow.
|
||||
- **Tool/Wrapper Providers**: High complexity. You need to implement the argument/output mapping to the provider and handle problems that the tool/wrapper may have. You now have IAC and the PowerShell wrapper as example to follow.
|
||||
- **Hybrid Providers**: High complexity. You need to "customize" your provider, mixing the other types of providers in order to achieve the desired result. You have M365 (msgraph SDK + PowerShell wrapper) and Github (PyGithub SDK + graphql API requests) as examples.
|
||||
- **Hybrid Providers**: High complexity. You need to "customize" your provider, mixing the other types of providers to achieve the desired result. You have M365 (msgraph SDK + PowerShell wrapper) and GitHub (PyGithub SDK + graphql API requests) as examples.
|
||||
|
||||
### Determining Regional vs Non-Regional Architecture
|
||||
|
||||
@@ -814,7 +814,7 @@ class YourProviderMutelist(Mutelist):
|
||||
Region management is essential for cloud providers that operate across multiple geographic locations. This component handles region validation and provides region-specific functionality.
|
||||
|
||||
<Note>
|
||||
Regions are optional, only if the provider has regions, for example Github does not have regions, but AWS does.
|
||||
Regions are optional, only if the provider has regions, for example GitHub does not have regions, but AWS does.
|
||||
</Note>
|
||||
|
||||
**File:** `prowler/providers/<provider_name>/lib/regions/<provider_name>_regions.py`
|
||||
@@ -1773,7 +1773,7 @@ The implementation of the mutelist is the same as the [SDK providers](#step-5-im
|
||||
Region management is essential for cloud providers that operate across multiple geographic locations. This component handles region validation and provides region-specific functionality.
|
||||
|
||||
<Note>
|
||||
Regions are optional, only if the provider has regions, for example Github does not have regions, but AWS does.
|
||||
Regions are optional, only if the provider has regions, for example GitHub does not have regions, but AWS does.
|
||||
</Note>
|
||||
|
||||
**File:** `prowler/providers/<provider_name>/lib/regions/<provider_name>_regions.py`
|
||||
|
||||
@@ -525,7 +525,7 @@ with mock.patch(
|
||||
):
|
||||
```
|
||||
|
||||
As demonstrated in the code above, mocking both the AWS audit information and all utilized services is mandatory for proper test execution.
|
||||
As demonstrated in the code above, mocking both the AWS audit information and all used services is mandatory for proper test execution.
|
||||
|
||||
#### Patching vs. Importing
|
||||
|
||||
|
||||
@@ -182,7 +182,8 @@
|
||||
"pages": [
|
||||
"user-guide/tutorials/prowler-app-s3-integration",
|
||||
"user-guide/tutorials/prowler-app-security-hub-integration",
|
||||
"user-guide/tutorials/prowler-app-jira-integration"
|
||||
"user-guide/tutorials/prowler-app-jira-integration",
|
||||
"user-guide/tutorials/prowler-app-slack-integration"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -301,6 +302,15 @@
|
||||
{
|
||||
"group": "Providers",
|
||||
"pages": [
|
||||
{
|
||||
"group": "Organizations",
|
||||
"pages": [
|
||||
"user-guide/organizations",
|
||||
"user-guide/providers/aws/organizations",
|
||||
"user-guide/providers/gcp/organization",
|
||||
"user-guide/providers/azure/management-groups"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Alibaba Cloud",
|
||||
"pages": [
|
||||
@@ -330,6 +340,7 @@
|
||||
"user-guide/providers/azure/getting-started-azure",
|
||||
"user-guide/providers/azure/authentication",
|
||||
"user-guide/providers/azure/use-non-default-cloud",
|
||||
"user-guide/providers/azure/management-groups",
|
||||
"user-guide/providers/azure/subscriptions",
|
||||
"user-guide/providers/azure/resource-groups",
|
||||
"user-guide/providers/azure/create-prowler-service-principal"
|
||||
|
||||
@@ -22,7 +22,7 @@ Google Cloud Security Command Center (Cloud SCC) is a centralized security and r
|
||||
|
||||
- **GCP-Centric:** While Cloud SCC is powerful within the GCP ecosystem, it is primarily focused on GCP and does not natively extend to multi-cloud environments without additional tools or connectors.
|
||||
|
||||
- **Cost Considerations:** As a managed service within GCP, costs can scale with the amount of data ingested and the complexity of the environment, especially as additional features or higher volumes of data are utilized.
|
||||
- **Cost Considerations:** As a managed service within GCP, costs can scale with the amount of data ingested and the complexity of the environment, especially as additional features or higher volumes of data are used.
|
||||
|
||||
- **Dependency on GCP Services:** Cloud SCC's capabilities depend on other GCP services being enabled, such as Security Health Analytics and Web Security Scanner, which may increase overall complexity and cost.
|
||||
|
||||
|
||||
@@ -128,12 +128,12 @@ To update the environment file:
|
||||
Edit the `.env` file and change version values:
|
||||
|
||||
```env
|
||||
PROWLER_UI_VERSION="5.38.0"
|
||||
PROWLER_API_VERSION="5.38.0"
|
||||
PROWLER_UI_VERSION="5.39.0"
|
||||
PROWLER_API_VERSION="5.39.0"
|
||||
```
|
||||
|
||||
<Note>
|
||||
You can find the latest versions of Prowler Local Server in the [Releases Github section](https://github.com/prowler-cloud/prowler/releases) or in the [Container Versions](#container-versions) section of this documentation.
|
||||
You can find the latest versions of Prowler Local Server in the [Releases GitHub section](https://github.com/prowler-cloud/prowler/releases) or in the [Container Versions](#container-versions) section of this documentation.
|
||||
</Note>
|
||||
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ To install Prowler as a Python package, use `Python >= 3.10, <= 3.13`. Prowler i
|
||||
_Requirements_:
|
||||
|
||||
* Have `docker` installed: https://docs.docker.com/get-docker/.
|
||||
* In the command below, change `-v` to your local directory path in order to access the reports.
|
||||
* In the command below, change `-v` to your local directory path to access the reports.
|
||||
* AWS, GCP, Azure and/or Kubernetes credentials
|
||||
|
||||
_Commands_:
|
||||
|
||||
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 563 KiB |
|
After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 269 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 93 KiB |
@@ -92,6 +92,7 @@ li[id="/user-guide/tutorials/prowler-alerts"] a > div > div > span:first-child::
|
||||
li[id="/user-guide/tutorials/prowler-app-attack-paths-active-queries"] a > div > div > span:first-child::after,
|
||||
li[id="/user-guide/tutorials/prowler-app-findings-triage"] a > div > div > span:first-child::after,
|
||||
li[id="/user-guide/tutorials/prowler-app-scan-configuration"] a > div > div > span:first-child::after,
|
||||
li[id="/user-guide/tutorials/prowler-app-slack-integration"] a > div > div > span:first-child::after,
|
||||
li[id="/user-guide/tutorials/prowler-cloud-aws-organizations"] a > div > div > span:first-child::after,
|
||||
li[id="/user-guide/tutorials/prowler-cloud-azure-management-groups"] a > div > div > span:first-child::after,
|
||||
li[id="/user-guide/tutorials/prowler-cloud-gcp-organizations"] a > div > div > span:first-child::after,
|
||||
|
||||
@@ -495,7 +495,7 @@ aws:
|
||||
|
||||
# AWS CloudTrail Configuration
|
||||
# aws.cloudtrail_threat_detection_privilege_escalation
|
||||
threat_detection_privilege_escalation_threshold: 0.2 # Percentage of actions found to decide if it is an privilege_escalation attack event, by default is 0.2 (20%)
|
||||
threat_detection_privilege_escalation_threshold: 0.2 # Percentage of actions found to decide if it is a privilege_escalation attack event, by default is 0.2 (20%)
|
||||
threat_detection_privilege_escalation_minutes: 1440 # Past minutes to search from now for privilege_escalation attacks, by default is 1440 minutes (24 hours)
|
||||
threat_detection_privilege_escalation_actions:
|
||||
[
|
||||
|
||||
@@ -6,7 +6,7 @@ In certain organizations, the severity of specific checks might differ from the
|
||||
|
||||
The custom metadata option offers a means to override default metadata set by Prowler.
|
||||
|
||||
You can utilize `--custom-checks-metadata-file` followed by the path to your custom checks metadata YAML file.
|
||||
You can use `--custom-checks-metadata-file` followed by the path to your custom checks metadata YAML file.
|
||||
|
||||
## Available Fields
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ def get_table(data):
|
||||
|
||||
## S3 Integration
|
||||
|
||||
If you are using Prowler Cloud with the S3 integration or that integration from Prowler CLI and you want to use your data from your S3 bucket, you can run the following command in order to load the dashboard with the new files:
|
||||
If you are using Prowler Cloud with the S3 integration or that integration from Prowler CLI and you want to use your data from your S3 bucket, you can run the following command to load the dashboard with the new files:
|
||||
|
||||
```sh
|
||||
aws s3 cp s3://<your-bucket>/output/csv ./output --recursive
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: 'Organizations Across Cloud Providers'
|
||||
description: 'Understand organization hierarchies and onboarding across AWS, Google Cloud, and Azure'
|
||||
---
|
||||
|
||||
Cloud providers use organization-level hierarchies to group accounts, projects, or subscriptions and apply access and governance consistently. Prowler uses these hierarchies to discover cloud targets and help configure multi-account or multi-project scanning.
|
||||
|
||||
This guide explains the shared lifecycle and the differences between AWS Organizations, Google Cloud organizations, and Azure Management Groups. Use the provider-specific guides for commands, permissions, and limitations.
|
||||
|
||||
## Organization Lifecycle
|
||||
|
||||
Organization-level onboarding generally follows these steps:
|
||||
|
||||
1. **Identify the hierarchy:** Locate the organization, management account, management group, folder, organizational unit, or equivalent parent node in the cloud provider.
|
||||
2. **Grant access:** Assign the provider permissions required to enumerate the hierarchy and read the resources that Prowler scans.
|
||||
3. **Discover members:** Use Prowler to retrieve accounts, projects, or subscriptions under the selected hierarchy.
|
||||
4. **Select scan targets:** Choose the cloud targets to connect or scan. Discovery does not necessarily make every discovered target a Prowler provider.
|
||||
5. **Test access:** Confirm that Prowler can authenticate to each selected target and read its resources.
|
||||
6. **Scan and maintain:** Run scans, review findings, and repeat discovery when the provider hierarchy changes.
|
||||
|
||||
<Note>
|
||||
Organization membership changes are not automatically synchronized in every Prowler workflow. Follow the provider-specific guide to learn when manual rediscovery is required.
|
||||
</Note>
|
||||
|
||||
## Capability Matrix
|
||||
|
||||
| Capability | AWS Organizations | Google Cloud organization | Azure Management Groups |
|
||||
| --- | --- | --- | --- |
|
||||
| Hierarchy members | AWS accounts grouped in organizational units (OUs) | Projects grouped in folders and nested folders | Subscriptions grouped in management groups |
|
||||
| Organization-level discovery | Supported through AWS Organizations APIs | Supported through the Cloud Asset API | Supported through Azure management-group and subscription APIs |
|
||||
| Primary scan target | AWS account | Google Cloud project | Azure subscription |
|
||||
| Common organization-level permission | IAM role in the management or delegated administrator account | Cloud Asset Viewer or Cloud Asset Owner at the organization node | Appropriate Azure role assignment at the management-group or subscription scope |
|
||||
| Provider-specific onboarding | AWS account discovery and optional StackSet role deployment | Project discovery under an organization ID | Subscription discovery under a management group; role assignments inherit to subscriptions |
|
||||
| Membership maintenance | Repeat the discovery flow when accounts are added or removed | Re-run organization discovery when projects or folders change | Refresh discovery when subscriptions move between management groups |
|
||||
|
||||
## Provider Guides
|
||||
|
||||
### AWS Organizations
|
||||
|
||||
The [AWS Organizations guide](/user-guide/providers/aws/organizations) covers account details, delegated administration, IAM roles, CloudFormation StackSets, and CLI scanning. For Prowler Cloud onboarding, see [AWS Organizations in Prowler Cloud](/user-guide/tutorials/prowler-cloud-aws-organizations).
|
||||
|
||||
### Google Cloud Organization
|
||||
|
||||
The [Google Cloud organization guide](/user-guide/providers/gcp/organization) covers scanning projects under an organization ID, organization-level permissions, and Cloud Asset API requirements. For Prowler Cloud onboarding, see [Google Cloud organizations in Prowler Cloud](/user-guide/tutorials/prowler-cloud-gcp-organizations).
|
||||
|
||||
### Azure Management Groups
|
||||
|
||||
The [Azure Management Groups guide](/user-guide/providers/azure/management-groups) covers hierarchy setup, role assignment, subscription scope, and Azure-specific limitations. For Prowler Cloud onboarding, see [Azure Management Groups in Prowler Cloud](/user-guide/tutorials/prowler-cloud-azure-management-groups).
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
The organization concepts in this guide refer only to cloud-provider resource hierarchies:
|
||||
|
||||
- **GitHub organizations** group repositories and GitHub resources. They are a separate provider concept and are not part of AWS, Google Cloud, or Azure organization discovery.
|
||||
- **MongoDB Atlas organizations** group Atlas projects and teams. They use a separate provider API and authentication model.
|
||||
- **Prowler Cloud organizations** are internal tenants that isolate providers, scans, findings, users, and permissions. They are not the same as a cloud-provider organization and do not replace one.
|
||||
|
||||
Choose the guide that matches the hierarchy being configured, then use the relevant Prowler Cloud or CLI workflow for the scan targets.
|
||||
@@ -64,7 +64,7 @@ This method grants permanent access and is the recommended setup for production
|
||||
|
||||
7. Click "Submit" to deploy the stack
|
||||
|
||||

|
||||

|
||||
</Tab>
|
||||
<Tab title="Terraform">
|
||||
To provision the scan role using Terraform:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: 'Scanning Multiple AWS Accounts with Prowler'
|
||||
---
|
||||
|
||||
Prowler enables security scanning across multiple AWS accounts by utilizing the [Assume Role feature](/user-guide/providers/aws/role-assumption) and [integration with AWS Organizations feature](/user-guide/providers/aws/organizations).
|
||||
Prowler enables security scanning across multiple AWS accounts by using the [Assume Role feature](/user-guide/providers/aws/role-assumption) and [integration with AWS Organizations feature](/user-guide/providers/aws/organizations).
|
||||
|
||||
This approach allows execution from a single account with permissions to assume roles in the target accounts.
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ See [AWS Organizations](/user-guide/tutorials/prowler-cloud-aws-organizations) i
|
||||
|
||||
Prowler can integrate with AWS Organizations to manage the visibility and onboarding of accounts centrally.
|
||||
|
||||
For the cross-provider organization lifecycle and capability comparison, see [Organizations Across Cloud Providers](/user-guide/organizations).
|
||||
|
||||
When trusted access is enabled with the Organization, Prowler can discover accounts as they are created and even automate deployment of the Prowler Scan IAM Role.
|
||||
|
||||
> ℹ️ Trusted access can be enabled in the Management Account from the AWS Console under **AWS Organizations → Settings → Trusted access for AWS CloudFormation StackSets**.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: 'Azure Management Groups in Prowler'
|
||||
---
|
||||
|
||||
Azure Management Groups provide a hierarchy above subscriptions. They allow Azure role assignments and governance policies to apply to multiple subscriptions through a shared scope.
|
||||
|
||||
For the cross-provider concepts and lifecycle, see [Organizations Across Cloud Providers](/user-guide/organizations).
|
||||
|
||||
## Azure Hierarchy
|
||||
|
||||
Azure resources are organized in the following order:
|
||||
|
||||
1. Tenant
|
||||
2. Management groups
|
||||
3. Subscriptions
|
||||
4. Resource groups
|
||||
5. Resources
|
||||
|
||||
Prowler scans Azure subscriptions. Management groups help organize those subscriptions and provide a scope where permissions can be assigned, but a management group is not itself a scan target.
|
||||
|
||||
## Create a Management Group
|
||||
|
||||
To create a management group, follow the [official Azure guide](https://learn.microsoft.com/en-us/azure/governance/management-groups/create-management-group-portal).
|
||||
|
||||

|
||||
|
||||
After creating the management group, add the subscriptions that Prowler should access and scan.
|
||||
|
||||

|
||||
|
||||
## Assign Roles
|
||||
|
||||
Assign the roles required by Prowler at the management-group scope instead of assigning them separately to every subscription. Role assignments at a management group can inherit to its child subscriptions, subject to Azure role-assignment and inheritance rules.
|
||||
|
||||
Use the [subscription scope permissions](/user-guide/providers/azure/authentication#subscription-scope-permissions) guide to identify the permissions required for scans. The identity used by Prowler must be able to read the management-group hierarchy and access each subscription selected for scanning.
|
||||
|
||||
## Subscription Scope
|
||||
|
||||
Management groups organize subscriptions, but Azure scan results remain scoped to individual subscriptions:
|
||||
|
||||
- Prowler Cloud scans one subscription per scan.
|
||||
- Prowler CLI can scan multiple subscriptions by using the `--subscription-ids` option.
|
||||
- A subscription must be accessible to the configured identity before Prowler can scan it.
|
||||
- Moving a subscription between management groups can change the permissions it inherits and may require a connection test or rediscovery.
|
||||
|
||||
See [Azure Subscription Scope](/user-guide/providers/azure/subscriptions) for subscription selection and CLI options.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Management groups do not replace subscription providers in Prowler.
|
||||
- Azure role inheritance depends on the management-group hierarchy and the scope of each assignment; verify access on every subscription selected for scanning.
|
||||
- The Prowler Cloud workflow is designed around Azure management-group discovery and subscription onboarding. The Prowler CLI workflow still requires explicit subscription selection when restricting scans.
|
||||
- Changes to management-group membership or role assignments may not be reflected until the hierarchy is refreshed and access is tested again.
|
||||
@@ -25,14 +25,4 @@ Check the [Authentication > Subscription Scope Permissions](/user-guide/provider
|
||||
|
||||
## Recommendation for Managing Multiple Subscriptions
|
||||
|
||||
Scanning multiple subscriptions requires creating and assigning roles for each, which can be a time-consuming process. To streamline subscription management and auditing, use management groups in Azure. This approach allows Prowler to efficiently organize and audit multiple subscriptions collectively.
|
||||
|
||||
1. **Create a Management Group**: Follow the [official guide](https://learn.microsoft.com/en-us/azure/governance/management-groups/create-management-group-portal) to create a new management group.
|
||||
|
||||

|
||||
|
||||
2. **Assign Roles**: Assign necessary roles to the management group, similar to the [role assignment process](#assigning-permissions-for-subscription-scans).
|
||||
|
||||
Role assignment should be done at the management group level instead of per subscription.
|
||||
|
||||
3. **Add Subscriptions**: Add all subscriptions you want to audit to the newly created management group. 
|
||||
Scanning multiple subscriptions requires creating and assigning roles for each, which can be a time-consuming process. To streamline subscription management and auditing, use [Azure Management Groups](/user-guide/providers/azure/management-groups) to organize subscriptions and assign permissions collectively.
|
||||
|
||||
@@ -4,6 +4,8 @@ title: 'Scanning a Specific GCP Organization'
|
||||
|
||||
By default, Prowler scans all Google Cloud projects accessible to the authenticated user.
|
||||
|
||||
For the cross-provider organization lifecycle and capability comparison, see [Organizations Across Cloud Providers](/user-guide/organizations).
|
||||
|
||||
To limit the scan to projects within a specific Google Cloud organization, use the `--organization-id` option with the GCP organization’s ID:
|
||||
|
||||
```console
|
||||
|
||||
@@ -42,7 +42,7 @@ Required for scanning repository security settings:
|
||||
|
||||
| Permission | Access Level | Purpose | Checks Enabled |
|
||||
|------------|-------------|---------|----------------|
|
||||
| **Administration** | Read | Branch protection, security settings | All branch protection checks, secret scanning status |
|
||||
| **Administration** | Read | Branch protection, security and Actions settings | All branch protection checks, secret scanning status, `repository_default_workflow_permissions_read_only` |
|
||||
| **Contents** | Read | File existence checks | `repository_public_has_securitymd_file`, `repository_has_codeowners_file` |
|
||||
| **Metadata** | Read | Basic repository information | All checks (automatically granted) |
|
||||
| **Dependabot alerts** | Read | Dependency vulnerability scanning | `repository_dependency_scanning_enabled` |
|
||||
@@ -63,7 +63,7 @@ Required for scanning organization-level security settings:
|
||||
|
||||
| Permission | Access Level | Purpose | Checks Enabled |
|
||||
|------------|-------------|---------|----------------|
|
||||
| **Administration** | Read | Organization security policies | `organization_members_mfa_required`, `organization_repository_creation_limited`, `organization_default_repository_permission_strict` |
|
||||
| **Administration** | Read | Organization security policies and Actions settings | `organization_members_mfa_required`, `organization_repository_creation_limited`, `organization_default_repository_permission_strict`, `organization_default_workflow_permissions_read_only`, `organization_actions_pull_request_approval_disabled` |
|
||||
| **Members** | Read | Member access reviews | Organization membership auditing |
|
||||
|
||||
#### Account Permissions (Fine-Grained PAT only)
|
||||
@@ -83,8 +83,8 @@ With the **Read-only permissions** listed above, Prowler can run:
|
||||
| Check Category | Coverage | Notes |
|
||||
|----------------|----------|-------|
|
||||
| Branch protection checks (12 checks) | ✅ Full | Signed commits, status checks, PR reviews, etc. |
|
||||
| Repository security checks | ✅ Full | Secret scanning, Dependabot, SECURITY.md, CODEOWNERS |
|
||||
| Organization checks (3 checks) | ✅ Full | MFA, repo creation policies, default permissions |
|
||||
| Repository security checks | ✅ Full | Secret scanning, Dependabot, SECURITY.md, CODEOWNERS, default workflow permissions |
|
||||
| Organization checks (5 checks) | ✅ Full | MFA, repo creation policies, default permissions, Actions workflow permissions |
|
||||
| Compliance frameworks | ✅ Full | CIS GitHub Benchmark and others |
|
||||
| Merge settings (`delete_branch_on_merge`) | ⚠️ MANUAL | Requires write permission (see below) |
|
||||
|
||||
@@ -171,6 +171,7 @@ Use OAuth App Tokens when building applications that need delegated user permiss
|
||||
|
||||
- `repo`: Full control of repositories
|
||||
- `read:org`: Read organization and team membership
|
||||
- `admin:org`: Required by `organization_default_workflow_permissions_read_only` and `organization_actions_pull_request_approval_disabled` to read the organization Actions workflow permissions
|
||||
- `read:user`: Read user profile data
|
||||
|
||||
**Create an OAuth App:**
|
||||
@@ -214,7 +215,7 @@ If a GitHub App is required:
|
||||
|
||||
| Permission | Access Level | Purpose | Checks Enabled |
|
||||
|------------|-------------|---------|----------------|
|
||||
| **Administration** | Read | Branch protection, security settings | All branch protection checks, `repository_secret_scanning_enabled` |
|
||||
| **Administration** | Read | Branch protection, security and Actions settings | All branch protection checks, `repository_secret_scanning_enabled`, `repository_default_workflow_permissions_read_only` |
|
||||
| **Contents** | Read | File existence checks | `repository_public_has_securitymd_file`, `repository_has_codeowners_file` |
|
||||
| **Metadata** | Read | Basic repository information | All checks (automatically granted) |
|
||||
| **Dependabot alerts** | Read | Dependency vulnerability scanning | `repository_dependency_scanning_enabled` |
|
||||
@@ -223,7 +224,7 @@ If a GitHub App is required:
|
||||
|
||||
| Permission | Access Level | Purpose | Checks Enabled |
|
||||
|------------|-------------|---------|----------------|
|
||||
| **Administration** | Read | Organization security policies | `organization_members_mfa_required`, `organization_repository_creation_limited`, `organization_default_repository_permission_strict` |
|
||||
| **Administration** | Read | Organization security policies and Actions settings | `organization_members_mfa_required`, `organization_repository_creation_limited`, `organization_default_repository_permission_strict`, `organization_default_workflow_permissions_read_only`, `organization_actions_pull_request_approval_disabled` |
|
||||
| **Members** | Read | Member access reviews | Organization membership auditing |
|
||||
|
||||
**Create a GitHub App:**
|
||||
|
||||
@@ -34,4 +34,4 @@ prowler scaleway
|
||||
|
||||
## Required Scaleway Permissions
|
||||
|
||||
The API key bearer needs read access to the IAM API in order to list users and API keys. The `IAMReadOnly` policy is sufficient. Refer to the [Scaleway IAM policy reference](https://www.scaleway.com/en/docs/identity-and-access-management/iam/reference-content/permission-sets/) for the full list of permissions.
|
||||
The API key bearer needs read access to the IAM API to list users and API keys. The `IAMReadOnly` policy is sufficient. Refer to the [Scaleway IAM policy reference](https://www.scaleway.com/en/docs/identity-and-access-management/iam/reference-content/permission-sets/) for the full list of permissions.
|
||||
|
||||
@@ -13,7 +13,7 @@ StackIT support in Prowler is community-maintained. For commercial support or to
|
||||
Before running Prowler with the StackIT provider, ensure you have:
|
||||
|
||||
1. A StackIT account with at least one project
|
||||
2. A StackIT service account key file with permissions on the project (`iaas.viewer` is enough for the currently shipped IaaS checks; `project.owner` works for any future service). See the [Authentication guide](/user-guide/providers/stackit/authentication) for the full setup.
|
||||
2. A StackIT service account key file with project permissions for the services to scan. `iaas.viewer` is sufficient for the currently shipped IaaS checks. For SKE, assign the project-scoped `Reader` role as a broad read-only convenience, or create a custom role with `resource-manager.project.get`, `ske.cluster.list`, `ske.cluster.get`, and `ske.version.list`. The `resource-manager.project.get` permission is optional for scanning; it only lets Prowler verify and display the project name. See the [Authentication guide](/user-guide/providers/stackit/authentication) for the full setup.
|
||||
3. Access to Prowler CLI (see [Installation](/getting-started/installation/prowler-cli))
|
||||
|
||||
## Prowler CLI
|
||||
@@ -124,7 +124,9 @@ Prowler outputs findings to the console and writes reports to the `output/` dire
|
||||
|
||||
| Service | StackIT API | Description | Example Checks |
|
||||
|---------|-------------|-------------|----------------|
|
||||
| **IaaS** | `iaas` | Virtual machines, network interfaces, security groups | `iaas_security_group_ssh_unrestricted`, `iaas_security_group_rdp_unrestricted`, `iaas_security_group_database_unrestricted`, `iaas_security_group_all_traffic_unrestricted` |
|
||||
| **IaaS** | `iaas` | Virtual machines, network interfaces, security groups | `iaas_security_group_ssh_unrestricted`, `iaas_security_group_rdp_unrestricted`, `iaas_security_group_database_unrestricted`, `iaas_security_group_all_traffic_unrestricted`, `iaas_server_public_ip_attached` |
|
||||
| **Object Storage** | `objectstorage` | Buckets, object lock, retention policies, access keys | `objectstorage_bucket_object_lock_enabled`, `objectstorage_bucket_retention_policy`, `objectstorage_access_key_expiration` |
|
||||
| **Kubernetes Engine** | `ske` | Managed Kubernetes clusters | `ske_cluster_no_public_endpoint` |
|
||||
|
||||
Additional services will be added in future releases. Track progress in the [Prowler release notes](https://github.com/prowler-cloud/prowler/releases).
|
||||
|
||||
@@ -136,6 +138,8 @@ If the scan fails with a 401 error, the service account key is no longer valid (
|
||||
|
||||
### Permission Errors
|
||||
|
||||
If checks fail with a 403 error, the service account is missing the required role on the project. Re-check the role assignment in the StackIT portal (`iaas.viewer` is the minimum for the shipped IaaS checks).
|
||||
If the connection probe warns that Resource Manager access could not be verified with a 403 response, Prowler continues to service discovery. Grant the optional `resource-manager.project.get` permission to verify and display the project name.
|
||||
|
||||
If a service call fails with a 403 response, the service account is missing a permission required by that service. `iaas.viewer` is sufficient for the shipped IaaS checks. The `ske_cluster_no_public_endpoint` check lists clusters and therefore requires `ske.cluster.list`; STACKIT least-privilege custom-role examples also include `ske.cluster.get` and `ske.version.list`. Service API calls remain authoritative and stop the scan when access is denied.
|
||||
|
||||
For detailed setup steps, see the [Authentication guide](/user-guide/providers/stackit/authentication).
|
||||
|
||||
@@ -4,6 +4,8 @@ title: 'AWS Organizations Bulk Provisioning in Prowler'
|
||||
|
||||
Prowler offers an automated tool to discover and provision all AWS accounts within an AWS Organization. This streamlines onboarding for organizations managing multiple AWS accounts by automatically generating the configuration needed for bulk provisioning.
|
||||
|
||||
For the cross-provider organization lifecycle and terminology, see [Organizations Across Cloud Providers](/user-guide/organizations).
|
||||
|
||||
The tool, `aws_org_generator.py`, complements the [Bulk Provider Provisioning](./bulk-provider-provisioning) tool and is available in the Prowler repository at: [util/prowler-bulk-provisioning](https://github.com/prowler-cloud/prowler/tree/master/util/prowler-bulk-provisioning)
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: 'Alerts'
|
||||
sidebarTitle: 'Alerts'
|
||||
description: 'Create email alerts from Prowler Cloud findings to monitor relevant security changes after scans or in daily digests.'
|
||||
description: 'Create alerts from Prowler Cloud findings, deliver them to email recipients and Slack channels, and monitor relevant security changes after scans or in daily digests.'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
@@ -9,7 +9,7 @@ import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
|
||||
|
||||
<VersionBadge version="5.26.0" />
|
||||
|
||||
Alerts notify recipients by email when security findings match saved filter conditions. Use Alerts to track high-priority findings, monitor specific providers or services, and keep teams informed about scan results that match defined criteria.
|
||||
Alerts notify their destinations — email recipients, Slack channels, or both — when security findings match saved filter conditions. Use Alerts to track high-priority findings, monitor specific providers or services, and keep teams informed about scan results that match defined criteria.
|
||||
|
||||
<SubscriptionBanner />
|
||||
|
||||
@@ -19,12 +19,13 @@ Before creating Alerts, ensure that:
|
||||
|
||||
* At least one scan has completed and produced findings.
|
||||
* The user role includes the `manage_alerts` permission.
|
||||
* To deliver Alerts to Slack channels, a Slack workspace is connected, at least one channel is authorized on it, and the integration's connection check has confirmed that channel. See [Slack Integration](/user-guide/tutorials/prowler-app-slack-integration).
|
||||
|
||||
The `manage_alerts` permission is required to create, edit, test, enable, disable, and delete Alerts. See [RBAC Administrative Permissions](/user-guide/tutorials/prowler-app-rbac#rbac-administrative-permissions) for details.
|
||||
|
||||
## How Alerts Work
|
||||
|
||||
Alerts are created from Findings filters. When an Alert runs, Prowler Cloud evaluates the saved conditions against findings and sends an email digest when matching findings exist.
|
||||
Alerts are created from Findings filters. When an Alert runs, Prowler Cloud evaluates the saved conditions against findings and notifies the Alert's destinations when matching findings exist: an email digest to each recipient, a message to each Slack channel, or both. Destination kinds are independent — neither requires the other, and neither displaces the other.
|
||||
|
||||
<Note>
|
||||
Alerts evaluate findings with status `FAIL` only. Findings with status `PASS` or `MANUAL`, and muted findings, never trigger an Alert regardless of the saved filters.
|
||||
@@ -53,6 +54,7 @@ To create an Alert:
|
||||
* **Description:** Add optional context for the Alert.
|
||||
* **Frequency:** Select when Prowler Cloud should evaluate the Alert.
|
||||
* **Recipients:** Select the recipients who should receive the email digest.
|
||||
* **Destination channels:** Select the Slack channels that should receive the Alert. See [Slack Channel Destinations](#slack-channel-destinations).
|
||||
|
||||

|
||||
|
||||
@@ -86,11 +88,18 @@ Navigate to **Alerts** to review and manage existing Alerts.
|
||||
|
||||

|
||||
|
||||
The **Destinations** column summarizes where each Alert delivers, without the Alert being opened:
|
||||
|
||||
* **Email recipients:** The first address, plus a count of the rest, such as `security@example.com +2 more`.
|
||||
* **Slack channels:** The first channel, plus a count of the rest, such as `#sec-alerts +1 more`.
|
||||
|
||||
Each summary is omitted when that destination kind is empty, and the column reads **No destinations** when an Alert has neither.
|
||||
|
||||
Each Alert provides these actions:
|
||||
|
||||
| Action | Description |
|
||||
|--------|-------------|
|
||||
| Edit | Update name, description, recipients, frequency, or filters. |
|
||||
| Edit | Update name, description, recipients, Slack channels, frequency, or filters. |
|
||||
| Enable/Disable | Start or stop Alert evaluation without deleting the Alert. |
|
||||
| Delete | Permanently remove the Alert. |
|
||||
|
||||
@@ -125,6 +134,37 @@ By default, the **organization owner** receives a **daily digest** for **critica
|
||||
|
||||
If a recipient unsubscribes from Alerts, that address stops receiving digests until it is reconfirmed.
|
||||
|
||||
An Alert does not require email recipients: an Alert that targets Slack channels only is accepted with those channels as its sole destinations. An Alert with no destinations at all stays valid and keeps evaluating its filters, but it delivers nothing.
|
||||
|
||||
## Slack Channel Destinations
|
||||
|
||||
<VersionBadge version="5.40.0" />
|
||||
|
||||
An Alert can post to Slack channels alongside its email recipients, or instead of them. The **Destination channels** field sits directly below **Recipients** in the Alert form, both when creating an Alert and when editing one. When the Alert matches findings, Prowler Cloud posts a message to each of its channels and sends the email digest to each of its recipients, independently of each other.
|
||||
|
||||
The channels offered are the confirmed channels of the connected Slack integration, never the whole Slack workspace. Widening the pool takes two steps on the integration: authorize the channel there, then run its connection check, which confirms the channel by posting a one-time confirmation message to it. Once confirmed, the channel is selectable on every Alert. See [Slack Integration](/user-guide/tutorials/prowler-app-slack-integration) for connecting a workspace, authorizing its channels, and confirming them.
|
||||
|
||||
A channel that was authorized a moment ago but does not appear in the Alert form has not been confirmed yet. Run **Test connection** on the Slack integration, then reopen the Alert form.
|
||||
|
||||
Private channels are identified as **Private** both in the open channel list and on the selected channels once the list is closed, so a private destination is never mistaken for a public one.
|
||||
|
||||
### When Slack Channels Cannot Be Selected
|
||||
|
||||
The field is always present, so channel delivery is never silently missing. It reports why it cannot be used:
|
||||
|
||||
| State | What the Alert form shows |
|
||||
|-------|---------------------------|
|
||||
| No Slack workspace connected | The field is visible but cannot be edited, explaining that posting Alerts to Slack channels needs a connected Slack workspace, with a link to the Slack integration. |
|
||||
| Workspace connected, no confirmed channels | A notice that no channels are available yet and that they are authorized and confirmed on the Slack integration, with the same link. |
|
||||
|
||||
In both states the rest of the Alert is unaffected: it can still be created or saved with its filters, frequency, and email recipients.
|
||||
|
||||
<Note>
|
||||
Slack destinations stay in step with the integration. Removing a channel from the integration's authorized set — or disconnecting the Slack integration altogether — removes that channel from every Alert that targeted it, so an Alert never keeps a destination Prowler can no longer deliver to. The Alert keeps its filters, frequency, and email recipients, and future delivery to that channel stops: nothing is posted to announce the removal, and the notifications already delivered stay in the channel. Restoring delivery means authorizing and confirming the channel again on the integration, then selecting it again on the Alert.
|
||||
</Note>
|
||||
|
||||
Saving an Alert that names a channel which is not a confirmed channel of a connected Slack integration is refused, and the reason is reported on the Alert form. Authorize and confirm the channel on the Slack integration, or remove it from the Alert, and save again.
|
||||
|
||||
## Email Notifications
|
||||
|
||||
When an Alert matches findings, Prowler Cloud sends a security alert email that summarizes the matching findings. The email includes:
|
||||
@@ -141,6 +181,6 @@ When an Alert matches findings, Prowler Cloud sends a security alert email that
|
||||
|
||||
* **Start with focused filters:** Create Alerts for specific high-priority scopes, such as critical findings, production providers, or important services.
|
||||
* **Use clear names:** Choose names that explain the intent of the Alert.
|
||||
* **Review recipients regularly:** Keep recipient lists aligned with current ownership.
|
||||
* **Review destinations regularly:** Keep recipient lists and channel selections aligned with current ownership.
|
||||
* **Test before saving edits:** Use **Test** after changing filters to confirm that the Alert matches the expected findings.
|
||||
* **Disable instead of deleting during tuning:** Disable Alerts temporarily when adjusting filters or recipients.
|
||||
* **Disable instead of deleting during tuning:** Disable Alerts temporarily when adjusting filters or destinations.
|
||||
|
||||
@@ -16,26 +16,24 @@ Attack Paths analyzes relationships between cloud resources, permissions, and se
|
||||
By mapping these relationships as a graph, Attack Paths reveals risks that individual security checks cannot detect on their own, such as an IAM role that can escalate its own permissions, or a chain of policies that grants unintended access to sensitive resources.
|
||||
|
||||
<Note>
|
||||
Attack Paths is currently available for **AWS** providers. Support for
|
||||
additional providers is planned.
|
||||
Attack Paths is currently available for **AWS** providers. Support for additional providers is planned.
|
||||
</Note>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The following prerequisites are required for Attack Paths:
|
||||
|
||||
- **An AWS provider is configured** with valid credentials in Prowler Cloud. For setup instructions, see [Getting Started with AWS](/user-guide/providers/aws/getting-started-aws).
|
||||
- **An AWS provider is configured** with valid credentials. For setup instructions, see [Getting Started with AWS](/user-guide/providers/aws/getting-started-aws).
|
||||
- **At least one scan has completed** on the configured AWS provider and produced graph data. Attack Paths scans run automatically alongside regular security scans, no separate configuration is required.
|
||||
|
||||
## How Attack Paths Scans Work
|
||||
|
||||
Attack Paths scans are generated automatically when a security scan runs on an AWS provider. Each completed scan produces graph data that maps relationships between IAM principals, policies, trust configurations, and other resources.
|
||||
Attack Paths scans are generated automatically when a security scan runs on an AWS provider. When a scan produces graph data, it maps relationships between IAM principals, policies, trust configurations, and other resources. A scan can complete without producing graph data.
|
||||
|
||||
Once the scan finishes and graph data is ready, the scan appears in the Attack Paths scan table with a **Completed** status and a check in the **Graph** column. Scans that are still queued or running remain visible, but they cannot be selected until graph data is ready.
|
||||
When graph data is ready, the scan appears in the Attack Paths scan table with a check in the **Graph** column and can be selected regardless of its current status. Scans without graph data remain visible but cannot be selected. If a new scan cycle starts after graph data is available, the previous cycle remains available while the new scan runs.
|
||||
|
||||
<Note>
|
||||
Since Prowler scans all configured providers every **24 hours** by default,
|
||||
Attack Paths data stays up to date automatically.
|
||||
Prowler Cloud and Prowler Private Cloud scan configured providers every **24 hours** by default, so Attack Paths data stays up to date automatically.
|
||||
</Note>
|
||||
|
||||
## Accessing Attack Paths
|
||||
@@ -66,7 +64,7 @@ The scans table displays all Attack Paths scans with the following columns:
|
||||
- **Graph:** Whether Attack Paths graph data is available for the scan.
|
||||
- **Duration:** Total scan time.
|
||||
|
||||
To select a scan for analysis, click the radio button on any row with a **Completed** status and available graph data.
|
||||
To select a scan for analysis, click any row with a check in the **Graph** column. A row can remain selectable while a new scan cycle runs because Attack Paths keeps the graph from the previous completed cycle available.
|
||||
|
||||
<img
|
||||
src="/images/prowler-app/attack-paths/scan-list-table.png"
|
||||
@@ -75,8 +73,7 @@ To select a scan for analysis, click the radio button on any row with a **Comple
|
||||
/>
|
||||
|
||||
<Note>
|
||||
Only scans with graph data can be selected. Disabled rows include a tooltip
|
||||
that explains why the graph is not available yet.
|
||||
Only scans with graph data can be selected. Disabled rows include a tooltip that explains why the graph is not available yet.
|
||||
</Note>
|
||||
|
||||
## Choosing a Query
|
||||
@@ -97,9 +94,7 @@ To choose a query, click the dropdown and select from the available options. Eac
|
||||
Once selected, a description panel appears below the dropdown with more context about the query.
|
||||
|
||||
<Note>
|
||||
In Prowler Cloud and Prowler Private Cloud, the query selector hides queries
|
||||
confirmed empty for the selected scan, so only queries that return data remain
|
||||
visible. See [Active Queries](/user-guide/tutorials/prowler-app-attack-paths-active-queries).
|
||||
In Prowler Cloud and Prowler Private Cloud, the query selector hides built-in queries confirmed empty for the selected scan. Built-in queries without a confirmed empty result and the **Custom openCypher query** remain visible. See [Active Queries](/user-guide/tutorials/prowler-app-attack-paths-active-queries).
|
||||
</Note>
|
||||
|
||||
## Configuring Query Parameters
|
||||
@@ -120,7 +115,7 @@ For example, **Internet-Exposed EC2 with Sensitive S3 Access** uses **Tag key**
|
||||
|
||||
## Writing Custom openCypher Queries
|
||||
|
||||
In addition to the built-in queries, Attack Paths supports custom read-only [openCypher](https://opencypher.org/) queries. Custom queries provide direct access to the underlying graph so security teams can answer ad-hoc questions, prototype detections, or extend coverage beyond the built-in catalogue.
|
||||
In addition to the built-in queries, Attack Paths supports custom read-only [openCypher](https://opencypher.org/) queries. Custom queries provide direct access to the underlying graph so security teams can answer ad-hoc questions, prototype detections, or extend coverage beyond the built-in catalog.
|
||||
|
||||
To write a custom query, select **Custom openCypher query** from the query dropdown. A code editor with syntax highlighting and line numbers appears, ready to receive the query.
|
||||
|
||||
@@ -192,11 +187,7 @@ Custom queries traverse the same Cartography graph the built-in queries use. Nod
|
||||
For the complete reference, including the graph model, list-typed and JSON-encoded properties, performance guidance, and openCypher compatibility rules, see [Attack Paths Queries](/developer-guide/attack-paths-queries) in the Developer Guide.
|
||||
|
||||
<Note>
|
||||
AI assistants connected through Prowler MCP Server can fetch the exact
|
||||
Cartography schema for the active scan via the
|
||||
`prowler_get_attack_paths_cartography_schema` tool. This guarantees that
|
||||
generated queries match the schema version pinned by the running Prowler
|
||||
release.
|
||||
AI assistants connected through Prowler MCP Server can fetch the exact Cartography schema for the active scan via the `prowler_get_attack_paths_cartography_schema` tool. This guarantees that generated queries match the schema version pinned by the running Prowler release.
|
||||
</Note>
|
||||
|
||||
## Executing a Query
|
||||
@@ -221,12 +212,22 @@ If the query returns no results, an informational message appears. Common reason
|
||||
|
||||
After a successful execution, the graph visualization renders below the query builder. The graph maps relationships between cloud resources, IAM entities, public exposure, and security findings.
|
||||
|
||||
### Grouped Graphs and Query Outcomes
|
||||
|
||||
<VersionBadge version="5.39.0" />
|
||||
|
||||
Prowler Cloud and Prowler Private Cloud group resources of the same class and graph level into expandable nodes. Built-in query graphs also end with a query outcome, which states the result that the path can lead to.
|
||||
|
||||
Prowler Local Server keeps the flat graph view, including the provider root. Custom openCypher queries do not have a catalog outcome, so their graphs do not include an outcome node.
|
||||
|
||||
### Node Types
|
||||
|
||||
- **Provider root nodes:** Represent the AWS account or provider root for the selected scan.
|
||||
- **Resource nodes:** Represent cloud resources such as IAM roles, policies, EC2 instances, security groups, and S3 buckets.
|
||||
- **Grouped resource nodes:** Represent multiple resources of the same class in Prowler Cloud and Prowler Private Cloud. A number in the upper-right corner shows how many resources the node contains. A red outline indicates that one or more resources in the group have findings.
|
||||
- **Resource nodes:** Represent individual cloud resources such as IAM roles, policies, EC2 instances, security groups, and S3 buckets. A class with one resource remains an individual node.
|
||||
- **Internet nodes:** Represent exposure from the public internet.
|
||||
- **Finding nodes:** Represent Prowler findings linked to resources. Finding colors indicate risk level, such as critical, high, medium, or low.
|
||||
- **Outcome nodes:** Mark the terminal result of a built-in query in Prowler Cloud and Prowler Private Cloud. The orange node displays outcomes such as **Code execution**, **Privilege escalation**, **Public exposure**, or **Resource inventory**. A dashed ring and the label **Latent outcome** indicate an inventory or another partial outcome.
|
||||
- **Provider root nodes:** Represent the AWS account or provider root in the Prowler Local Server flat graph.
|
||||
|
||||
### Edge Types
|
||||
|
||||
@@ -234,7 +235,7 @@ After a successful execution, the graph visualization renders below the query bu
|
||||
- **Finding edges:** Dashed relationships between resources and their associated findings.
|
||||
- **Highlighted paths:** Green edges that show the active path when you hover a node or focus a finding.
|
||||
|
||||
The standard graph view includes a minimap and a legend below the canvas. The legend shows the provider roots, visible node types, finding risk levels, node states, and edge types present in the current view.
|
||||
The standard graph view includes a minimap and a legend below the canvas. The legend shows the visible node types, finding risk levels, node states, and edge types present in the current view. Prowler Local Server also displays the provider root in the legend.
|
||||
|
||||
<img
|
||||
src="/images/prowler-app/attack-paths/graph-visualization.png"
|
||||
@@ -244,17 +245,32 @@ The standard graph view includes a minimap and a legend below the canvas. The le
|
||||
|
||||
## Interacting with the Graph
|
||||
|
||||
The graph banner describes the main interactions:
|
||||
The graph supports these interactions:
|
||||
|
||||
- Click a node with a number in the upper-right corner to expand its resource group.
|
||||
- Click a finding to focus its connected path.
|
||||
- Click a resource with findings to show or hide its related findings.
|
||||
- Hover a node to highlight its connected path.
|
||||
|
||||
### Expanding Resource Groups
|
||||
|
||||
In Prowler Cloud and Prowler Private Cloud, a node with a number in its upper-right corner represents a resource group. The number is the total number of resources in the group.
|
||||
|
||||
- Click the group node to display its resources. Multiple groups can remain expanded, and the graph automatically fits the visible nodes to the canvas.
|
||||
- Double-click any resource revealed from a group to collapse that group.
|
||||
- Click **Collapse all groups** in the graph toolbar to close every expanded group. This control appears only while at least one group is expanded.
|
||||
|
||||
<img
|
||||
src="/images/prowler-app/attack-paths/graph-visualization-expanded.png"
|
||||
alt="Attack Paths graph showing an expanded AWS Role group and remaining numbered resource groups"
|
||||
width="700"
|
||||
/>
|
||||
|
||||
### Showing Related Findings
|
||||
|
||||
Resource nodes with related findings are clickable. Click one of these resources to show its finding nodes. Click the resource again to hide them.
|
||||
|
||||
The graph automatically fits the selected resource and its related findings when the findings are shown.
|
||||
The selected resource is highlighted in green while its findings are visible. The graph automatically fits the selected resource and its related findings when the findings are shown.
|
||||
|
||||
### Focusing a Finding Path
|
||||
|
||||
@@ -279,12 +295,12 @@ The toolbar in the top-right corner of the graph provides:
|
||||
|
||||
- **Zoom in / Zoom out:** Adjust the zoom level
|
||||
- **Fit graph to view:** Reset the view to fit the visible graph
|
||||
- **Export graph:** Download the current graph as a PNG file
|
||||
- **Collapse all groups:** Close every expanded resource group. This control appears only when a group is expanded
|
||||
- **Export graph:** Download the current graph, including its grouped or expanded state and outcome, as a PNG file
|
||||
- **Fullscreen:** Open the graph in a full-size modal
|
||||
|
||||
<Note>
|
||||
Use **Ctrl + Scroll** (or **Cmd + Scroll** on macOS) to zoom directly within
|
||||
the graph area.
|
||||
Use **Ctrl + Scroll** (or **Cmd + Scroll** on macOS) to zoom directly within the graph area.
|
||||
</Note>
|
||||
|
||||
## Viewing Finding Details
|
||||
|
||||
@@ -69,7 +69,7 @@ To remove **another** user from your organization, use the [_Expel from organiza
|
||||
#### Inviting Users
|
||||
|
||||
<Note>
|
||||
Please be aware that at this time, an email address can only be associated with a single Prowler account.
|
||||
Note that an email address can only be associated with a single Prowler account.
|
||||
|
||||
</Note>
|
||||
Follow these steps to invite a user to your account:
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
---
|
||||
title: "Slack Integration"
|
||||
sidebarTitle: 'Slack'
|
||||
description: 'Connect a Slack workspace to Prowler Cloud or Prowler Private Cloud, authorize the channels Prowler posts to, and verify the connection.'
|
||||
---
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
|
||||
|
||||
<VersionBadge version="5.40.0" />
|
||||
|
||||
<SubscriptionBanner />
|
||||
|
||||
Prowler Cloud and Prowler Private Cloud connect to a Slack workspace so security updates arrive where teams already work. Connecting takes one approval in Slack — there is no bot token to create, copy, or store by hand — and Prowler records the set of channels it is authorized to post to.
|
||||
|
||||
Integrating Prowler Cloud or Prowler Private Cloud with Slack provides:
|
||||
|
||||
* **Approval-based setup:** Approve Prowler once in Slack instead of building a Slack app and pasting a token.
|
||||
* **Confirmed destinations:** The connection check verifies every authorized channel and confirms each new one in the channel itself, so a channel Prowler cannot reach is reported before anything depends on it.
|
||||
* **Controlled reach:** Prowler posts only to the channels authorized on the integration, and private channels stay invisible until the Prowler app is invited to them.
|
||||
|
||||
<Note>
|
||||
This guide covers the Slack integration in Prowler Cloud and Prowler Private Cloud. It is unrelated to the Prowler CLI `--slack` flag, which posts a scan summary from the command line using a self-created Slack app and the `SLACK_API_TOKEN` and `SLACK_CHANNEL_NAME` environment variables — see [CLI Integrations](/user-guide/cli/tutorials/integrations) for that feature.
|
||||
</Note>
|
||||
|
||||
## How the Slack Integration Works
|
||||
|
||||
When connected and configured:
|
||||
|
||||
1. A Slack workspace is approved once through Slack's app install flow, and Prowler stores the resulting credential encrypted.
|
||||
2. Prowler reads the channels it can post to: the workspace's public channels, plus the private channels the Prowler app has been invited to.
|
||||
3. Several of those channels are selected and saved as the integration's authorized channels.
|
||||
4. The connection check verifies the credential and every authorized channel, and posts a one-time confirmation message to each channel it has not confirmed yet.
|
||||
5. Features that deliver to Slack, such as [Alerts](/user-guide/tutorials/prowler-alerts), choose their destinations from the confirmed channels.
|
||||
6. Disconnecting removes the integration from Prowler and attempts to revoke Prowler's access at Slack.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The Slack integration is available only in **Prowler Cloud** and **Prowler Private Cloud**. Prowler Local Server does not serve the Slack endpoints at all, so the Slack card does not appear on the Integrations page and the management page redirects away.
|
||||
|
||||
Configuring and using the Slack integration requires the **Manage Integrations** permission. The integration is tenant-wide, so it does not require **Unlimited Visibility** or any specific Provider Group.
|
||||
|
||||
One Slack workspace connects per tenant. Approving Prowler again in the same workspace refreshes the stored credential and keeps the authorized channels, but it resets their confirmations and the connection state — the connection check has to be run again. Approving Prowler in a *different* workspace is refused until the current workspace is disconnected: a workspace is never swapped out silently.
|
||||
|
||||
## Permissions Prowler Requests in Slack
|
||||
|
||||
Slack shows a consent screen listing everything the Prowler app asks for. Prowler requests exactly four bot scopes:
|
||||
|
||||
| Scope | Why Prowler Requests It |
|
||||
|-------|-------------------------|
|
||||
| `chat:write` | Post the confirmation message, and any later notification, to the authorized channels. |
|
||||
| `chat:write.public` | Post to a public channel without first inviting the Prowler app to it. |
|
||||
| `channels:read` | List public channels for the channel selection and resolve the chosen ones. |
|
||||
| `groups:read` | List the private channels the Prowler app has been invited to, so they appear in the channel selection. |
|
||||
|
||||
Two of these read more broadly than they behave, and both are worth understanding before approving the app.
|
||||
|
||||
### What `chat:write.public` Does Not Grant
|
||||
|
||||
On the consent screen, `chat:write.public` reads as permission to post in any public channel. Prowler never uses it that way: **Prowler only ever posts to the channels authorized on the integration.** The scope exists so that authorizing a public channel does not also require someone to invite the Prowler app to it first.
|
||||
|
||||
### Why a Private Channel Is Missing From the Channel List
|
||||
|
||||
`groups:read` reveals only the private channels the Prowler app is already a member of. A private channel therefore appears in the channel list only after someone invites `@Prowler` to it in Slack:
|
||||
|
||||
```text
|
||||
/invite @Prowler
|
||||
```
|
||||
|
||||
That invite is issued in Slack, by that channel's own members, and **the invite itself is the permission grant** — no scope bypasses it. Prowler ships no in-product flow to get the app invited, because the decision belongs to the channel's members. After inviting the app, click **Refresh channels** to re-read the list.
|
||||
|
||||
## Connecting a Slack Workspace
|
||||
|
||||
To connect a Slack workspace to Prowler Cloud or Prowler Private Cloud:
|
||||
|
||||
1. In either product, navigate to **Integrations**.
|
||||
2. Locate the **Slack** card and click **Manage**.
|
||||
|
||||

|
||||
|
||||
3. Click **Add to Slack**.
|
||||
|
||||

|
||||
|
||||
4. In Slack, select the workspace to connect and approve the permissions listed on the consent screen.
|
||||
5. Slack returns to Prowler Cloud or Prowler Private Cloud, which completes the install and shows the connected workspace.
|
||||
|
||||

|
||||
|
||||
The connected card reports the workspace name and a **Not checked yet** status: the connection is checked against the authorized channels, and none are authorized at this point. Authorizing them is the next step. Once at least one channel is authorized, **Test connection** verifies the credential and every authorized channel, and confirms the ones not confirmed yet.
|
||||
|
||||
<Note>
|
||||
Declining the consent screen creates nothing. Prowler reports that the workspace was not connected and offers to start again.
|
||||
</Note>
|
||||
|
||||
## Authorizing Destination Channels
|
||||
|
||||
Prowler posts to the channels authorized on the integration. Several channels can be authorized at once, and once the connection check has confirmed them they are the pool every consumer of the integration draws from: an [Alert](/user-guide/tutorials/prowler-alerts) picks its Slack destinations from the confirmed channels, never from the whole workspace.
|
||||
|
||||
1. Open the **Destination channels** selection. It lists the workspace's public channels, plus the private channels the Prowler app has been invited to, each marked **Private**.
|
||||
|
||||

|
||||
|
||||
2. Select one or more channels. A selected private channel keeps its lock and **Private** identification with the list closed, so the authorized set stays readable at a glance.
|
||||
3. Click **Save channels**.
|
||||
|
||||
Prowler validates the selection against Slack and derives each channel name itself, so a recorded name can never drift from the channel it belongs to. Once the set is saved, the page reports where Prowler posts and runs the connection check over it.
|
||||
|
||||
If the selection reports that no channels are available, the workspace exposes nothing Prowler can see. Create a public channel, or invite `@Prowler` to a private one, then click **Refresh channels**.
|
||||
|
||||
A workspace can hold more channels than Prowler reads in one go. When that happens, the selection says so and lists what was read: every listed channel is usable, and a channel missing from a partial list is not necessarily one `@Prowler` has to be invited to. Only listed channels can be selected: **Refresh channels** repeats the same bounded read rather than reading further, and the selection's search filters what was already read, so neither surfaces a channel the read left out.
|
||||
|
||||
Saving a new selection replaces the authorized set: channels left out of it stop being authorized, and channels added to it are authorized but not yet confirmed. Changing which channels are in the set also resets the integration's connection state, so the check runs again over the new set — reordering the same channels does not. Saving an empty selection leaves the integration with no authorized channels, and **Test connection** cannot be run again until at least one channel is authorized.
|
||||
|
||||
<Warning>
|
||||
Removing a channel from the authorized set also removes it from every Alert that targeted it. Those Alerts keep their filters, frequency, and email recipients, and future delivery to that channel simply stops: nothing is posted to announce the removal, and the notifications already delivered stay in the channel. Disconnecting the integration has the same effect on every channel it had authorized. Restoring delivery means authorizing and confirming the channel again here, then selecting it again on each Alert.
|
||||
</Warning>
|
||||
|
||||
### Confirming the Authorized Channels
|
||||
|
||||
A channel becomes usable as a destination once the connection check has confirmed it. Click **Test connection**: it verifies the stored credential and every authorized channel, and posts a one-time message to each channel it has not confirmed yet.
|
||||
|
||||
```text
|
||||
✅ Prowler connection verified. Notifications will be delivered to this channel.
|
||||
```
|
||||
|
||||
Later checks never post that message again to a channel that is already confirmed, so it arrives once per channel. The integration reports as connected only when every check and every required confirmation succeeded; a failure names the channel that failed. The check needs at least one authorized channel — with none authorized, it cannot be run yet.
|
||||
|
||||
Confirmation is what makes a channel selectable elsewhere in Prowler Cloud. A channel authorized a moment ago is missing from an Alert's channel list until a connection check confirms it.
|
||||
|
||||
## Disconnecting a Slack Workspace
|
||||
|
||||
Disconnecting removes the integration from Prowler **and** attempts to revoke Prowler's access at Slack.
|
||||
|
||||
1. On the Slack management page, click **Disconnect**.
|
||||
2. Review the confirmation, then click **Disconnect workspace**.
|
||||
|
||||

|
||||
|
||||
The page returns to its unconnected state, ready for a new install.
|
||||
|
||||
### What Revocation Means
|
||||
|
||||
Revocation is attempted at Slack, and it is best-effort:
|
||||
|
||||
* **Revocation succeeded:** The stored credential no longer grants Prowler anything, and the integration is gone from Prowler.
|
||||
* **Revocation failed:** The integration and the stored credential are gone from Prowler either way, so there is nothing to retry. Slack did not confirm the revocation, which means the Prowler app may still be installed in the workspace. Remove it from that workspace's Slack app settings.
|
||||
* **Revocation unreported:** Slack's answer carried no outcome either way. The integration is gone from Prowler, and the disconnect is reported without any claim about revocation. When certainty matters, check the workspace's Slack app settings and remove the Prowler app if it is still installed.
|
||||
|
||||
Prowler reports the outcome it received: a failed revocation always names the manual cleanup step, and an unreported one is never presented as revoked.
|
||||
|
||||
<Warning>
|
||||
Disconnecting cannot be undone, and it removes the Slack channels from every Alert that targeted them. Reconnecting means approving Prowler in Slack again, authorizing the destination channels again, confirming them with a connection check, and selecting them again on each Alert that posts to Slack.
|
||||
</Warning>
|
||||
|
||||
## Integration Status
|
||||
|
||||
The Slack management page reports the state of the connection and offers these actions:
|
||||
|
||||
| Button | Purpose | Notes |
|
||||
|--------|---------|-------|
|
||||
| **Test connection** | Verify the credential and every authorized channel, and confirm the ones not confirmed yet | Posts the confirmation message once per channel and updates the last-checked time. Cannot be run until at least one channel is authorized |
|
||||
| **Refresh channels** | Re-read the workspace's channel list | Use after inviting `@Prowler` to a private channel |
|
||||
| **Save channels** | Record the selected channels as the integration's authorized set | Enabled once the selection differs from the authorized set |
|
||||
| **Disconnect** | Remove the integration and attempt to revoke access at Slack | ⚠️ **Cannot be undone** — confirm before disconnecting |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Slack Is Not Available in This Environment Yet
|
||||
|
||||
The Prowler Slack app is not configured for the deployment being used, so no workspace can be connected. This resolves without any action on the tenant's side — the page starts working as soon as the app is configured.
|
||||
|
||||
### A Private Channel Does Not Appear in the Channel List
|
||||
|
||||
The Prowler app has not been invited to it. In Slack, run `/invite @Prowler` in that channel, then click **Refresh channels**. Membership is the permission: no scope reveals a private channel the app is not in.
|
||||
|
||||
### Connection Test Fails
|
||||
|
||||
* Confirm every authorized channel still exists and has not been archived. A failure names the channel Slack refused, and the integration reports as connected only when every authorized channel passes.
|
||||
* For a private authorized channel, confirm the Prowler app is still a member of it.
|
||||
* Confirm the Prowler app is still installed in the workspace.
|
||||
|
||||
### A Channel Is Missing From an Alert's Channel List
|
||||
|
||||
The channel is authorized here but not confirmed yet. Click **Test connection**: it confirms every authorized channel it has not confirmed, and confirmed channels become selectable on Alerts.
|
||||
|
||||
### Prowler's Access Has Been Revoked
|
||||
|
||||
When Slack stops accepting the stored credential — because a workspace administrator revoked it, or the app was removed from the workspace — Prowler reports the workspace as disconnected and offers **Reconnect to Slack**. Approving Prowler in Slack again restores access.
|
||||
|
||||
### The Connection Check Fails on a Channel
|
||||
|
||||
* Check the outcome reported on the page: it names the channel Slack refused and the reason Slack gave — an archived or deleted channel surfaces here rather than failing silently.
|
||||
* Confirm that channel is still one of the intended destinations, and that it has not been archived or deleted in Slack.
|
||||
* For a private channel, confirm the Prowler app is still a member of it.
|
||||
* One unreachable channel is enough to report the integration as not connected, so removing a retired channel from the authorized set clears the failure — bearing in mind that removing it also removes it from every Alert that targeted it.
|
||||
@@ -10,6 +10,8 @@ import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
|
||||
|
||||
Prowler Cloud onboards every AWS account in your Organization through a single guided wizard. Instead of connecting accounts one by one, you can discover every account in your AWS Organization, select the ones you want to monitor, test connectivity, and launch scans — all from the Prowler Cloud UI.
|
||||
|
||||
For the cross-provider organization lifecycle and terminology, see [Organizations Across Cloud Providers](/user-guide/organizations).
|
||||
|
||||
<SubscriptionBanner>
|
||||
For CLI-based multi-account scanning, see [AWS Organizations in Prowler CLI](/user-guide/providers/aws/organizations).
|
||||
</SubscriptionBanner>
|
||||
@@ -266,7 +268,7 @@ Click **Save**, **Save and launch scan**, or **Launch scan**, depending on the s
|
||||
After launching:
|
||||
- Scans appear in the **Scans** page as they start and complete.
|
||||
- Results populate the **Overview** and **Findings** pages.
|
||||
- Prowler runs an **automatic sync every 6 hours** to detect accounts added to or removed from your Organization. New accounts under the targeted OU or root are onboarded automatically.
|
||||
- To detect accounts added to or removed from the AWS Organization, repeat the discovery flow described in [Add or Remove Organization Accounts](#add-or-remove-organization-accounts).
|
||||
|
||||
## Manage Your Organization After Onboarding
|
||||
|
||||
@@ -288,6 +290,24 @@ Open the row actions menu on the organization row on the **Providers** page.
|
||||
|
||||
Organizational unit rows carry the same **Test Connections** and **Delete Organizational Unit** actions, scoped to the accounts beneath them.
|
||||
|
||||
### Add or Remove Organization Accounts
|
||||
|
||||
To refresh the account membership of an existing AWS Organization, repeat the same discovery flow used during onboarding:
|
||||
|
||||
1. Navigate to **Providers**, click **Add Provider**, and select **Amazon Web Services**.
|
||||
2. Choose **Add Multiple Accounts With AWS Organizations**.
|
||||
3. Enter the existing **Organization ID**, proceed to **Authentication Details**, and use the existing deployment account **Role ARN**.
|
||||
4. Confirm that the stack is deployed and click **Authenticate**. Prowler reuses the existing organization and starts a new discovery instead of creating a duplicate.
|
||||
|
||||
The refreshed tree shows the accounts currently returned by AWS Organizations:
|
||||
|
||||
- **New accounts** appear in the tree. Select them, test their connections, and save the configuration to connect them as providers. Existing providers and their historical data are preserved.
|
||||
- **Accounts that left the Organization** no longer appear in the tree. Discovery does not automatically delete their existing providers. To remove one, return to the **Providers** page, open the account provider actions, and select **Delete Provider**.
|
||||
|
||||
<Danger>
|
||||
Deleting a provider permanently removes its scans, findings, resources, and other stored data. Confirm that the account has left the AWS Organization and that its historical data is no longer required before deleting it.
|
||||
</Danger>
|
||||
|
||||
### Update Organization Credentials
|
||||
|
||||
Choosing **Update Credentials** re-enters the Authentication Details step. Because the organization already holds a credential, Prowler warns before overwriting it and names how many providers re-authenticate with the new one:
|
||||
@@ -458,16 +478,17 @@ Deploy the ProwlerScan role to every member account with a [CloudFormation Stack
|
||||
</Note>
|
||||
|
||||
1. In your management account, navigate to **CloudFormation > StackSets > Create StackSet** ([open directly](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacksets/create)).
|
||||
2. Choose **Service-managed permissions** so AWS Organizations deploys the role automatically across current and future member accounts.
|
||||
3. Select **Amazon S3 URL** as the template source and paste:
|
||||
2. Choose **Service-managed permissions**.
|
||||
3. Enable **Automatic deployment** so CloudFormation deploys the role to accounts added to the targeted root or OUs. Configure the account removal behavior based on whether the stack and its resources should be retained when an account leaves the target.
|
||||
4. Select **Amazon S3 URL** as the template source and paste:
|
||||
```
|
||||
https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml
|
||||
```
|
||||
4. Set the **ExternalId** parameter to the External ID shown in the Prowler wizard.
|
||||
5. Choose your deployment targets (entire organization or specific OUs) and regions, then click **Create StackSet**.
|
||||
6. Open the **Stack instances** tab and confirm every instance shows **Status: CURRENT** and **Stack status: CREATE_COMPLETE**. Deployment typically takes **2–5 minutes**; large organizations (500+ accounts) may take longer.
|
||||
5. Set the **ExternalId** parameter to the External ID shown in the Prowler wizard.
|
||||
6. Choose your deployment targets (entire organization or specific OUs) and regions, then click **Create StackSet**.
|
||||
7. Open the **Stack instances** tab and confirm every instance shows **Status: CURRENT** and **Stack status: CREATE_COMPLETE**. Deployment typically takes **2–5 minutes**; large organizations (500+ accounts) may take longer.
|
||||
|
||||
The StackSet role uses read-only access only (`SecurityAudit`, `ViewOnlyAccess`, plus a small set of additional read-only permissions). Prowler makes no changes to your accounts. See the [CloudFormation template](https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml) for the full list. When you add new accounts under the targeted OU or root, the StackSet deploys the role automatically, and Prowler's 6-hour sync onboards them end-to-end.
|
||||
The StackSet role uses read-only access only (`SecurityAudit`, `ViewOnlyAccess`, plus a small set of additional read-only permissions). Prowler makes no changes to your accounts. See the [CloudFormation template](https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml) for the full list. When **Automatic deployment** is enabled, the StackSet deploys the role to new accounts under the targeted OU or root. Repeat the [organization discovery flow](#add-or-remove-organization-accounts) to connect those accounts in Prowler Cloud.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
|
||||
@@ -153,6 +153,18 @@ export PROWLER_CLOUD_API_KEY="pk_your_api_key_here"
|
||||
prowler aws --push-to-cloud
|
||||
```
|
||||
|
||||
### TLS Certificate Trust
|
||||
|
||||
For `--push-to-cloud` uploads, Prowler CLI creates one ingestion-scoped TLS context and validates HTTPS certificates with one handshake and one POST request. The upload does not retry or fall back to another TLS configuration. Redirect responses are rejected.
|
||||
|
||||
The ingestion context combines the operating system roots with the default certificate authority (CA) roots bundled with Requests. If `REQUESTS_CA_BUNDLE` is configured, Prowler CLI also loads that file or directory into the ingestion context. Otherwise, Prowler CLI loads `CURL_CA_BUNDLE` when configured.
|
||||
|
||||
`REQUESTS_CA_BUNDLE` and `CURL_CA_BUNDLE` can also affect other Requests-based connections throughout the Prowler CLI process, including provider authentication. A bundle containing only a private CA can cause connections to public services to fail before the upload starts. Installing the private CA in the operating system or container trust store is recommended. If a custom bundle is required, it must include both the public CA roots, such as the certifi bundle, and the required private CA certificates.
|
||||
|
||||
For Prowler Private Cloud deployments that use an organization CA or a TLS-intercepting corporate proxy, installing the required root CA in the operating system or container store remains the recommended approach. Containers have an isolated system CA store, so add the organization or proxy CA to the container image or runtime, then run the operating system's CA update command, such as `update-ca-certificates`, before starting Prowler CLI. Installing a CA on the container host does not automatically install it inside the container.
|
||||
|
||||
Prowler CLI does not create, modify, or remove these environment variables. The custom TLS context created by `--push-to-cloud` applies only to the temporary ingestion session and does not change API, provider, integration, global SSL, or unrelated Requests session behavior.
|
||||
|
||||
### Combining with Output Formats
|
||||
|
||||
When using `--push-to-cloud` with custom output formats that exclude OCSF, Prowler generates a temporary OCSF file for upload:
|
||||
|
||||
@@ -113,7 +113,8 @@ make test-mcp # Run the MCP test suite exactly as CI does
|
||||
- [ ] Models use `MinimalSerializerMixin`
|
||||
- [ ] API responses transformed to simplified models
|
||||
- [ ] No hardcoded secrets
|
||||
- [ ] Error handling returns structured responses
|
||||
- [ ] Failures are raised, not returned (see `prowler_mcp_server/lib/errors.py`);
|
||||
a returned error dict is reported to the client as a success
|
||||
- [ ] Parameter descriptions use Pydantic `Field()`
|
||||
- [ ] Tests added under `mcp_server/tests/`, mirroring the source path below the
|
||||
package root (`prowler_mcp_server/prowler_app/tools/` -> `tests/prowler_app/tools/`),
|
||||
|
||||
@@ -4,6 +4,24 @@ All notable changes to the **Prowler MCP Server** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [0.11.0] (Prowler v5.40.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Failures shared by every tool - a rejected credential, a missing permission, a rate limit, an outage, an unreachable API, a bad argument - are now explained with a message that says what went wrong and what to do about it [(#12531)](https://github.com/prowler-cloud/prowler/pull/12531)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- `prowler_docs_search` returns results again: it calls the search endpoint docs.prowler.com moved to, since the one it used no longer exists, and each result now names the page's title, the section it matched and a URL anchored at that section [(#12578)](https://github.com/prowler-cloud/prowler/pull/12578)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- Stop relaying upstream response bodies to agents: a failed request now reaches the caller as a sentence this server wrote, with the full body kept to the logs, so a gateway error page or a debug traceback can no longer be replayed into a model's context [(#12531)](https://github.com/prowler-cloud/prowler/pull/12531)
|
||||
- `sqlite-libs` upgraded to 3.53.4-r0 in the container image, patching CVE-2026-11822 and CVE-2026-11824 [(#12537)](https://github.com/prowler-cloud/prowler/pull/12537)
|
||||
- `libcrypto3` and `libssl3` upgraded to 3.5.8-r0 in the container image, patching CVE-2026-14456 [(#12547)](https://github.com/prowler-cloud/prowler/pull/12547)
|
||||
|
||||
---
|
||||
|
||||
## [0.10.0] (Prowler v5.38.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -29,6 +29,22 @@ FROM python:3.13.14-alpine3.23@sha256:9fdbf2e3e82628351513560b121e2ee6ce31cac212
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud"
|
||||
|
||||
# High CVEs fixed in Alpine 3.23 but not yet in the pinned base image:
|
||||
# sqlite-libs 3.53.4-r0 CVE-2026-11822, CVE-2026-11824 (image ships 3.51.2-r0)
|
||||
# libcrypto3/libssl3 3.5.8-r0 CVE-2026-14456 (image ships 3.5.7-r0)
|
||||
# The base image pins python 3.13.14, which has not been rebuilt since those
|
||||
# packages were published, so the upgrade is taken here rather than by moving
|
||||
# the pin -- the newest published python:3.13-alpine3.23 carries the same
|
||||
# vulnerable versions. libcrypto3 and libssl3 are both built from openssl and
|
||||
# are flagged separately, so both are named.
|
||||
# `>=` rather than `=`: Alpine keeps only the newest build of a package in a
|
||||
# branch's index, so an exact pin breaks this build the day one of these is
|
||||
# superseded. Drop an entry once the base image ships that version or later.
|
||||
RUN apk add --no-cache --upgrade \
|
||||
"sqlite-libs>=3.53.4-r0" \
|
||||
"libcrypto3>=3.5.8-r0" \
|
||||
"libssl3>=3.5.8-r0"
|
||||
|
||||
# Create non-root user for security
|
||||
# Using specific UID/GID for consistency across environments
|
||||
RUN addgroup -g 1001 prowler && \
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
"""Shared failure classification for every tool in this server."""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastmcp.exceptions import McpError, NotFoundError, ToolError
|
||||
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
|
||||
from pydantic import ValidationError
|
||||
|
||||
from prowler_mcp_server.lib.logger import logger
|
||||
|
||||
# ------------------------------------------------------------- failure types
|
||||
|
||||
|
||||
class ProwlerAPIError(Exception):
|
||||
"""An error response returned by the Prowler API.
|
||||
|
||||
Attributes:
|
||||
status_code: HTTP status the API answered with
|
||||
detail: JSON:API `errors[0].detail`, None when there is none to trust
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, message: str, status_code: int, *, detail: str | None = None
|
||||
) -> None:
|
||||
super().__init__(message)
|
||||
self.status_code: int = status_code
|
||||
# Prowler's own JSON:API `errors[0].detail`, which our API writes for a
|
||||
# caller and we therefore trust. None when the body was not JSON:API --
|
||||
# a gateway HTML page or a debug traceback, which is exactly the case
|
||||
# that must never be repeated to a model -- and None for a 5xx, see
|
||||
# `jsonapi_detail`.
|
||||
self.detail: str | None = detail
|
||||
|
||||
|
||||
class ProwlerAPIUnreachable(Exception):
|
||||
"""The request never got an answer, so whether it was applied is unknown."""
|
||||
|
||||
|
||||
class ProwlerAPIInvalidResponse(Exception):
|
||||
"""The API answered, but with a body this server could not read as JSON."""
|
||||
|
||||
|
||||
def jsonapi_detail(response: httpx.Response) -> str | None:
|
||||
"""Return the API's own JSON:API error detail, when there is one to trust.
|
||||
|
||||
Args:
|
||||
response: Error response returned by the Prowler API
|
||||
|
||||
Returns:
|
||||
`errors[0].detail`, or None if the status is 5xx or the body is not
|
||||
JSON:API
|
||||
"""
|
||||
if response.status_code >= 500:
|
||||
return None
|
||||
|
||||
try:
|
||||
errors = response.json().get("errors")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if not isinstance(errors, list) or not errors:
|
||||
return None
|
||||
|
||||
detail = errors[0].get("detail") if isinstance(errors[0], dict) else None
|
||||
return detail if isinstance(detail, str) and detail.strip() else None
|
||||
|
||||
|
||||
class InvalidArgument(ValueError):
|
||||
"""An argument this server rejected before any request went out."""
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- messages
|
||||
|
||||
|
||||
def _describe_prowler_api_error(exc: ProwlerAPIError) -> str:
|
||||
"""Describe a request the Prowler API answered with an error status."""
|
||||
status = exc.status_code
|
||||
|
||||
# The fallback: a status this server has nothing specific to say about.
|
||||
message = (
|
||||
f"Prowler rejected the request with status {status} and gave no reason. "
|
||||
"Check the arguments against the tool description."
|
||||
)
|
||||
|
||||
if status == 401:
|
||||
message = (
|
||||
"Prowler rejected this server's credential: it is missing, malformed "
|
||||
"or expired. In HTTP mode the request needs an 'Authorization: Bearer "
|
||||
"<token>' header; in STDIO mode PROWLER_API_KEY must hold a valid key."
|
||||
)
|
||||
elif status == 403:
|
||||
message = (
|
||||
"The credential is valid but not allowed to do this. Use "
|
||||
"prowler_get_current_user to see which role it holds."
|
||||
)
|
||||
elif status == 429:
|
||||
message = (
|
||||
"Prowler is rate limiting this credential. Wait before retrying, and "
|
||||
"narrow the request with tighter filters or a smaller page_size."
|
||||
)
|
||||
elif status >= 500:
|
||||
message = (
|
||||
f"Prowler answered {status}: the request failed on Prowler's side, "
|
||||
"not because of anything in the call."
|
||||
)
|
||||
elif exc.detail:
|
||||
# Written by the Prowler API for a caller to read, so it is ours to relay.
|
||||
message = f"Prowler rejected the request ({status}): {exc.detail}"
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def _describe_upstream_http_error(exc: httpx.HTTPError) -> str:
|
||||
"""Describe a failure from an upstream this server reads directly."""
|
||||
# `.request` raises rather than returning None when it was never set, and
|
||||
# this runs inside an exception handler, so it is read defensively.
|
||||
request = getattr(exc, "_request", None)
|
||||
host = request.url.host if request is not None else "the upstream service"
|
||||
|
||||
if not isinstance(exc, httpx.HTTPStatusError):
|
||||
return f"{host} could not be reached: {type(exc).__name__}."
|
||||
|
||||
status = exc.response.status_code
|
||||
if status == 429:
|
||||
return f"{host} is rate limiting this server. Wait before retrying."
|
||||
if status >= 500:
|
||||
return (
|
||||
f"{host} answered {status}: the request failed on its side, not "
|
||||
"because of anything in the call."
|
||||
)
|
||||
return (
|
||||
f"{host} rejected the request with status {status}. Check the arguments "
|
||||
"against the tool description."
|
||||
)
|
||||
|
||||
|
||||
def _describe_failure(exc: BaseException) -> str | None:
|
||||
"""Describe a failure for a model to read, or return None to leave it masked."""
|
||||
# InvalidArgument first: it is the only ValueError here whose message this
|
||||
# server wrote. The ones below quote the input they rejected, so they are
|
||||
# matched by type and answered with a message of our own.
|
||||
if isinstance(exc, InvalidArgument):
|
||||
return str(exc)
|
||||
|
||||
if isinstance(exc, ProwlerAPIError):
|
||||
return _describe_prowler_api_error(exc)
|
||||
|
||||
if isinstance(exc, ProwlerAPIInvalidResponse):
|
||||
return (
|
||||
"Prowler answered with a body this server could not read, so the "
|
||||
"outcome of the call is unknown. If it changes anything, check the "
|
||||
"current state before sending it again."
|
||||
)
|
||||
|
||||
if isinstance(exc, ProwlerAPIUnreachable):
|
||||
# The only failure a model can turn into a duplicate write by repeating.
|
||||
return (
|
||||
f"Prowler could not be reached: {exc}. Whether the request was "
|
||||
"applied is unknown, so check the current state before sending it again."
|
||||
)
|
||||
|
||||
if isinstance(exc, ValidationError):
|
||||
problems = [
|
||||
f"{'.'.join(str(part) for part in error['loc']) or '(argument)'}: {error['msg']}"
|
||||
for error in exc.errors(include_url=False)
|
||||
]
|
||||
# Field and expectation only: pydantic quotes the rejected value back.
|
||||
return f"Invalid arguments -- {'; '.join(problems)}."
|
||||
|
||||
if isinstance(exc, json.JSONDecodeError):
|
||||
return (
|
||||
"An argument that had to be a JSON object could not be parsed. Send "
|
||||
"it as a real object rather than as a quoted or escaped string."
|
||||
)
|
||||
|
||||
# Only the Hub and documentation tools reach here: the Prowler API client
|
||||
# converts its own httpx failures into the two types matched above.
|
||||
if isinstance(exc, (httpx.HTTPStatusError, httpx.RequestError)):
|
||||
return _describe_upstream_http_error(exc)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- middleware
|
||||
|
||||
|
||||
class SharedFailureMiddleware(Middleware):
|
||||
"""Replace the failures many tools share with a message a model can act on."""
|
||||
|
||||
async def on_call_tool(
|
||||
self,
|
||||
context: MiddlewareContext[Any],
|
||||
call_next: CallNext[Any, Any],
|
||||
) -> Any:
|
||||
"""Replace a shared tool failure with the message that describes it.
|
||||
|
||||
Args:
|
||||
context: Tool call being handled
|
||||
call_next: Rest of the middleware chain
|
||||
|
||||
Returns:
|
||||
The tool result when the call succeeded
|
||||
|
||||
Raises:
|
||||
ToolError: With the classified message when the failure is one this
|
||||
module recognises
|
||||
"""
|
||||
try:
|
||||
return await call_next(context)
|
||||
except (NotFoundError, McpError):
|
||||
# Protocol-level, not a tool failure. Must stay exactly as it is.
|
||||
raise
|
||||
except Exception as exc:
|
||||
# FastMCP wraps whatever the tool raised and records it as __cause__.
|
||||
# An absent cause means the message is already the final word: a
|
||||
# ToolError raised deliberately without a `from` clause.
|
||||
original = exc.__cause__
|
||||
message = _describe_failure(original) if original is not None else None
|
||||
|
||||
if message is None:
|
||||
if original is not None:
|
||||
logger.warning(
|
||||
"Tool %s failed with an unclassified error: %s: %s",
|
||||
getattr(context.message, "name", "<unknown>"),
|
||||
type(original).__name__,
|
||||
original,
|
||||
)
|
||||
raise
|
||||
|
||||
logger.warning(
|
||||
"Tool %s failed: %s: %s",
|
||||
getattr(context.message, "name", "<unknown>"),
|
||||
type(original).__name__,
|
||||
original,
|
||||
)
|
||||
raise ToolError(message) from exc
|
||||
@@ -7,28 +7,22 @@ from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
from fastmcp.exceptions import ToolError
|
||||
|
||||
from prowler_mcp_server import __version__
|
||||
from prowler_mcp_server.lib.errors import (
|
||||
InvalidArgument,
|
||||
ProwlerAPIError,
|
||||
ProwlerAPIInvalidResponse,
|
||||
ProwlerAPIUnreachable,
|
||||
jsonapi_detail,
|
||||
)
|
||||
from prowler_mcp_server.lib.logger import logger
|
||||
from prowler_mcp_server.prowler_app.utils.auth import ProwlerAppAuth
|
||||
|
||||
ALLOWED_EXTERNAL_DOMAINS: frozenset[str] = frozenset({"raw.githubusercontent.com"})
|
||||
|
||||
|
||||
class ProwlerAPIError(Exception):
|
||||
"""An error response returned by the Prowler API.
|
||||
|
||||
Raised only when the API answered with an error status, which tells a caller
|
||||
something no plain exception can: the request reached Prowler and was
|
||||
rejected, so it changed nothing. A timeout or a dropped connection stays a
|
||||
bare exception because the request may well have been processed.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, status_code: int) -> None:
|
||||
super().__init__(message)
|
||||
self.status_code: int = status_code
|
||||
|
||||
|
||||
class HTTPMethod(StrEnum):
|
||||
"""HTTP methods enum."""
|
||||
|
||||
@@ -88,7 +82,8 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
|
||||
Raises:
|
||||
ProwlerAPIError: If the API answered with an error status
|
||||
Exception: If the request could not be completed
|
||||
ProwlerAPIUnreachable: If the request got no answer
|
||||
ProwlerAPIInvalidResponse: If the answer was not readable as JSON
|
||||
"""
|
||||
try:
|
||||
token: str = await self.auth_manager.get_valid_token()
|
||||
@@ -103,31 +98,61 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
json=json_data,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
if not response.content:
|
||||
return {
|
||||
"success": True,
|
||||
"status_code": response.status_code,
|
||||
}
|
||||
else:
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"HTTP error during {method.value} {path}: {e}")
|
||||
error_detail: str = ""
|
||||
try:
|
||||
error_data: dict[str, any] = e.response.json()
|
||||
error_detail = error_data.get("errors", [{}])[0].get("detail", "")
|
||||
except Exception:
|
||||
error_detail = e.response.text
|
||||
|
||||
raise ProwlerAPIError(
|
||||
f"API request failed: {e.response.status_code} - {error_detail}",
|
||||
e.response.status_code,
|
||||
status: int = e.response.status_code
|
||||
# `jsonapi_detail` returns nothing for a 5xx, so a server error never
|
||||
# puts upstream text into the exception message either.
|
||||
detail: str | None = jsonapi_detail(e.response)
|
||||
# The full body goes to the log and nowhere else. A body that is not
|
||||
# JSON:API is upstream text of unknown provenance, and the exception
|
||||
# message is read by a model.
|
||||
logger.error(
|
||||
"HTTP error during %s %s: %s %s",
|
||||
method.value,
|
||||
path,
|
||||
status,
|
||||
(e.response.text or "")[:500],
|
||||
)
|
||||
|
||||
message = f"API request failed: {status}"
|
||||
if detail:
|
||||
message = f"{message} - {detail}"
|
||||
|
||||
raise ProwlerAPIError(message, status, detail=detail) from e
|
||||
except httpx.RequestError as e:
|
||||
# No answer came back, so whether the request was applied is unknown.
|
||||
logger.error(f"Error during {method.value} {path}: {e}")
|
||||
raise ProwlerAPIUnreachable(
|
||||
f"{method.value} {path} got no answer: {type(e).__name__}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
logger.error(f"Error during {method.value} {path}: {e}")
|
||||
raise
|
||||
|
||||
if not response.content:
|
||||
return {
|
||||
"success": True,
|
||||
"status_code": response.status_code,
|
||||
}
|
||||
|
||||
# Parsed outside the block above so that a body we cannot read is told
|
||||
# apart from an argument a tool could not parse: both are a
|
||||
# `JSONDecodeError`, and only the second one is the caller's doing.
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError as e:
|
||||
logger.error(
|
||||
"Unreadable response body during %s %s: %s %s",
|
||||
method.value,
|
||||
path,
|
||||
response.status_code,
|
||||
(response.text or "")[:500],
|
||||
)
|
||||
raise ProwlerAPIInvalidResponse(
|
||||
f"{method.value} {path} answered {response.status_code} with a "
|
||||
"body that is not JSON"
|
||||
) from e
|
||||
|
||||
async def get(
|
||||
self, path: str, params: dict[str, any] | None = None
|
||||
) -> dict[str, any]:
|
||||
@@ -229,14 +254,14 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
Raw text content from the URL
|
||||
|
||||
Raises:
|
||||
ValueError: If the URL domain is not in the allowlist
|
||||
Exception: If the HTTP request fails
|
||||
InvalidArgument: If the URL scheme or domain is not allowed
|
||||
ToolError: If the fetch failed
|
||||
"""
|
||||
parsed = urlparse(url)
|
||||
if parsed.scheme != "https":
|
||||
raise ValueError(f"Only HTTPS URLs are allowed, got '{parsed.scheme}'")
|
||||
raise InvalidArgument(f"Only HTTPS URLs are allowed, got '{parsed.scheme}'")
|
||||
if parsed.hostname not in ALLOWED_EXTERNAL_DOMAINS:
|
||||
raise ValueError(
|
||||
raise InvalidArgument(
|
||||
f"Domain '{parsed.hostname}' is not allowed. "
|
||||
f"Allowed domains: {', '.join(sorted(ALLOWED_EXTERNAL_DOMAINS))}"
|
||||
)
|
||||
@@ -249,13 +274,20 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"HTTP error fetching external URL {url}: {e}")
|
||||
raise Exception(
|
||||
f"Failed to fetch external URL: {e.response.status_code}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
# The status is ours to report; the body is upstream text and stays
|
||||
# in the log. No `from` clause: this sentence is the final word.
|
||||
logger.error(
|
||||
"HTTP error fetching external URL %s: %s %s",
|
||||
url,
|
||||
e.response.status_code,
|
||||
(e.response.text or "")[:500],
|
||||
)
|
||||
raise ToolError(
|
||||
f"Fetching {url} failed with status {e.response.status_code}."
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Error fetching external URL {url}: {e}")
|
||||
raise
|
||||
raise ToolError(f"Fetching {url} got no answer: {type(e).__name__}.")
|
||||
|
||||
async def poll_task_until_complete(
|
||||
self,
|
||||
@@ -278,7 +310,7 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
The complete task response when terminal state is reached
|
||||
|
||||
Raises:
|
||||
Exception: If task fails, is cancelled, or timeout is exceeded
|
||||
ToolError: If the task fails, is cancelled, or the timeout is exceeded
|
||||
"""
|
||||
terminal_states = {"completed", "failed", "cancelled"}
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
@@ -292,7 +324,7 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
# Check if we've exceeded the timeout
|
||||
current_time = asyncio.get_event_loop().time()
|
||||
if current_time >= max_time:
|
||||
raise Exception(
|
||||
raise ToolError(
|
||||
f"Task {task_id} polling timed out after {timeout} seconds. "
|
||||
f"The task may still be running. Try increasing the timeout or check task status manually."
|
||||
)
|
||||
@@ -311,10 +343,14 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
logger.info(f"Task {task_id} completed successfully")
|
||||
return response
|
||||
elif state == "failed":
|
||||
error_msg = task_attrs.get("error", "Unknown error")
|
||||
raise Exception(f"Task {task_id} failed: {error_msg}")
|
||||
# The task's own failure text is an upstream body: a celery
|
||||
# traceback, a provider message. Log it, never relay it.
|
||||
logger.error(
|
||||
f"Task {task_id} failed: {task_attrs.get('error', 'no error reported')}"
|
||||
)
|
||||
raise ToolError(f"Task {task_id} failed.")
|
||||
elif state == "cancelled":
|
||||
raise Exception(f"Task {task_id} was cancelled")
|
||||
raise ToolError(f"Task {task_id} was cancelled")
|
||||
|
||||
# Wait before next poll
|
||||
await asyncio.sleep(poll_interval)
|
||||
@@ -330,12 +366,12 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
Parsed datetime object
|
||||
|
||||
Raises:
|
||||
ValueError: If date format is invalid
|
||||
InvalidArgument: If date format is invalid
|
||||
"""
|
||||
try:
|
||||
return datetime.strptime(date_str, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
raise InvalidArgument(
|
||||
f"Invalid date format for {param_name}. Expected YYYY-MM-DD (e.g., '2025-01-15'), got '{date_str}'. "
|
||||
f"Full date required - partial dates like '2025' or '2025-01' are not accepted."
|
||||
)
|
||||
@@ -347,10 +383,10 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
page_size: Page size to validate
|
||||
|
||||
Raises:
|
||||
ValueError: If page size is out of valid range (1-1000)
|
||||
InvalidArgument: If page size is out of valid range (1-1000)
|
||||
"""
|
||||
if page_size < 1 or page_size > 1000:
|
||||
raise ValueError(
|
||||
raise InvalidArgument(
|
||||
f"Invalid page_size: {page_size}. Must be between 1 and 1000 (inclusive)."
|
||||
)
|
||||
|
||||
@@ -373,7 +409,7 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
None if no dates provided, otherwise tuple of (date_from, date_to) as strings
|
||||
|
||||
Raises:
|
||||
ValueError: If date range exceeds max_days or date format is invalid
|
||||
InvalidArgument: If date range exceeds max_days or date format is invalid
|
||||
"""
|
||||
if not date_from and not date_to:
|
||||
return None
|
||||
@@ -394,7 +430,7 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
|
||||
# Validate that date_from is before or equal to date_to
|
||||
if from_date > to_date:
|
||||
raise ValueError(
|
||||
raise InvalidArgument(
|
||||
f"Invalid date range: date_from must be before or equal to date_to. "
|
||||
f"Got date_from='{from_date.date()}' and date_to='{to_date.date()}'. "
|
||||
f"Please swap the dates or use the correct order."
|
||||
@@ -403,7 +439,7 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
# Validate range doesn't exceed max_days
|
||||
delta: int = (to_date - from_date).days + 1
|
||||
if delta > max_days:
|
||||
raise ValueError(
|
||||
raise InvalidArgument(
|
||||
f"Date range cannot exceed {max_days} days. "
|
||||
f"Requested range: {from_date.date()} to {to_date.date()} ({delta} days)"
|
||||
)
|
||||
|
||||
@@ -8,12 +8,21 @@ class SearchResult(BaseModel):
|
||||
"""Search result model."""
|
||||
|
||||
path: str = Field(description="Document path")
|
||||
title: str = Field(description="Document title")
|
||||
url: str = Field(description="Documentation URL")
|
||||
highlights: list[str] = Field(
|
||||
description="Highlighted content snippets showing query matches with <mark><b> tags",
|
||||
title: str = Field(description="Title of the page the match is on")
|
||||
section: str = Field(
|
||||
description="Heading of the section the match is in", default=""
|
||||
)
|
||||
breadcrumbs: list[str] = Field(
|
||||
description="Where the page sits in the documentation, from the top-level group down to the page itself",
|
||||
default_factory=list,
|
||||
)
|
||||
url: str = Field(
|
||||
description="Documentation URL, anchored at the matching section when it has an anchor"
|
||||
)
|
||||
excerpt: str = Field(
|
||||
description="Text of the matching section, which is a part of the page and not the whole of it",
|
||||
default="",
|
||||
)
|
||||
score: float = Field(
|
||||
description="Relevance score for the search result", default=0.0
|
||||
)
|
||||
@@ -24,12 +33,10 @@ class ProwlerDocsSearchEngine:
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the search engine."""
|
||||
self.api_base_url = (
|
||||
"https://api.mintlifytrieve.com/api/chunk_group/group_oriented_autocomplete"
|
||||
)
|
||||
self.dataset_id = "0096ba11-3f72-463b-9d95-b788495ac392"
|
||||
self.api_key = "tr-T6JLeTkFXeNbNPyhijtI9XhIncydQQ3O"
|
||||
self.docs_base_url = "https://prowler.mintlify.app"
|
||||
# The endpoint docs.prowler.com itself calls, with the site's Mintlify
|
||||
# project name as the last segment.
|
||||
self.api_base_url = "https://leaves.mintlify.com/api/search/prowler"
|
||||
self.docs_base_url = "https://docs.prowler.com"
|
||||
|
||||
# HTTP client for Mintlify API
|
||||
self.mintlify_client = httpx.Client(
|
||||
@@ -38,9 +45,6 @@ class ProwlerDocsSearchEngine:
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"User-Agent": f"prowler-mcp-server/{__version__}",
|
||||
"TR-Dataset": self.dataset_id,
|
||||
"Authorization": self.api_key,
|
||||
"X-API-Version": "V2",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -59,75 +63,51 @@ class ProwlerDocsSearchEngine:
|
||||
|
||||
Args:
|
||||
query: Search query string
|
||||
page_size: Maximum number of results to return
|
||||
page_size: Maximum number of results to return. The API decides how
|
||||
many matches it answers with and takes no size of its own, so
|
||||
this only trims the list it returned.
|
||||
|
||||
Returns:
|
||||
list of search results
|
||||
"""
|
||||
try:
|
||||
# Construct request body
|
||||
payload = {
|
||||
"query": query,
|
||||
"search_type": "fulltext",
|
||||
"extend_results": True,
|
||||
"highlight_options": {
|
||||
"highlight_window": 10,
|
||||
"highlight_max_num": 1,
|
||||
"highlight_max_length": 2,
|
||||
"highlight_strategy": "exactmatch",
|
||||
"highlight_delimiters": ["?", ",", ".", "!", "\n"],
|
||||
},
|
||||
"score_threshold": 0.2,
|
||||
"filters": {"must_not": [{"field": "tag_set", "match": ["code"]}]},
|
||||
"page_size": page_size,
|
||||
"group_size": 3,
|
||||
}
|
||||
|
||||
# Make request to Mintlify API
|
||||
response = self.mintlify_client.post(
|
||||
self.api_base_url,
|
||||
json=payload,
|
||||
json={"query": query, "filters": {}},
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Parse results
|
||||
results = []
|
||||
for result in data.get("results", []):
|
||||
group = result.get("group", {})
|
||||
chunks = result.get("chunks", [])
|
||||
for match in data.get("results", [])[:page_size]:
|
||||
metadata = match.get("metadata", {})
|
||||
breadcrumbs = metadata.get("breadcrumbs", [])
|
||||
doc_path = match.get("page", "")
|
||||
|
||||
# Get document path and title from group
|
||||
doc_path = group.get("name", "")
|
||||
group_title = group.get("name", "").replace("/", " / ").title()
|
||||
# A match is one section of a page rather than the page: the
|
||||
# heading it was found under is its header, and the page's own
|
||||
# title is the last step of its breadcrumb trail.
|
||||
section = match.get("header", "")
|
||||
title = breadcrumbs[-1] if breadcrumbs else section
|
||||
|
||||
# If chunks exist, use the first chunk's title from metadata
|
||||
title = group_title
|
||||
if chunks:
|
||||
first_chunk = chunks[0].get("chunk", {})
|
||||
metadata = first_chunk.get("metadata", {})
|
||||
title = metadata.get("title", group_title)
|
||||
|
||||
# Construct full URL to docs
|
||||
full_url = f"{self.docs_base_url}/{doc_path}"
|
||||
|
||||
# Extract highlights and scores from chunks
|
||||
highlights = []
|
||||
max_score = 0.0
|
||||
for chunk_data in chunks:
|
||||
chunk_highlights = chunk_data.get("highlights", [])
|
||||
highlights.extend(chunk_highlights)
|
||||
# Track the highest score among all chunks in this group
|
||||
chunk_score = chunk_data.get("score", 0.0)
|
||||
max_score = max(max_score, chunk_score)
|
||||
# Sent as "" for the section a page opens with and as null for
|
||||
# the pages that have no anchors at all; both mean the page.
|
||||
anchor = metadata.get("hash")
|
||||
url = f"{self.docs_base_url}/{doc_path}"
|
||||
if anchor:
|
||||
url = f"{url}#{anchor}"
|
||||
|
||||
results.append(
|
||||
SearchResult(
|
||||
path=doc_path,
|
||||
title=title,
|
||||
url=full_url,
|
||||
highlights=highlights,
|
||||
score=max_score,
|
||||
section=section,
|
||||
breadcrumbs=breadcrumbs,
|
||||
url=url,
|
||||
excerpt=match.get("content", ""),
|
||||
score=match.get("score", 0.0),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ def search(
|
||||
term: str = Field(description="The term to search for in the documentation"),
|
||||
page_size: int = Field(
|
||||
5,
|
||||
description="Number of top results to return to return. It must be between 1 and 20.",
|
||||
gt=1,
|
||||
lt=20,
|
||||
description="Number of top results to return. It must be between 1 and 20.",
|
||||
ge=1,
|
||||
le=20,
|
||||
),
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Search in Prowler documentation.
|
||||
@@ -27,11 +27,12 @@ def search(
|
||||
This tool searches through the official Prowler documentation
|
||||
to find relevant information about everything related to Prowler.
|
||||
|
||||
Uses fulltext search to find the most relevant documentation pages
|
||||
based on your query.
|
||||
A result is one section of a documentation page, not the page itself: its
|
||||
'excerpt' is that section alone. Read the whole page with
|
||||
`prowler_docs_get_document`, passing the result's 'path'.
|
||||
|
||||
Returns:
|
||||
List of search results with highlights showing matched terms (in <mark><b> tags)
|
||||
List of matching documentation sections, most relevant first
|
||||
"""
|
||||
return prowler_docs_search_engine.search(term, page_size) # type: ignore In the hint we cannot put SearchResult type because JSON API MCP Generator cannot handle Pydantic models yet
|
||||
|
||||
|
||||
@@ -2,9 +2,17 @@ from fastmcp import FastMCP
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from prowler_mcp_server import __version__
|
||||
from prowler_mcp_server.lib.errors import SharedFailureMiddleware
|
||||
from prowler_mcp_server.lib.logger import logger
|
||||
|
||||
prowler_mcp_server = FastMCP("prowler-mcp-server")
|
||||
# `mask_error_details` keeps an unhandled failure from relaying text this server
|
||||
# does not control. It is set on every sub-server as well, because it does not
|
||||
# reach mounted children -- FastMCP warns about exactly that at mount time.
|
||||
prowler_mcp_server = FastMCP("prowler-mcp-server", mask_error_details=True)
|
||||
|
||||
# Middleware, unlike masking, does reach mounted children, so the classifier that
|
||||
# gives the masked failures a sentence back is wired here once.
|
||||
prowler_mcp_server.add_middleware(SharedFailureMiddleware())
|
||||
|
||||
|
||||
def setup_main_server():
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Tests for the shared failure classifier.
|
||||
|
||||
Two properties are pinned here: a failed tool call answers with ``isError: true``
|
||||
rather than a result object the client reads as a success, and the only text that
|
||||
reaches a model is text this server produced.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from fastmcp import Client
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from prowler_mcp_server.lib.errors import InvalidArgument, _describe_failure
|
||||
from prowler_mcp_server.prowler_app.utils.api_client import (
|
||||
ProwlerAPIError,
|
||||
ProwlerAPIInvalidResponse,
|
||||
ProwlerAPIUnreachable,
|
||||
)
|
||||
from tests.helpers.jsonapi import jsonapi_error
|
||||
|
||||
LATEST = "/api/v1/findings/latest"
|
||||
|
||||
|
||||
# ------------------------------------------------------------ classification
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status", "expected"),
|
||||
[
|
||||
(401, "missing, malformed or expired"),
|
||||
(403, "prowler_get_current_user"),
|
||||
(429, "rate limiting"),
|
||||
(503, "failed on Prowler's side"),
|
||||
],
|
||||
ids=["unauthorized", "forbidden", "rate-limited", "unavailable"],
|
||||
)
|
||||
def test_the_failures_every_authenticated_tool_shares_get_one_message(status, expected):
|
||||
"""These four mean the same thing whichever tool hit them."""
|
||||
message = _describe_failure(ProwlerAPIError("failed", status))
|
||||
|
||||
assert expected in message
|
||||
|
||||
|
||||
def test_a_rejection_relays_the_apis_own_reason():
|
||||
"""`errors[].detail` is written by Prowler for a caller, so it is ours to relay."""
|
||||
message = _describe_failure(
|
||||
ProwlerAPIError("failed", 400, detail="scan_id is not a valid UUID.")
|
||||
)
|
||||
|
||||
assert "scan_id is not a valid UUID." in message
|
||||
|
||||
|
||||
def test_a_rejection_with_no_trustworthy_reason_says_so_instead_of_guessing():
|
||||
"""A body that was not JSON:API leaves `detail` unset, and it stays unrelayed."""
|
||||
message = _describe_failure(ProwlerAPIError("failed", 400))
|
||||
|
||||
assert "gave no reason" in message
|
||||
|
||||
|
||||
def test_a_request_that_got_no_answer_says_the_outcome_is_unknown():
|
||||
"""An unanswered write may well have landed, so repeating it can duplicate it."""
|
||||
message = _describe_failure(ProwlerAPIUnreachable("POST /providers got no answer"))
|
||||
|
||||
assert "could not be reached" in message
|
||||
assert "unknown" in message
|
||||
|
||||
|
||||
def test_an_unreadable_api_answer_is_not_blamed_on_the_arguments():
|
||||
"""The same `JSONDecodeError` means opposite things on the two sides."""
|
||||
message = _describe_failure(
|
||||
ProwlerAPIInvalidResponse(
|
||||
"GET /findings answered 200 with a body that is not JSON"
|
||||
)
|
||||
)
|
||||
|
||||
assert "could not read" in message
|
||||
assert "argument" not in message
|
||||
|
||||
|
||||
def test_an_unreadable_api_answer_is_never_called_safe_to_repeat():
|
||||
"""`post`, `patch` and `delete` reach this too, and a write may have landed."""
|
||||
message = _describe_failure(
|
||||
ProwlerAPIInvalidResponse(
|
||||
"POST /providers answered 201 with a body that is not JSON"
|
||||
)
|
||||
)
|
||||
|
||||
assert "unknown" in message
|
||||
assert "check the current state" in message
|
||||
|
||||
|
||||
def test_an_argument_this_server_rejected_is_repeated_verbatim():
|
||||
"""`InvalidArgument` exists to mark a message as one we wrote."""
|
||||
message = _describe_failure(
|
||||
InvalidArgument("page_size must be between 1 and 1000.")
|
||||
)
|
||||
|
||||
assert message == "page_size must be between 1 and 1000."
|
||||
|
||||
|
||||
def test_a_pydantic_rejection_names_the_field_without_echoing_the_value():
|
||||
"""Pydantic quotes `input_value` back, and these tools take credentials."""
|
||||
|
||||
class Credentials(BaseModel):
|
||||
api_token: int
|
||||
|
||||
with pytest.raises(ValidationError) as raised:
|
||||
Credentials(api_token="hunter2-the-real-secret")
|
||||
|
||||
message = _describe_failure(raised.value)
|
||||
|
||||
assert "api_token" in message
|
||||
assert "hunter2-the-real-secret" not in message
|
||||
|
||||
|
||||
def test_unparseable_json_is_reported_without_quoting_the_input():
|
||||
"""`JSONDecodeError` is a ValueError whose message quotes what it was given."""
|
||||
with pytest.raises(json.JSONDecodeError) as raised:
|
||||
json.loads('{"api_token": "hunter2-the-real-secret"')
|
||||
|
||||
message = _describe_failure(raised.value)
|
||||
|
||||
assert "could not be parsed" in message
|
||||
assert "hunter2" not in message
|
||||
|
||||
|
||||
def test_an_unrecognised_failure_is_left_masked():
|
||||
"""Saying nothing is the safe default; the alternative is relaying anything."""
|
||||
assert (
|
||||
_describe_failure(RuntimeError("connection pool exhausted at 10.0.0.4:5432"))
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------- through the server
|
||||
|
||||
|
||||
async def test_a_failing_tool_answers_with_is_error_not_a_result(
|
||||
mcp_root_server, mock_api_client, mock_router
|
||||
):
|
||||
"""An error dict would arrive as `isError: false` and read as a success."""
|
||||
mock_router.add("GET", LATEST, status=403, json=jsonapi_error(403, "Denied."))
|
||||
|
||||
async with Client(mcp_root_server) as client:
|
||||
result = await client.call_tool_mcp("prowler_search_security_findings", {})
|
||||
|
||||
assert result.isError is True
|
||||
assert result.structuredContent is None
|
||||
assert "prowler_get_current_user" in result.content[0].text
|
||||
|
||||
|
||||
async def test_an_upstream_body_never_reaches_the_agent(
|
||||
mcp_root_server, mock_api_client, mock_router
|
||||
):
|
||||
"""A body this server did not write is logged and replaced, never relayed."""
|
||||
mock_router.add(
|
||||
"GET",
|
||||
LATEST,
|
||||
status=500,
|
||||
text="Traceback: psycopg2 could not connect to internal-db:5432",
|
||||
)
|
||||
|
||||
async with Client(mcp_root_server) as client:
|
||||
result = await client.call_tool_mcp("prowler_search_security_findings", {})
|
||||
|
||||
assert result.isError is True
|
||||
assert "internal-db" not in result.content[0].text
|
||||
assert "failed on Prowler's side" in result.content[0].text
|
||||
|
||||
|
||||
async def test_a_bad_argument_is_rejected_before_any_request_goes_out(
|
||||
mcp_root_server, mock_api_client, mock_router
|
||||
):
|
||||
"""Local validation saves a round trip, and its message is safe to repeat."""
|
||||
async with Client(mcp_root_server) as client:
|
||||
result = await client.call_tool_mcp(
|
||||
"prowler_search_security_findings", {"page_size": 5000}
|
||||
)
|
||||
|
||||
assert result.isError is True
|
||||
assert "Must be between 1 and 1000" in result.content[0].text
|
||||
assert mock_router.requests == []
|
||||
|
||||
|
||||
async def test_an_unreadable_api_answer_does_not_reach_the_agent_as_a_bad_argument(
|
||||
mcp_root_server, mock_api_client, mock_router
|
||||
):
|
||||
"""Told apart by type, so the agent is not sent to fix an argument that is fine."""
|
||||
mock_router.add("GET", LATEST, text="<html>gateway timeout</html>")
|
||||
|
||||
async with Client(mcp_root_server) as client:
|
||||
result = await client.call_tool_mcp("prowler_search_security_findings", {})
|
||||
|
||||
assert result.isError is True
|
||||
assert "gateway timeout" not in result.content[0].text
|
||||
assert "argument" not in result.content[0].text
|
||||
@@ -7,9 +7,19 @@ query encoding and header assembly stay covered.
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastmcp.exceptions import ToolError
|
||||
|
||||
from prowler_mcp_server.prowler_app.utils.api_client import ProwlerAPIError
|
||||
from tests.helpers.jsonapi import jsonapi_collection, jsonapi_error, jsonapi_resource
|
||||
from prowler_mcp_server.prowler_app.utils.api_client import (
|
||||
ProwlerAPIError,
|
||||
ProwlerAPIInvalidResponse,
|
||||
ProwlerAPIUnreachable,
|
||||
)
|
||||
from tests.helpers.jsonapi import (
|
||||
jsonapi_collection,
|
||||
jsonapi_error,
|
||||
jsonapi_resource,
|
||||
task_document,
|
||||
)
|
||||
from tests.helpers.tokens import FAKE_API_KEY
|
||||
|
||||
|
||||
@@ -46,11 +56,7 @@ async def test_get_forwards_query_parameters(mock_api_client, mock_router):
|
||||
|
||||
|
||||
async def test_error_response_surfaces_the_jsonapi_detail(mock_api_client, mock_router):
|
||||
"""A failed request is raised with the API's own `errors[].detail` message.
|
||||
|
||||
Tools relay this text straight to the model, so losing it turns an actionable
|
||||
error into an opaque one.
|
||||
"""
|
||||
"""A failed request carries the API's own `errors[].detail`."""
|
||||
mock_router.add(
|
||||
"GET",
|
||||
"/api/v1/findings/nope",
|
||||
@@ -64,26 +70,112 @@ async def test_error_response_surfaces_the_jsonapi_detail(mock_api_client, mock_
|
||||
await mock_api_client.get("/findings/nope")
|
||||
|
||||
assert raised.value.status_code == 404
|
||||
assert raised.value.detail == "Not found."
|
||||
|
||||
|
||||
async def test_a_body_that_is_not_jsonapi_is_never_repeated(
|
||||
mock_api_client, mock_router
|
||||
):
|
||||
"""A body that is not JSON:API leaves `detail` unset, so nothing is relayed."""
|
||||
mock_router.add(
|
||||
"GET",
|
||||
"/api/v1/findings",
|
||||
status=502,
|
||||
text="<html><body>Traceback: secret-internal-host:5432</body></html>",
|
||||
)
|
||||
|
||||
with pytest.raises(ProwlerAPIError) as raised:
|
||||
await mock_api_client.get("/findings")
|
||||
|
||||
assert raised.value.detail is None
|
||||
assert "secret-internal-host" not in str(raised.value)
|
||||
|
||||
|
||||
async def test_a_server_error_detail_never_reaches_the_exception_text(
|
||||
mock_api_client, mock_router
|
||||
):
|
||||
"""On a 5xx `errors[].detail` carries the failure, not a reason for a caller.
|
||||
|
||||
Tools that answer with `str(exc)` bypass the shared classifier, so the check
|
||||
is on the exception itself rather than on the message the classifier builds.
|
||||
"""
|
||||
mock_router.add(
|
||||
"GET",
|
||||
"/api/v1/findings",
|
||||
status=500,
|
||||
json=jsonapi_error(
|
||||
500, "OperationalError: could not connect to secret-internal-host:5432"
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(ProwlerAPIError) as raised:
|
||||
await mock_api_client.get("/findings")
|
||||
|
||||
assert raised.value.detail is None
|
||||
assert "secret-internal-host" not in str(raised.value)
|
||||
|
||||
|
||||
async def test_an_unreadable_body_is_not_an_argument_failure(
|
||||
mock_api_client, mock_router
|
||||
):
|
||||
"""A `JSONDecodeError` here is the API's doing, and must not read as ours."""
|
||||
mock_router.add("GET", "/api/v1/findings", text="<html>gateway timeout</html>")
|
||||
|
||||
with pytest.raises(ProwlerAPIInvalidResponse) as raised:
|
||||
await mock_api_client.get("/findings")
|
||||
|
||||
assert not isinstance(raised.value, ValueError)
|
||||
assert "gateway timeout" not in str(raised.value)
|
||||
|
||||
|
||||
async def test_a_mutation_with_an_unreadable_answer_still_raises(
|
||||
mock_api_client, mock_router
|
||||
):
|
||||
"""`post` shares the parse, so a write cannot answer with an unread body."""
|
||||
mock_router.add(
|
||||
"POST", "/api/v1/providers", status=201, text="<html>accepted</html>"
|
||||
)
|
||||
|
||||
with pytest.raises(ProwlerAPIInvalidResponse):
|
||||
await mock_api_client.post("/providers", json_data={"data": {}})
|
||||
|
||||
|
||||
async def test_a_request_that_got_no_answer_is_not_an_api_error(
|
||||
mock_api_client, mock_router
|
||||
):
|
||||
"""`ProwlerAPIError` means the API answered, and callers act on that.
|
||||
|
||||
A write tool tells a rejected request -- which changed nothing -- from one
|
||||
that may have been processed by the type of the failure, so a timeout must
|
||||
not be dressed up as a rejection.
|
||||
"""
|
||||
"""`ProwlerAPIError` means the API answered, so a timeout must not use it."""
|
||||
|
||||
def timed_out(request):
|
||||
raise httpx.ReadTimeout("Timed out reading the response", request=request)
|
||||
|
||||
mock_router.add_handler("GET", "/api/v1/findings", timed_out)
|
||||
|
||||
with pytest.raises(httpx.ReadTimeout):
|
||||
with pytest.raises(ProwlerAPIUnreachable) as raised:
|
||||
await mock_api_client.get("/findings")
|
||||
|
||||
assert not isinstance(raised.value, ProwlerAPIError)
|
||||
|
||||
|
||||
async def test_a_failed_task_does_not_relay_its_own_error_text(
|
||||
mock_api_client, mock_router
|
||||
):
|
||||
"""A failed task's error is an upstream body, so polling must not repeat it."""
|
||||
mock_router.add(
|
||||
"GET",
|
||||
"/api/v1/tasks/t1",
|
||||
json=task_document(
|
||||
"t1",
|
||||
"failed",
|
||||
error="Traceback: connection to secret-internal-host:5432 refused",
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(ToolError) as raised:
|
||||
await mock_api_client.poll_task_until_complete(task_id="t1", timeout=5)
|
||||
|
||||
assert "secret-internal-host" not in str(raised.value)
|
||||
assert "t1" in str(raised.value)
|
||||
|
||||
|
||||
def test_build_filter_params_normalises_types_for_the_api(mock_api_client):
|
||||
"""Booleans become lowercase strings, sequences become CSV, `None` is dropped."""
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Tests for the Prowler documentation search tool.
|
||||
|
||||
Mintlify moved the docs search to a new endpoint that answers with page
|
||||
sections, so a result is a part of a page and has to read as one.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from fastmcp import Client
|
||||
|
||||
SEARCH = "/api/search/prowler"
|
||||
|
||||
|
||||
def search_match(
|
||||
path: str = "getting-started/installation",
|
||||
*,
|
||||
header: str = "Requirements",
|
||||
breadcrumbs: tuple[str, ...] = ("Get Started", "Installation"),
|
||||
anchor: str | None = "requirements",
|
||||
score: float = 4.9,
|
||||
):
|
||||
"""One match as Mintlify answers with it: a section of a page, not the page."""
|
||||
return {
|
||||
"page": path,
|
||||
"header": header,
|
||||
"content": "Prowler runs on Python 3.9 or later.",
|
||||
"metadata": {
|
||||
"title": header,
|
||||
"breadcrumbs": list(breadcrumbs),
|
||||
"icon": "",
|
||||
"hash": anchor,
|
||||
"openapi": "",
|
||||
},
|
||||
"score": score,
|
||||
}
|
||||
|
||||
|
||||
def stub_search_hit(docs_router, *matches):
|
||||
"""Serve the search endpoint, with one default match when none are given."""
|
||||
if not matches:
|
||||
matches = (search_match(),)
|
||||
return docs_router.add("POST", SEARCH, json={"results": list(matches)})
|
||||
|
||||
|
||||
async def test_search_returns_the_matching_sections(mcp_root_server, docs_router):
|
||||
"""Every field of a result, since the shape of one changed with the endpoint."""
|
||||
stub_search_hit(docs_router)
|
||||
|
||||
async with Client(mcp_root_server) as client:
|
||||
result = await client.call_tool("prowler_docs_search", {"term": "install"})
|
||||
|
||||
match = result.data[0]
|
||||
assert match["path"] == "getting-started/installation"
|
||||
# The page's title, so a result reads as more than the heading it matched.
|
||||
assert match["title"] == "Installation"
|
||||
assert match["section"] == "Requirements"
|
||||
assert match["breadcrumbs"] == ["Get Started", "Installation"]
|
||||
assert match["excerpt"] == "Prowler runs on Python 3.9 or later."
|
||||
assert match["score"] == 4.9
|
||||
# Anchored: a match is a section, and the page it is on can be a long one.
|
||||
assert match["url"] == (
|
||||
"https://docs.prowler.com/getting-started/installation#requirements"
|
||||
)
|
||||
|
||||
|
||||
async def test_the_search_query_is_sent_as_the_api_expects_it(
|
||||
mcp_root_server, docs_router
|
||||
):
|
||||
"""The endpoint takes a POST body, not the payload the old one took."""
|
||||
stub_search_hit(docs_router)
|
||||
|
||||
async with Client(mcp_root_server) as client:
|
||||
await client.call_tool("prowler_docs_search", {"term": "install"})
|
||||
|
||||
request = docs_router.request_for("POST", SEARCH)
|
||||
assert json.loads(request.content) == {"query": "install", "filters": {}}
|
||||
|
||||
|
||||
async def test_a_section_with_no_anchor_links_to_the_page(mcp_root_server, docs_router):
|
||||
"""The API sends "" for a page's first section and null for pages without anchors."""
|
||||
stub_search_hit(
|
||||
docs_router,
|
||||
search_match(anchor=""),
|
||||
search_match(path="getting-started/requirements", anchor=None),
|
||||
)
|
||||
|
||||
async with Client(mcp_root_server) as client:
|
||||
result = await client.call_tool("prowler_docs_search", {"term": "install"})
|
||||
|
||||
assert result.data[0]["url"] == (
|
||||
"https://docs.prowler.com/getting-started/installation"
|
||||
)
|
||||
assert result.data[1]["url"] == (
|
||||
"https://docs.prowler.com/getting-started/requirements"
|
||||
)
|
||||
|
||||
|
||||
async def test_page_size_caps_a_response_the_api_did_not_size(
|
||||
mcp_root_server, docs_router
|
||||
):
|
||||
"""The endpoint takes no size argument, so the cap has to be applied here."""
|
||||
stub_search_hit(docs_router, *(search_match() for _ in range(6)))
|
||||
|
||||
async with Client(mcp_root_server) as client:
|
||||
result = await client.call_tool(
|
||||
"prowler_docs_search", {"term": "install", "page_size": 2}
|
||||
)
|
||||
|
||||
assert len(result.data) == 2
|
||||
@@ -31,6 +31,8 @@
|
||||
"ec2:GetInstanceMetadataDefaults",
|
||||
"ecr:Describe*",
|
||||
"ecr:GetRegistryScanningConfiguration",
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"elasticfilesystem:DescribeBackupPolicy",
|
||||
"glue:GetConnections",
|
||||
"glue:GetSecurityConfiguration*",
|
||||
@@ -42,6 +44,7 @@
|
||||
"lightsail:GetRelationalDatabases",
|
||||
"macie2:GetMacieSession",
|
||||
"macie2:GetAutomatedDiscoveryConfiguration",
|
||||
"rolesanywhere:ListProfiles",
|
||||
"rolesanywhere:ListTagsForResource",
|
||||
"rolesanywhere:ListTrustAnchors",
|
||||
"s3:GetAccountPublicAccessBlock",
|
||||
|
||||
@@ -203,6 +203,8 @@ Resources:
|
||||
- "ec2:GetInstanceMetadataDefaults"
|
||||
- "ecr:Describe*"
|
||||
- "ecr:GetRegistryScanningConfiguration"
|
||||
- "ecr:BatchGetImage"
|
||||
- "ecr:GetDownloadUrlForLayer"
|
||||
- "elasticfilesystem:DescribeBackupPolicy"
|
||||
- "glue:GetConnections"
|
||||
- "glue:GetSecurityConfiguration*"
|
||||
@@ -213,6 +215,7 @@ Resources:
|
||||
- "lightsail:GetRelationalDatabases"
|
||||
- "macie2:GetMacieSession"
|
||||
- "macie2:GetAutomatedDiscoveryConfiguration"
|
||||
- "rolesanywhere:ListProfiles"
|
||||
- "rolesanywhere:ListTagsForResource"
|
||||
- "rolesanywhere:ListTrustAnchors"
|
||||
- "s3:GetAccountPublicAccessBlock"
|
||||
@@ -469,6 +472,8 @@ Resources:
|
||||
- "ec2:GetInstanceMetadataDefaults"
|
||||
- "ecr:Describe*"
|
||||
- "ecr:GetRegistryScanningConfiguration"
|
||||
- "ecr:BatchGetImage"
|
||||
- "ecr:GetDownloadUrlForLayer"
|
||||
- "elasticfilesystem:DescribeBackupPolicy"
|
||||
- "glue:GetConnections"
|
||||
- "glue:GetSecurityConfiguration*"
|
||||
|
||||
@@ -4,6 +4,59 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [5.40.0] (Prowler v5.40.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- NCSC Cyber Essentials 3.3 compliance framework with Azure provider coverage across the five Cyber Essentials themes [(#11588)](https://github.com/prowler-cloud/prowler/pull/11588)
|
||||
- `oss_bucket_versioning_enabled` check for Alibaba Cloud provider, verifying that OSS buckets have versioning enabled to allow recovery from accidental or malicious object overwrite and deletion [(#11913)](https://github.com/prowler-cloud/prowler/pull/11913)
|
||||
- `defender_domain_dmarc_records_published` checks that every Exchange Online domain publishes a DMARC record with an enforcing policy (`p=quarantine` or `p=reject`) [(#11936)](https://github.com/prowler-cloud/prowler/pull/11936)
|
||||
- `ske_cluster_no_public_endpoint` check for STACKIT provider, flagging SKE clusters whose Kubernetes API endpoint is reachable from the whole internet because the ACL extension is disabled or its allowed CIDR list contains `0.0.0.0/0` or `::/0` [(#11943)](https://github.com/prowler-cloud/prowler/pull/11943)
|
||||
- `oss_bucket_server_side_encryption_enabled` check for Alibaba Cloud provider, verifying that OSS buckets have a default server-side encryption rule (AES256 or KMS) [(#11981)](https://github.com/prowler-cloud/prowler/pull/11981)
|
||||
- `organization_default_workflow_permissions_read_only` check for GitHub provider, verifying that organizations grant GitHub Actions workflows a read-only default `GITHUB_TOKEN` [(#12122)](https://github.com/prowler-cloud/prowler/pull/12122)
|
||||
- `ecr_repository_image_no_secrets` check for AWS provider, scanning the latest ECR repository image's configuration and filesystem layers for hardcoded secrets [(#12123)](https://github.com/prowler-cloud/prowler/pull/12123)
|
||||
- `repository_default_workflow_permissions_read_only` check for GitHub provider, verifying that repositories grant GitHub Actions workflows a read-only default `GITHUB_TOKEN` [(#12143)](https://github.com/prowler-cloud/prowler/pull/12143)
|
||||
- `vpc_security_group_open_egress` check for Huawei Cloud provider: VPC security groups do not allow open egress to the internet [(#12209)](https://github.com/prowler-cloud/prowler/pull/12209)
|
||||
- `organization_actions_pull_request_approval_disabled` check for GitHub provider, verifying that organizations prevent GitHub Actions from creating and approving pull requests [(#12394)](https://github.com/prowler-cloud/prowler/pull/12394)
|
||||
- Add the `iam_workload_identity_pool_provider_attribute_condition` check to flag GCP Workload Identity Federation providers that trust a multi-tenant issuer without an attribute condition restricting which external identities can impersonate federated principals [(#12416)](https://github.com/prowler-cloud/prowler/pull/12416)
|
||||
- Add the `rolesanywhere_profile_restricts_session_permissions` check to flag AWS IAM Roles Anywhere profiles that reference an administrative role without scoping down the vended session with a session policy or managed policies [(#12416)](https://github.com/prowler-cloud/prowler/pull/12416)
|
||||
- `bedrock_guardrail_contextual_grounding_filter_enabled`, `bedrock_custom_model_encrypted_with_cmk`, `bedrock_knowledge_base_encrypted_with_cmk` and `bedrock_agent_role_not_shared_across_agents` are four new AWS Bedrock checks covering guardrail contextual grounding, custom model encryption, knowledge-base data-source encryption, and non-shared agent execution roles. [(#12459)](https://github.com/prowler-cloud/prowler/pull/12459)
|
||||
- `Cluster` column in Kubernetes CIS, ISO27001, Prowler ThreatScore, and universal compliance outputs, populated with the resolved cluster name so multi-cluster scans can be told apart in the output [(#12506)](https://github.com/prowler-cloud/prowler/pull/12506)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Kubernetes `kubelet` checks no longer disappear from the scan with `TypeError: 'NoneType' object is not iterable` when a `kubelet-config` ConfigMap is broken: one with malformed YAML is logged and skipped while the valid ones are still evaluated, one without kubelet data is evaluated with an empty configuration instead of crashing the checks, and the `apiserver`, `controllermanager`, `etcd` and `scheduler` pod gatherers now always return a list [(#12225)](https://github.com/prowler-cloud/prowler/pull/12225)
|
||||
- IaC provider now raises typed `IacBaseException` errors (repository clone, Trivy missing, scan and output processing failures) instead of calling `sys.exit(1)`; the CLI still stops with the logged message, and API scans fail as regular task errors instead of a `SystemExit` escaping the worker [(#12227)](https://github.com/prowler-cloud/prowler/pull/12227)
|
||||
- CLI Slack integration (`--slack`) no longer fails when a scan produces no findings: the pass and fail percentages are guarded against a `findings_count` of 0, which previously raised `ZeroDivisionError` and sent `blocks=None` to Slack instead of the summary [(#12229)](https://github.com/prowler-cloud/prowler/pull/12229)
|
||||
- AWS FSBP compliance mapping for `IAM.9` and `EKS.1` referenced missing/renamed checks; both now point to their real, existing check IDs [(#12372)](https://github.com/prowler-cloud/prowler/pull/12372)
|
||||
- `ec2_securitygroup_not_used` no longer reports a false positive for security groups attached only to an AWS Batch compute environment, which holds them in configuration without creating a network interface while scaled down to zero instances [(#12458)](https://github.com/prowler-cloud/prowler/pull/12458)
|
||||
- Bedrock Agent ARNs are now built from the audited partition instead of a hardcoded `arn:aws:`, so findings in GovCloud and China carry a resolvable ARN and `--resource-arn` scoping matches agents in those partitions. [(#12459)](https://github.com/prowler-cloud/prowler/pull/12459)
|
||||
- `push-to-cloud` now validates Private Cloud TLS certificates with the operating system trust store without changing provider HTTP clients [(#12485)](https://github.com/prowler-cloud/prowler/pull/12485)
|
||||
- `prowler.compliance.universal` entry point directories are resolved through a single shared helper and deduplicated by resolved path, so a directory reached through two entry points is parsed once and a package that fails to import no longer hides the rest [(#12536)](https://github.com/prowler-cloud/prowler/pull/12536)
|
||||
- OSS bucket logging, versioning, default encryption and ACL configurations are now read correctly from the Alibaba Cloud SDK, so `oss_bucket_logging_enabled`, `oss_bucket_versioning_enabled`, `oss_bucket_server_side_encryption_enabled` and `oss_bucket_not_publicly_accessible` no longer report every bucket as unconfigured [(#12546)](https://github.com/prowler-cloud/prowler/pull/12546)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- `openssl`, `libssl3t64` and `openssl-provider-legacy` upgraded to 3.5.7-1~deb13u2 in the SDK container image, patching ten high OpenSSL CVEs [(#12549)](https://github.com/prowler-cloud/prowler/pull/12549)
|
||||
|
||||
---
|
||||
|
||||
## [5.39.1] (Prowler v5.39.1)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Bump alibabacloud-tea-openapi to 0.4.6, oci to 2.184.1 and pyopenssl to 26.4.0 so the published wheel installs with cryptography 50.0.0; 5.38.0 declared cryptography 50.0.0 while those packages capped it below 50, so pip could not install it and `pip install prowler` silently fell back to 5.37.1 [(#12477)](https://github.com/prowler-cloud/prowler/pull/12477)
|
||||
- Pin zstd to 1.5.7.2; 1.5.7.3 was yanked from PyPI as not thread safe [(#12477)](https://github.com/prowler-cloud/prowler/pull/12477)
|
||||
- ECS task-definition checks no longer report PASS when `DescribeTaskDefinition` fails before container evidence is gathered [(#12478)](https://github.com/prowler-cloud/prowler/pull/12478)
|
||||
- `ses_identity_not_publicly_accessible` now evaluates every SES identity authorization policy and marks mixed public Allow and Deny statements for manual review [(#12480)](https://github.com/prowler-cloud/prowler/pull/12480)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- Trivy from v0.72.0 to v0.73.0 in the container image, fixing HIGH CVE-2026-46600 in the bundled `golang.org/x/net` [(#12445)](https://github.com/prowler-cloud/prowler/pull/12445)
|
||||
- Trivy v0.74.0 and Debian util-linux 2.41.5-0+deb13u1 in the SDK container image, patching Go standard library vulnerabilities and CVE-2026-53615 [(#12470)](https://github.com/prowler-cloud/prowler/pull/12470)
|
||||
|
||||
---
|
||||
|
||||
## [5.39.0] (Prowler v5.39.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -126,7 +126,10 @@ from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_m365
|
||||
from prowler.lib.outputs.csv.csv import CSV
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
from prowler.lib.outputs.html.html import HTML
|
||||
from prowler.lib.outputs.ocsf.ingestion import send_ocsf_to_api
|
||||
from prowler.lib.outputs.ocsf.ingestion import (
|
||||
SystemTrustStoreError,
|
||||
send_ocsf_to_api,
|
||||
)
|
||||
from prowler.lib.outputs.ocsf.ocsf import OCSF
|
||||
from prowler.lib.outputs.outputs import extract_findings_statistics, report
|
||||
from prowler.lib.outputs.sarif.sarif import SARIF
|
||||
@@ -145,6 +148,7 @@ from prowler.providers.gcp.models import GCPOutputOptions
|
||||
from prowler.providers.github.models import GithubOutputOptions
|
||||
from prowler.providers.googleworkspace.models import GoogleWorkspaceOutputOptions
|
||||
from prowler.providers.huaweicloud.models import HuaweiCloudOutputOptions
|
||||
from prowler.providers.iac.exceptions.exceptions import IacBaseException
|
||||
from prowler.providers.iac.models import IACOutputOptions
|
||||
from prowler.providers.image.exceptions.exceptions import ImageBaseException
|
||||
from prowler.providers.image.models import ImageOutputOptions
|
||||
@@ -162,6 +166,75 @@ from prowler.providers.stackit.models import StackITOutputOptions
|
||||
from prowler.providers.vercel.models import VercelOutputOptions
|
||||
|
||||
|
||||
def _send_ocsf_to_cloud(file_path: str) -> dict | None:
|
||||
"""Upload OCSF findings and report safe, actionable CLI failures."""
|
||||
try:
|
||||
return send_ocsf_to_api(file_path)
|
||||
except requests.exceptions.JSONDecodeError:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: "
|
||||
"the API returned an invalid JSON response. "
|
||||
f"Scan results were saved to {file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
except ValueError:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.YELLOW}\nPush to Prowler Cloud skipped: no API key configured. "
|
||||
"Set the PROWLER_CLOUD_API_KEY environment variable to enable it. "
|
||||
f"Scan results were saved to {file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
except SystemTrustStoreError:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: the operating system trust store "
|
||||
"could not be initialized. Check the configured CA bundle paths, verify the host certificate "
|
||||
"hostname, validity period, and certificate chain, and install the organization or TLS-intercepting "
|
||||
"proxy CA in the operating system trust store. In containers, configure and update the container "
|
||||
"system CA store. "
|
||||
f"Scan results were saved to {file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
except requests.exceptions.SSLError:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: TLS certificate validation failed. "
|
||||
"Verify the hostname, validity period, and certificate chain, and install the organization or "
|
||||
"TLS-intercepting proxy CA in the operating system trust store. In containers, configure and update "
|
||||
"the container system CA store. "
|
||||
f"Scan results were saved to {file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
except requests.ConnectionError:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: could not reach the Prowler Cloud API at "
|
||||
f"{cloud_api_base_url}. Check the URL and your network connection. "
|
||||
f"Scan results were saved to {file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
except requests.HTTPError as http_err:
|
||||
status_code = (
|
||||
http_err.response.status_code if http_err.response is not None else None
|
||||
)
|
||||
if status_code == 402:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: "
|
||||
"this feature is only available with a Prowler Cloud subscription. "
|
||||
f"Scan results were saved to {file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
elif status_code is None:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: "
|
||||
"the API request failed without a response status. "
|
||||
f"Scan results were saved to {file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: the API returned HTTP "
|
||||
f"{status_code}. Verify your API key is valid and has the right permissions. "
|
||||
f"Scan results were saved to {file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
except Exception:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed unexpectedly. "
|
||||
f"Scan results were saved to {file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def prowler():
|
||||
# Parse Arguments
|
||||
# Refactor(CLI)
|
||||
@@ -493,12 +566,16 @@ def prowler():
|
||||
except ImageBaseException as error:
|
||||
logger.critical(f"{error}")
|
||||
sys.exit(1)
|
||||
elif provider == "iac":
|
||||
try:
|
||||
findings = global_provider.run()
|
||||
except IacBaseException as error:
|
||||
logger.critical(f"{error}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# IAC and external tool-wrapper providers registered via entry
|
||||
# points. Unexpected failures propagate to the outer except
|
||||
# Exception backstop further down in this file — keeping the
|
||||
# branch free of an Image-specific catch that would otherwise
|
||||
# mislead plug-in authors reading this code.
|
||||
# External tool-wrapper providers registered via entry points.
|
||||
# Unexpected failures propagate to the outer except Exception
|
||||
# backstop further down in this file.
|
||||
findings = global_provider.run()
|
||||
# Note: External tool providers don't support granular progress tracking since
|
||||
# they run external tools as a black box and return all findings at once.
|
||||
@@ -641,39 +718,8 @@ def prowler():
|
||||
print(
|
||||
f"{Style.BRIGHT}\nPushing findings to Prowler Cloud, please wait...{Style.RESET_ALL}"
|
||||
)
|
||||
try:
|
||||
response = send_ocsf_to_api(ocsf_output.file_path)
|
||||
except ValueError:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.YELLOW}\nPush to Prowler Cloud skipped: no API key configured. "
|
||||
"Set the PROWLER_CLOUD_API_KEY environment variable to enable it. "
|
||||
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
except requests.ConnectionError:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: could not reach the Prowler Cloud API at "
|
||||
f"{cloud_api_base_url}. Check the URL and your network connection. "
|
||||
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
except requests.HTTPError as http_err:
|
||||
if http_err.response.status_code == 402:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: "
|
||||
"this feature is only available with a Prowler Cloud subscription. "
|
||||
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: the API returned HTTP {http_err.response.status_code}. "
|
||||
"Verify your API key is valid and has the right permissions. "
|
||||
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
except Exception as error:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed unexpectedly: {error}. "
|
||||
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
response = _send_ocsf_to_cloud(ocsf_output.file_path)
|
||||
if response is not None:
|
||||
job_id = response.get("data", {}).get("id") if response else None
|
||||
if job_id:
|
||||
print(
|
||||
|
||||
@@ -110,7 +110,9 @@
|
||||
"eks_cluster_kms_cmk_encryption_in_secrets_enabled",
|
||||
"dynamodb_tables_kms_cmk_encryption_enabled",
|
||||
"sns_topics_kms_encryption_at_rest_enabled",
|
||||
"sqs_queues_server_side_encryption_enabled"
|
||||
"sqs_queues_server_side_encryption_enabled",
|
||||
"bedrock_custom_model_encrypted_with_cmk",
|
||||
"bedrock_knowledge_base_encrypted_with_cmk"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -638,10 +640,12 @@
|
||||
"Section": "AI Application Security",
|
||||
"SubSection": "Output Validation",
|
||||
"Service": "bedrock",
|
||||
"Type": "Manual"
|
||||
"Type": "Automated"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
"Checks": [
|
||||
"bedrock_guardrail_contextual_grounding_filter_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "AISF-AI-10",
|
||||
@@ -652,10 +656,12 @@
|
||||
"Section": "AI Application Security",
|
||||
"SubSection": "RAG Security",
|
||||
"Service": "bedrock",
|
||||
"Type": "Manual"
|
||||
"Type": "Automated"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
"Checks": [
|
||||
"bedrock_knowledge_base_encrypted_with_cmk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "AISF-AI-11",
|
||||
@@ -680,10 +686,12 @@
|
||||
"Section": "AI Application Security",
|
||||
"SubSection": "Agent Governance",
|
||||
"Service": "bedrock",
|
||||
"Type": "Manual"
|
||||
"Type": "Automated"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
"Checks": [
|
||||
"bedrock_agent_role_not_shared_across_agents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "AISF-AGENT-02",
|
||||
|
||||
@@ -1782,7 +1782,7 @@
|
||||
"Name": "EKS cluster endpoints should not be publicly accessible",
|
||||
"Description": "This control checks whether an Amazon EKS cluster endpoint is publicly accessible. The control fails if an EKS cluster has an endpoint that is publicly accessible.",
|
||||
"Checks": [
|
||||
"eks_endpoints_not_publicly_accessible"
|
||||
"eks_cluster_not_publicly_accessible"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -2634,7 +2634,9 @@
|
||||
"Id": "IAM.9",
|
||||
"Name": "MFA should be enabled for the root user",
|
||||
"Description": "The root user has complete access to all the services and resources in an AWS account. MFA adds an extra layer of protection on top of a user name and password. With MFA enabled, when a user signs in to the AWS Management Console, they're prompted for their user name and password and for an authentication code from their AWS MFA device.",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"iam_root_mfa_enabled"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"ItemId": "IAM.9",
|
||||
|
||||
@@ -0,0 +1,601 @@
|
||||
{
|
||||
"framework": "Cyber-Essentials",
|
||||
"name": "NCSC Cyber Essentials: Requirements for IT Infrastructure",
|
||||
"version": "3.3",
|
||||
"description": "The UK National Cyber Security Centre (NCSC) Cyber Essentials scheme - Requirements for IT Infrastructure v3.3 (April 2026). Cyber Essentials organises its technical requirements into five control themes: Firewalls, Secure Configuration, Security Update Management, User Access Control, and Malware Protection. Cloud services are explicitly in scope for Cyber Essentials and cannot be excluded, so under the shared responsibility model this framework covers the controls the applicant organisation is responsible for implementing on its own cloud infrastructure (Infrastructure/Platform as a Service). Requirements that apply to end-user devices, on-premises network appliances, or organisational process and are not observable from cloud control-plane evidence are included for completeness with an empty check list and 'non-applicable' or 'Manual' attributes.",
|
||||
"icon": "cyber-essentials",
|
||||
"attributes_metadata": [
|
||||
{
|
||||
"key": "Theme",
|
||||
"label": "Technical Control Theme",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"enum": [
|
||||
"Firewalls",
|
||||
"Secure Configuration",
|
||||
"Security Update Management",
|
||||
"User Access Control",
|
||||
"Malware Protection"
|
||||
],
|
||||
"output_formats": {
|
||||
"csv": true,
|
||||
"ocsf": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "AssessmentStatus",
|
||||
"label": "Assessment Status",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"enum": [
|
||||
"Automated",
|
||||
"Manual"
|
||||
],
|
||||
"output_formats": {
|
||||
"csv": true,
|
||||
"ocsf": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "CloudApplicability",
|
||||
"label": "Cloud Applicability",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"enum": [
|
||||
"full",
|
||||
"partial",
|
||||
"non-applicable"
|
||||
],
|
||||
"output_formats": {
|
||||
"csv": true,
|
||||
"ocsf": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "RemediationProcedure",
|
||||
"label": "Remediation Procedure",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"output_formats": {
|
||||
"csv": true,
|
||||
"ocsf": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "References",
|
||||
"label": "References",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"output_formats": {
|
||||
"csv": true,
|
||||
"ocsf": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": {
|
||||
"table_config": {
|
||||
"group_by": "Theme"
|
||||
},
|
||||
"pdf_config": {
|
||||
"language": "en",
|
||||
"primary_color": "#003D54",
|
||||
"secondary_color": "#0072CE",
|
||||
"bg_color": "#F0F4FA",
|
||||
"group_by_field": "Theme",
|
||||
"sections": [
|
||||
"Firewalls",
|
||||
"Secure Configuration",
|
||||
"Security Update Management",
|
||||
"User Access Control",
|
||||
"Malware Protection"
|
||||
],
|
||||
"section_short_names": {
|
||||
"Firewalls": "Firewalls",
|
||||
"Secure Configuration": "Secure Config",
|
||||
"Security Update Management": "Update Mgmt",
|
||||
"User Access Control": "Access Control",
|
||||
"Malware Protection": "Malware"
|
||||
},
|
||||
"charts": [
|
||||
{
|
||||
"id": "theme_compliance",
|
||||
"type": "horizontal_bar",
|
||||
"group_by": "Theme",
|
||||
"title": "Compliance Score by Cyber Essentials Theme",
|
||||
"y_label": "Theme",
|
||||
"x_label": "Compliance %",
|
||||
"value_source": "compliance_percent",
|
||||
"color_mode": "by_value"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"only_failed": true,
|
||||
"include_manual": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"requirements": [
|
||||
{
|
||||
"id": "CE-FW-01",
|
||||
"name": "Boundary firewall on every in-scope device",
|
||||
"description": "Every device in scope must be protected by a correctly configured firewall or network device with firewall functionality, restricting inbound and outbound network services to those that are secure and necessary.",
|
||||
"attributes": {
|
||||
"Theme": "Firewalls",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Restrict inbound access from the internet to management and data services (RDP, SSH, database ports) using the provider's network firewall, security group or access-control-list controls, and remove any rule that allows unrestricted inbound access.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"network_rdp_internet_access_restricted",
|
||||
"network_ssh_internet_access_restricted",
|
||||
"network_udp_internet_access_restricted",
|
||||
"network_http_internet_access_restricted"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-FW-02",
|
||||
"name": "Change default administrative passwords or disable remote admin access",
|
||||
"description": "Default administrative passwords on firewalls and network devices must be changed to a strong, unique password, or remote administrative access must be disabled entirely.",
|
||||
"attributes": {
|
||||
"Theme": "Firewalls",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "non-applicable",
|
||||
"RemediationProcedure": "This requirement applies to on-premises network appliances and home/remote routers, which are outside the scope of cloud control-plane evidence. Manage and document this control as part of your organisation's device estate.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-FW-03",
|
||||
"name": "Restrict access to firewall/network device administrative interfaces",
|
||||
"description": "Access to the administrative interface used to manage firewall or network device configuration must not be possible from the internet, unless there is a documented business need and the interface is protected by MFA or an IP allow list combined with a managed password approach.",
|
||||
"attributes": {
|
||||
"Theme": "Firewalls",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Disable public network access on management-plane resources (object storage, secret and key management services) or restrict access to trusted networks and IP ranges, and require MFA for any administrative access exposed to the internet.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"storage_account_public_network_access_disabled",
|
||||
"storage_default_network_access_rule_is_denied",
|
||||
"keyvault_access_only_through_private_endpoints",
|
||||
"network_rdp_internet_access_restricted",
|
||||
"network_ssh_internet_access_restricted"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-FW-04",
|
||||
"name": "Block unauthenticated inbound connections by default",
|
||||
"description": "Firewalls and network devices must block unauthenticated inbound connections by default.",
|
||||
"attributes": {
|
||||
"Theme": "Firewalls",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "full",
|
||||
"RemediationProcedure": "Configure network access-control rules with a default-deny inbound posture and only allow specific, documented inbound services. Disable public network access on managed services that do not require it.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"network_rdp_internet_access_restricted",
|
||||
"network_ssh_internet_access_restricted",
|
||||
"network_udp_internet_access_restricted",
|
||||
"network_http_internet_access_restricted",
|
||||
"storage_account_public_network_access_disabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-FW-05",
|
||||
"name": "Inbound firewall rules approved and documented",
|
||||
"description": "Inbound firewall rules must be approved and documented by an authorised person, including the business need for the rule.",
|
||||
"attributes": {
|
||||
"Theme": "Firewalls",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "non-applicable",
|
||||
"RemediationProcedure": "Maintain a change-approval record (e.g. change tickets or a network rule register) for every inbound network access-control rule, including the business justification and approver.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-FW-06",
|
||||
"name": "Remove or disable unnecessary firewall rules",
|
||||
"description": "Firewall rules that are no longer needed must be removed or disabled.",
|
||||
"attributes": {
|
||||
"Theme": "Firewalls",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "non-applicable",
|
||||
"RemediationProcedure": "Periodically review firewall and network access-control rules, removing or disabling any rule that no longer has a documented business need.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-FW-07",
|
||||
"name": "Software firewall on devices used on untrusted networks",
|
||||
"description": "Devices that connect to untrusted networks, such as public Wi-Fi hotspots, must use a software firewall.",
|
||||
"attributes": {
|
||||
"Theme": "Firewalls",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "non-applicable",
|
||||
"RemediationProcedure": "This is an end-user device control (host-based firewall) and has no cloud control-plane equivalent. Enforce via endpoint management policy.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-SC-01",
|
||||
"name": "Remove or disable unnecessary user accounts",
|
||||
"description": "Unnecessary user accounts, such as guest accounts and unused administrative accounts, must be removed or disabled.",
|
||||
"attributes": {
|
||||
"Theme": "Secure Configuration",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Restrict guest and external user invitations and access, and review identity-provider and cloud role assignments to remove unused guest or administrative accounts.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"entra_policy_guest_users_access_restrictions",
|
||||
"entra_policy_guest_invite_only_for_admin_roles"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-SC-02",
|
||||
"name": "Change default or guessable account passwords",
|
||||
"description": "Default or guessable account passwords must be changed before a device or service is used.",
|
||||
"attributes": {
|
||||
"Theme": "Secure Configuration",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Enable identity-provider security defaults (or an equivalent sign-in protection baseline) so that default or weak credentials cannot be used for sign-in.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"entra_security_defaults_enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-SC-03",
|
||||
"name": "Remove or disable unnecessary software",
|
||||
"description": "Unnecessary software, including applications, system utilities and network services, must be removed or disabled.",
|
||||
"attributes": {
|
||||
"Theme": "Secure Configuration",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "non-applicable",
|
||||
"RemediationProcedure": "This is an operating-system level control for devices and servers and has no direct cloud control-plane equivalent. Maintain an approved software baseline and image hardening process for VM images.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-SC-04",
|
||||
"name": "Disable auto-run features",
|
||||
"description": "Auto-run features that allow file execution without user authorisation must be disabled.",
|
||||
"attributes": {
|
||||
"Theme": "Secure Configuration",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "non-applicable",
|
||||
"RemediationProcedure": "This is an operating-system level control and has no cloud control-plane equivalent. Disable AutoRun/AutoPlay via OS configuration or group policy on VM images.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-SC-05",
|
||||
"name": "Authenticate users before granting access to organisational data or services",
|
||||
"description": "Users must be authenticated before being allowed access to organisational data or services, including cloud services.",
|
||||
"attributes": {
|
||||
"Theme": "Secure Configuration",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "full",
|
||||
"RemediationProcedure": "Disable anonymous or public access to object storage and require authenticated, encrypted (TLS 1.2+) access. Use role-based access control for secret and key management data-plane access instead of policies that allow unauthenticated retrieval.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"storage_secure_transfer_required_is_enabled",
|
||||
"storage_blob_public_access_level_is_disabled",
|
||||
"storage_account_public_network_access_disabled",
|
||||
"storage_ensure_minimum_tls_version_12",
|
||||
"keyvault_rbac_enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-SC-06",
|
||||
"name": "Device locking and brute-force protection for unlocking credentials",
|
||||
"description": "Devices that require a user's physical presence must use an unlocking credential (biometric, password or PIN) of at least 6 characters, protected against brute-force guessing by throttling or lockout after no more than 10 attempts.",
|
||||
"attributes": {
|
||||
"Theme": "Secure Configuration",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "non-applicable",
|
||||
"RemediationProcedure": "This is an end-user device control (screen lock credential length and brute-force lockout) and has no cloud control-plane equivalent. Enforce a minimum unlock credential length and a lockout threshold of no more than 10 attempts through your device management policy.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-SUM-01",
|
||||
"name": "All software licensed and supported",
|
||||
"description": "All software on in-scope devices must be licensed and supported by the vendor.",
|
||||
"attributes": {
|
||||
"Theme": "Security Update Management",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "non-applicable",
|
||||
"RemediationProcedure": "Maintain a software asset inventory confirming each product is licensed and has an active vendor support commitment, including operating systems and any third-party software running on cloud compute resources.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-SUM-02",
|
||||
"name": "Remove unsupported software",
|
||||
"description": "Software must be removed from devices when it becomes unsupported, or isolated into a defined sub-set that prevents all internet traffic.",
|
||||
"attributes": {
|
||||
"Theme": "Security Update Management",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "non-applicable",
|
||||
"RemediationProcedure": "Track vendor end-of-support dates for operating systems and applications running on cloud compute resources, and decommission or isolate workloads before support ends.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-SUM-03",
|
||||
"name": "Automatic updates enabled where possible",
|
||||
"description": "Automatic updates must be enabled on in-scope software where this is possible.",
|
||||
"attributes": {
|
||||
"Theme": "Security Update Management",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Enable automatic updates on in-scope software where possible: configure platform-managed guest patching on cloud virtual machines and instance groups, and enable vendor auto-update mechanisms for any third-party software running on those workloads. Note: no check currently observes whether automatic updates are enabled (vulnerability assessment and security-posture provisioning only prove monitoring and assessment coverage), so this requirement must be verified manually until a dedicated check validating update automation exists.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-SUM-04",
|
||||
"name": "Critical and high-risk updates applied within 14 days",
|
||||
"description": "Updates that fix vulnerabilities described by the vendor as critical or high risk, or that address a CVSS v3 base score of 7 or above (or where severity is unspecified), must be applied within 14 days of release.",
|
||||
"attributes": {
|
||||
"Theme": "Security Update Management",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Enable the provider's security-posture service for system update recommendations and vulnerability assessment, and remediate flagged virtual machines within 14 days of a critical or high-risk update being released. Note: these checks evidence that update monitoring and vulnerability assessment coverage is enabled, not that a given update was applied within the 14-day window, which must be verified from your patch management records.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"defender_ensure_system_updates_are_applied",
|
||||
"defender_auto_provisioning_vulnerabilty_assessments_machines_on"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-UAC-01",
|
||||
"name": "Process to create and approve user accounts",
|
||||
"description": "A documented process must be in place to create and approve user accounts before access is granted.",
|
||||
"attributes": {
|
||||
"Theme": "User Access Control",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "non-applicable",
|
||||
"RemediationProcedure": "Maintain a joiner/mover/leaver process with documented approval steps for creating identity-provider accounts and assigning cloud roles.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-UAC-02",
|
||||
"name": "Authenticate users with unique credentials",
|
||||
"description": "Users must be authenticated with unique credentials before being granted access to applications or devices.",
|
||||
"attributes": {
|
||||
"Theme": "User Access Control",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Enable identity-provider security defaults and require every user to authenticate with their own directory-backed identity rather than shared account keys or long-lived access keys when accessing cloud resources such as object storage.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"entra_security_defaults_enabled",
|
||||
"storage_default_to_entra_authorization_enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-UAC-03",
|
||||
"name": "Remove or disable user accounts when no longer required",
|
||||
"description": "User accounts must be removed or disabled when they are no longer required, for example when a user leaves the organisation or after a defined period of inactivity.",
|
||||
"attributes": {
|
||||
"Theme": "User Access Control",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "non-applicable",
|
||||
"RemediationProcedure": "Implement a leaver process and periodic identity-provider access reviews to disable or remove accounts that are no longer required.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-UAC-04",
|
||||
"name": "Multi-factor authentication for cloud services",
|
||||
"description": "Multi-factor authentication must be implemented where available, and authentication to cloud services must always use MFA.",
|
||||
"attributes": {
|
||||
"Theme": "User Access Control",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "full",
|
||||
"RemediationProcedure": "Require multi-factor authentication for all users through an enforced sign-in policy, covering administrative consoles, management APIs, and users with access to virtual machines.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"entra_privileged_user_has_mfa",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
"entra_conditional_access_policy_require_mfa_for_admin_portals",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api",
|
||||
"entra_user_with_vm_access_has_mfa"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-UAC-05",
|
||||
"name": "Separate accounts for administrative activities",
|
||||
"description": "Separate accounts must be used to perform administrative activities only, with no email, web browsing or other standard user activity that could expose administrative privileges to avoidable risk.",
|
||||
"attributes": {
|
||||
"Theme": "User Access Control",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Limit the number of highly privileged role assignments (global or organisation administrator), avoid granting owner or access-administrator roles at the account, subscription or project scope, and require named administrators to use dedicated privileged accounts for administrative tasks.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"entra_global_admin_in_less_than_five_users",
|
||||
"iam_role_user_access_admin_restricted",
|
||||
"iam_subscription_roles_owner_custom_not_created"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-UAC-06",
|
||||
"name": "Remove or disable special access privileges when no longer required",
|
||||
"description": "Special access privileges must be removed or disabled when they are no longer required, for example when a member of staff changes role.",
|
||||
"attributes": {
|
||||
"Theme": "User Access Control",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Periodically review identity-provider directory role assignments and cloud role-based access control assignments, removing privileged roles that are no longer needed for a user's current role.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"entra_global_admin_in_less_than_five_users",
|
||||
"iam_role_user_access_admin_restricted"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-UAC-07",
|
||||
"name": "Protect password-based authentication against brute-force attacks",
|
||||
"description": "Where authentication is carried out using a password, accounts must be protected against brute-force guessing using MFA, attempt throttling, or account lockout.",
|
||||
"attributes": {
|
||||
"Theme": "User Access Control",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Enable identity-provider security defaults (which include account lockout on repeated failed sign-ins) and require MFA, particularly for privileged accounts, to mitigate brute-force password attacks.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"entra_security_defaults_enabled",
|
||||
"entra_privileged_user_has_mfa"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-UAC-08",
|
||||
"name": "Technical controls to manage password quality",
|
||||
"description": "Technical controls must be used to manage the quality of passwords, using MFA, a minimum password length of at least 12 characters with no maximum length, or a minimum of 8 characters combined with a common-password deny list. Regular forced password expiry and complexity requirements should not be enforced.",
|
||||
"attributes": {
|
||||
"Theme": "User Access Control",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Enable identity-provider security defaults and password protection (banned or breached password lists), and require MFA so that password length alone is not the only protection.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"entra_security_defaults_enabled",
|
||||
"entra_privileged_user_has_mfa",
|
||||
"entra_non_privileged_user_has_mfa"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-MP-01",
|
||||
"name": "Malware protection mechanism active on all in-scope devices",
|
||||
"description": "A malware protection mechanism must be active on all devices in scope, using anti-malware software, application allow listing, or application sandboxing.",
|
||||
"attributes": {
|
||||
"Theme": "Malware Protection",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Ensure endpoint protection is installed on all virtual machines and enable the provider's workload protection service for servers, including endpoint detection and response integration.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"defender_assessments_vm_endpoint_protection_installed",
|
||||
"defender_ensure_wdatp_is_enabled",
|
||||
"defender_ensure_defender_for_server_is_on"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-MP-02",
|
||||
"name": "Anti-malware software configuration",
|
||||
"description": "Anti-malware software must be kept up to date in line with vendor recommendations, prevent malware from running, prevent execution of malicious code, and prevent connections to malicious websites.",
|
||||
"attributes": {
|
||||
"Theme": "Malware Protection",
|
||||
"AssessmentStatus": "Automated",
|
||||
"CloudApplicability": "partial",
|
||||
"RemediationProcedure": "Enable the provider's workload protection services for endpoints, servers and object storage so that signatures stay current and malicious files, code execution and connections are blocked.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": [
|
||||
"defender_ensure_wdatp_is_enabled",
|
||||
"defender_ensure_defender_for_server_is_on",
|
||||
"defender_ensure_defender_for_storage_is_on"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "CE-MP-03",
|
||||
"name": "Application allow listing",
|
||||
"description": "Where used as an alternative to anti-malware software, only approved applications restricted by code signing must be allowed to execute, with a maintained list of approved applications and no execution of unsigned or invalidly signed applications.",
|
||||
"attributes": {
|
||||
"Theme": "Malware Protection",
|
||||
"AssessmentStatus": "Manual",
|
||||
"CloudApplicability": "non-applicable",
|
||||
"RemediationProcedure": "This is an end-user device control implemented through application control or allow-listing policies and has no cloud control-plane equivalent.",
|
||||
"References": "NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section E"
|
||||
},
|
||||
"checks": {
|
||||
"azure": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -526,7 +526,9 @@
|
||||
{
|
||||
"Id": "2.1.10",
|
||||
"Description": "DMARC, or Domain-based Message Authentication, Reporting, and Conformance, assists recipient mail systems in determining the appropriate action to take when messages from a domain fail to meet SPF or DKIM authentication criteria.",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"defender_domain_dmarc_records_published"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "2 Microsoft 365 Defender",
|
||||
|
||||
@@ -543,7 +543,9 @@
|
||||
{
|
||||
"Id": "2.1.10",
|
||||
"Description": "DMARC, or Domain-based Message Authentication, Reporting, and Conformance, assists recipient mail systems in determining the appropriate action to take when messages from a domain fail to meet SPF or DKIM authentication criteria.",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"defender_domain_dmarc_records_published"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "2 Microsoft 365 Defender",
|
||||
|
||||
@@ -554,7 +554,9 @@
|
||||
{
|
||||
"Id": "2.1.10",
|
||||
"Description": "DMARC, or Domain-based Message Authentication, Reporting, and Conformance, assists recipient mail systems in determining the appropriate action to take when messages from a domain fail to meet SPF or DKIM authentication criteria.",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"defender_domain_dmarc_records_published"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "2 Microsoft Defender",
|
||||
|
||||
@@ -10,7 +10,10 @@ import requests
|
||||
import yaml
|
||||
from packaging import version
|
||||
|
||||
from prowler.lib.check.compliance_models import load_compliance_framework_universal
|
||||
from prowler.lib.check.compliance_models import (
|
||||
get_universal_compliance_entry_point_dirs,
|
||||
load_compliance_framework_universal,
|
||||
)
|
||||
|
||||
# Re-exported from a leaf module so prowler.lib.check.utils can import the
|
||||
# constant without participating in the config <-> compliance_models <-> utils
|
||||
@@ -49,7 +52,7 @@ class _MutableTimestamp:
|
||||
|
||||
timestamp = _MutableTimestamp(datetime.today())
|
||||
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
|
||||
prowler_version = "5.39.0"
|
||||
prowler_version = "5.40.0"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
@@ -170,21 +173,7 @@ def get_available_compliance_frameworks(provider=None):
|
||||
available_compliance_frameworks.append(name)
|
||||
# External multi-provider frameworks via the dedicated universal group;
|
||||
# filtered by supports_provider when a provider is given.
|
||||
for ep in importlib.metadata.entry_points(group="prowler.compliance.universal"):
|
||||
try:
|
||||
module = ep.load()
|
||||
path = (
|
||||
module.__path__[0]
|
||||
if hasattr(module, "__path__")
|
||||
else os.path.dirname(module.__file__)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
continue
|
||||
if not os.path.isdir(path):
|
||||
continue
|
||||
for path in get_universal_compliance_entry_point_dirs():
|
||||
for file in os.scandir(path):
|
||||
if file.is_file() and file.name.endswith(".json"):
|
||||
name = file.name.removesuffix(".json")
|
||||
|
||||
@@ -1049,6 +1049,46 @@ def load_compliance_framework_universal(path: str) -> ComplianceFramework:
|
||||
return None
|
||||
|
||||
|
||||
# Kept apart from the per-provider `prowler.compliance` group so the legacy
|
||||
# loader never parses a universal JSON.
|
||||
UNIVERSAL_COMPLIANCE_ENTRY_POINT_GROUP = "prowler.compliance.universal"
|
||||
|
||||
|
||||
def get_universal_compliance_entry_point_dirs() -> list[str]:
|
||||
"""Existing directories contributed through the universal compliance entry
|
||||
point group, in entry point order.
|
||||
|
||||
Deduped by resolved path, so a directory reached through a symlink counts
|
||||
once. A package that fails to import is logged and skipped: one broken
|
||||
plugin must not hide the rest.
|
||||
"""
|
||||
dirs = []
|
||||
seen = set()
|
||||
for ep in importlib.metadata.entry_points(
|
||||
group=UNIVERSAL_COMPLIANCE_ENTRY_POINT_GROUP
|
||||
):
|
||||
try:
|
||||
module = ep.load()
|
||||
path = (
|
||||
module.__path__[0]
|
||||
if hasattr(module, "__path__")
|
||||
else os.path.dirname(module.__file__)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
continue
|
||||
if not os.path.isdir(path):
|
||||
continue
|
||||
resolved = os.path.realpath(path)
|
||||
if resolved in seen:
|
||||
continue
|
||||
seen.add(resolved)
|
||||
dirs.append(path)
|
||||
return dirs
|
||||
|
||||
|
||||
def _load_jsons_from_dir(dir_path: str, provider: str, bulk: dict) -> None:
|
||||
"""Scan *dir_path* for JSON files and add matching frameworks to *bulk*."""
|
||||
for filename in os.listdir(dir_path):
|
||||
@@ -1109,20 +1149,10 @@ def get_bulk_compliance_frameworks_universal(provider: str) -> dict:
|
||||
if compliance_root and os.path.isdir(compliance_root):
|
||||
_load_jsons_from_dir(compliance_root, provider, bulk)
|
||||
|
||||
# External multi-provider frameworks via the dedicated universal entry
|
||||
# point group, kept separate from the per-provider `prowler.compliance`
|
||||
# group so the legacy loader never parses a universal JSON. Built-ins
|
||||
# (already in bulk) win on a name collision.
|
||||
for ep in importlib.metadata.entry_points(group="prowler.compliance.universal"):
|
||||
# Built-ins are already in `bulk` and win on a name collision.
|
||||
for ep_dir in get_universal_compliance_entry_point_dirs():
|
||||
try:
|
||||
module = ep.load()
|
||||
ep_dir = (
|
||||
module.__path__[0]
|
||||
if hasattr(module, "__path__")
|
||||
else os.path.dirname(module.__file__)
|
||||
)
|
||||
if os.path.isdir(ep_dir):
|
||||
_load_jsons_from_dir(ep_dir, provider, bulk)
|
||||
_load_jsons_from_dir(ep_dir, provider, bulk)
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
|
||||
@@ -55,6 +55,7 @@ class KubernetesCIS(ComplianceOutput):
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
Context=finding.account_name,
|
||||
Cluster=finding.account_uid,
|
||||
Namespace=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
@@ -89,6 +90,7 @@ class KubernetesCIS(ComplianceOutput):
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
Context="",
|
||||
Cluster="",
|
||||
Namespace="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
|
||||
@@ -148,6 +148,7 @@ class KubernetesCISModel(BaseModel):
|
||||
Provider: str
|
||||
Description: str
|
||||
Context: str
|
||||
Cluster: str
|
||||
Namespace: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
|
||||
@@ -55,6 +55,7 @@ class KubernetesISO27001(ComplianceOutput):
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
Context=finding.account_name,
|
||||
Cluster=finding.account_uid,
|
||||
Namespace=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
@@ -82,6 +83,7 @@ class KubernetesISO27001(ComplianceOutput):
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
Context="",
|
||||
Cluster="",
|
||||
Namespace="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
|
||||
@@ -90,6 +90,7 @@ class KubernetesISO27001Model(BaseModel):
|
||||
Provider: str
|
||||
Description: str
|
||||
Context: str
|
||||
Cluster: str
|
||||
Namespace: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
|
||||