Compare commits

...

16 Commits

Author SHA1 Message Date
Adrián Peña 40b7cb3991 fix(api): skip scan tasks when provider was deleted (#11185) 2026-05-15 13:48:02 +02:00
Pedro Martín c1cfda530f docs(powerbi): add new documentation + update .pbit (#11177) 2026-05-15 09:58:46 +02:00
lydiavilchez 9894ac7bc3 feat(googleworkspace): implement Chat service with 6 CIS checks (#11126) 2026-05-14 17:19:11 +02:00
Pedro Martín 8ae076f22c fix(gcp): update duplicated CIS requirement ID (#11180) 2026-05-14 16:17:04 +02:00
Adrián Peña 27fb8518cd chore(sdk): pin root transitive deps to prevent silent drift (#11178) 2026-05-14 16:14:34 +02:00
Pepe Fagoaga 375de5dc87 docs: replace safety for osv-scanner (#11179) 2026-05-14 15:04:09 +02:00
Pepe Fagoaga 3410fc927a chore(security): replace safety with osv-scanner (#11167) 2026-05-14 14:35:09 +02:00
Alejandro Bailo dcf91ef252 feat(ui): add health check endpoint (#11145) 2026-05-14 13:47:48 +02:00
lydiavilchez bf4fd8fabd fix(googleworkspace): use per-service resources for Directory (#11176) 2026-05-14 13:07:06 +02:00
Alejandro Bailo 3d65208fd3 fix(ui): update vulnerable npm packages (#11173) 2026-05-14 12:55:29 +02:00
Adrián Peña 7d3ed62e90 chore(sdk): migrate from poetry to uv (#11162) 2026-05-14 12:51:57 +02:00
lydiavilchez 5f92989492 fix(googleworkspace): use per-service resources for Calendar and Drive (#11161) 2026-05-14 12:43:29 +02:00
Hugo Pereira Brito 6befa78978 fix(cloudflare): plan-aware WAF FAIL hints for zones (#9896) 2026-05-14 12:27:47 +02:00
lydiavilchez 78af0c24fe fix(googleworkspace): use per-service resources for Gmail (#11169) 2026-05-14 12:01:07 +02:00
Andoni Alonso 1bb547e5e1 docs(cloudflare): add pre-configured token creation links (#11156) 2026-05-14 11:58:00 +02:00
June 1f39b01fb2 feat(sagemaker): add sagemaker_domain_sso_configured check (#11094)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-05-14 11:42:30 +02:00
182 changed files with 12119 additions and 11879 deletions
+5 -6
View File
@@ -2,20 +2,19 @@
# Runs automatically on `wt switch --create`.
# Block 1: setup + copy gitignored env files (.envrc, ui/.env.local)
# from the primary worktree patterns selected via .worktreeinclude.
# from the primary worktree - patterns selected via .worktreeinclude.
[[pre-start]]
skills = "./skills/setup.sh --claude"
python = "poetry env use python3.12"
envs = "wt step copy-ignored"
# Block 2: install Python deps (requires `poetry env use` from block 1).
# Block 2: install Python deps (uv manages the venv on `uv sync`).
[[pre-start]]
deps = "poetry install --with dev"
deps = "uv sync"
# Block 3: reminder last visible output before `wt switch` returns.
# Block 3: reminder - last visible output before `wt switch` returns.
# Hooks can't mutate the parent shell, so venv activation is manual.
[[pre-start]]
reminder = "echo '>> Reminder: activate the venv in this shell with: eval $(poetry env activate)'"
reminder = "echo '>> Reminder: activate the venv in this shell with: source .venv/bin/activate'"
# Background: pnpm install runs while you start working.
# Tail logs via `wt config state logs`.
+169
View File
@@ -0,0 +1,169 @@
name: 'OSV-Scanner'
description: 'Install osv-scanner and scan a lockfile, failing on HIGH/CRITICAL/UNKNOWN severity findings. Posts/updates a PR comment with findings on pull_request events (requires pull-requests: write).'
author: 'Prowler'
inputs:
lockfile:
description: 'Path to the lockfile to scan, relative to the repository root (e.g. uv.lock, api/uv.lock, ui/pnpm-lock.yaml).'
required: true
severity-levels:
description: 'Comma-separated severity levels that fail the scan. Default: HIGH,CRITICAL,UNKNOWN.'
required: false
default: 'HIGH,CRITICAL,UNKNOWN'
version:
description: 'osv-scanner release tag to install. When overriding, you MUST also override binary-sha256.'
required: false
default: 'v2.3.8'
binary-sha256:
description: 'Expected SHA256 of osv-scanner_linux_amd64 for the given version. Default tracks v2.3.8. See https://github.com/google/osv-scanner/releases/download/<version>/osv-scanner_SHA256SUMS.'
required: false
default: 'bc98e15319ed0d515e3f9235287ba53cdc5535d576d24fd573978ecfe9ab92dc'
post-pr-comment:
description: 'Post or update a PR comment with the scan report. Only effective on pull_request events. Requires pull-requests: write permission on the caller job.'
required: false
default: 'true'
runs:
using: 'composite'
steps:
- name: Install osv-scanner
shell: bash
env:
OSV_SCANNER_VERSION: ${{ inputs.version }}
# Download the binary AND the published SHA256SUMS file, then verify the
# binary checksum against the upstream-signed manifest. Aborts on mismatch.
run: |
set -euo pipefail
if command -v osv-scanner >/dev/null 2>&1; then
INSTALLED="$(osv-scanner --version 2>&1 | awk '/scanner version/ {print $NF; exit}')"
if [ "v${INSTALLED}" = "${OSV_SCANNER_VERSION}" ]; then
echo "osv-scanner ${OSV_SCANNER_VERSION} already installed."
exit 0
fi
fi
BASE="https://github.com/google/osv-scanner/releases/download/${OSV_SCANNER_VERSION}"
BIN_NAME="osv-scanner_linux_amd64"
curl -fSL --retry 3 "${BASE}/${BIN_NAME}" -o "${RUNNER_TEMP}/${BIN_NAME}"
curl -fSL --retry 3 "${BASE}/osv-scanner_SHA256SUMS" -o "${RUNNER_TEMP}/osv-scanner_SHA256SUMS"
(cd "${RUNNER_TEMP}" && sha256sum --check --ignore-missing osv-scanner_SHA256SUMS)
chmod +x "${RUNNER_TEMP}/${BIN_NAME}"
sudo mv "${RUNNER_TEMP}/${BIN_NAME}" /usr/local/bin/osv-scanner
rm -f "${RUNNER_TEMP}/osv-scanner_SHA256SUMS"
osv-scanner --version
- name: Run osv-scanner
id: scan
shell: bash
working-directory: ${{ github.workspace }}
env:
OSV_LOCKFILE: ${{ inputs.lockfile }}
OSV_SEVERITY_LEVELS: ${{ inputs.severity-levels }}
OSV_REPORT_FILE: ${{ runner.temp }}/osv-scanner-findings.json
# Per-vulnerability ignores (reason + expiry) live in osv-scanner.toml at the repo root, if present.
# Severity filter is enforced in the wrapper via OSV_SEVERITY_LEVELS.
# `continue-on-error: true` lets the PR-comment step run even when findings exist;
# the gate step below re-fails the job from the wrapper exit code.
continue-on-error: true
run: ./.github/scripts/osv-scan.sh --lockfile="${OSV_LOCKFILE}"
- name: Post osv-scanner report on PR
if: >-
always()
&& inputs.post-pr-comment == 'true'
&& github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
OSV_REPORT_FILE: ${{ runner.temp }}/osv-scanner-findings.json
OSV_LOCKFILE: ${{ inputs.lockfile }}
OSV_SEVERITY_LEVELS: ${{ inputs.severity-levels }}
with:
script: |
const fs = require('fs');
const lockfile = process.env.OSV_LOCKFILE;
const severityLevels = process.env.OSV_SEVERITY_LEVELS;
const reportFile = process.env.OSV_REPORT_FILE;
const marker = `<!-- osv-scanner-report:${lockfile} -->`;
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
let findings = [];
if (fs.existsSync(reportFile)) {
try {
findings = JSON.parse(fs.readFileSync(reportFile, 'utf8'));
} catch (err) {
core.warning(`Could not parse ${reportFile}: ${err.message}`);
return;
}
}
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body?.includes(marker));
if (findings.length === 0) {
if (existing) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
});
core.info(`Deleted stale osv-scanner comment for ${lockfile}.`);
} else {
core.info(`No findings and no stale comment for ${lockfile}.`);
}
return;
}
const sevIcon = (s) => ({
CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🟢', UNKNOWN: '⚪',
}[s] || '⚪');
const escape = (s) => String(s ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
const rows = findings.map(f =>
`| ${sevIcon(f.severity)} ${f.severity}${f.score ? ` (${f.score})` : ''} | \`${escape(f.id)}\` | \`${escape(f.ecosystem)}/${escape(f.package)}\` | \`${escape(f.version)}\` | ${escape(f.summary || '(no summary)')} |`
);
const body = [
marker,
`## 🔒 osv-scanner: ${findings.length} finding(s) in \`${lockfile}\``,
'',
`Severity gate: \`${severityLevels}\``,
'',
'| Severity | ID | Package | Version | Summary |',
'|----------|----|---------|---------|---------|',
...rows,
'',
`To accept a finding, add an \`[[IgnoredVulns]]\` entry to \`osv-scanner.toml\` at the repo root with a reason and \`ignoreUntil\`.`,
'',
`<sub>[View run](${runUrl})</sub>`,
].join('\n');
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
core.info(`Updated osv-scanner comment for ${lockfile}.`);
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
core.info(`Posted new osv-scanner comment for ${lockfile}.`);
}
- name: Enforce osv-scanner severity gate
shell: bash
env:
SCAN_OUTCOME: ${{ steps.scan.outcome }}
run: |
if [ "${SCAN_OUTCOME}" != "success" ]; then
echo "osv-scanner gate: scan reported findings (outcome=${SCAN_OUTCOME})" >&2
exit 1
fi
@@ -1,100 +0,0 @@
name: 'Setup Python with Poetry'
description: 'Setup Python environment with Poetry and install dependencies'
author: 'Prowler'
inputs:
python-version:
description: 'Python version to use'
required: true
working-directory:
description: 'Working directory for Poetry'
required: false
default: '.'
poetry-version:
description: 'Poetry version to install'
required: false
default: '2.3.4'
install-dependencies:
description: 'Install Python dependencies with Poetry'
required: false
default: 'true'
update-lock:
description: 'Run `poetry lock` during setup. Only enable when a prior step mutates pyproject.toml (e.g. API `@master` VCS rewrite). Default: false.'
required: false
default: 'false'
enable-cache:
description: 'Whether to enable Poetry dependency caching via actions/setup-python'
required: false
default: 'true'
runs:
using: 'composite'
steps:
- name: Replace @master with current branch in pyproject.toml (prowler repo only)
if: github.event_name == 'pull_request' && github.base_ref == 'master' && github.repository == 'prowler-cloud/prowler'
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
run: |
BRANCH_NAME="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
UPSTREAM="prowler-cloud/prowler"
if [ "$HEAD_REPO" != "$UPSTREAM" ]; then
echo "Fork PR detected (${HEAD_REPO}), rewriting VCS URL to fork"
sed -i "s|git+https://github.com/prowler-cloud/prowler\([^@]*\)@master|git+https://github.com/${HEAD_REPO}\1@$BRANCH_NAME|g" pyproject.toml
else
echo "Same-repo PR, using branch: $BRANCH_NAME"
sed -i "s|\(git+https://github.com/prowler-cloud/prowler[^@]*\)@master|\1@$BRANCH_NAME|g" pyproject.toml
fi
- name: Install poetry
shell: bash
run: |
python -m pip install --upgrade pip
pipx install poetry==${INPUTS_POETRY_VERSION}
env:
INPUTS_POETRY_VERSION: ${{ inputs.poetry-version }}
- name: Update poetry.lock with latest Prowler commit
if: github.repository_owner == 'prowler-cloud' && github.repository != 'prowler-cloud/prowler'
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
LATEST_COMMIT=$(curl -s "https://api.github.com/repos/prowler-cloud/prowler/commits/master" | jq -r '.sha')
echo "Latest commit hash: $LATEST_COMMIT"
sed -i '/url = "https:\/\/github\.com\/prowler-cloud\/prowler\.git"/,/resolved_reference = / {
s/resolved_reference = "[a-f0-9]\{40\}"/resolved_reference = "'"$LATEST_COMMIT"'"/
}' poetry.lock
echo "Updated resolved_reference:"
grep -A2 -B2 "resolved_reference" poetry.lock
- name: Update poetry.lock (prowler repo only)
if: github.repository == 'prowler-cloud/prowler' && inputs.update-lock == 'true'
shell: bash
working-directory: ${{ inputs.working-directory }}
run: poetry lock
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ inputs.python-version }}
# Disable cache when callers skip dependency install: Poetry 2.3.4 creates
# the venv in a path setup-python can't hash, breaking the post-step save-cache.
cache: ${{ inputs.enable-cache == 'true' && 'poetry' || '' }}
cache-dependency-path: ${{ inputs.enable-cache == 'true' && format('{0}/poetry.lock', inputs.working-directory) || '' }}
- name: Install Python dependencies
if: inputs.install-dependencies == 'true'
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
poetry install --no-root
poetry run pip list
- name: Update Prowler Cloud API Client
if: github.repository_owner == 'prowler-cloud' && github.repository != 'prowler-cloud/prowler'
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
poetry remove prowler-cloud-api-client
poetry add ./prowler-cloud-api-client
+1 -1
View File
@@ -49,7 +49,7 @@ Please add a detailed description of how to review this PR.
- [ ] Performance test results (if applicable)
- [ ] Any other relevant evidence of the implementation (if applicable)
- [ ] Verify if API specs need to be regenerated.
- [ ] Check if version updates are required (e.g., specs, Poetry, etc.).
- [ ] Check if version updates are required (e.g., specs, uv, etc.).
- [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/api/CHANGELOG.md), if applicable.
### License
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
# Run osv-scanner and fail when findings match the configured severity levels.
#
# Replaces `safety check --policy-file .safety-policy.yml`. Used by:
# - .github/actions/osv-scanner/action.yml (composite action)
# - .github/workflows/api-security.yml, sdk-security.yml, ui-security.yml
#
# Severity levels (comma-separated) are read from OSV_SEVERITY_LEVELS.
# Default: HIGH,CRITICAL,UNKNOWN — preserves prior .safety-policy.yml policy
# (ignore-cvss-severity-below: 7 + ignore-cvss-unknown-severity: False).
# osv-scanner has no native CVSS threshold (google/osv-scanner#1400, closed
# not-planned). Severity is derived from $group.max_severity (numeric CVSS
# score string) which osv-scanner emits per group.
#
# CVSS v3 score → categorical label:
# CRITICAL >= 9.0
# HIGH >= 7.0
# MEDIUM >= 4.0
# LOW > 0.0
# UNKNOWN no score available
#
# Per-vulnerability ignores (with reason + expiry) live in osv-scanner.toml at
# the repo root, if it exists. Without that file, osv-scanner uses defaults.
#
# Usage:
# osv-scan.sh [osv-scanner pass-through args...]
# Examples:
# osv-scan.sh --lockfile=uv.lock
# osv-scan.sh --recursive .
# OSV_SEVERITY_LEVELS=CRITICAL osv-scan.sh --lockfile=uv.lock
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
CONFIG="${ROOT}/osv-scanner.toml"
SEVERITY_LEVELS="${OSV_SEVERITY_LEVELS:-HIGH,CRITICAL,UNKNOWN}"
for bin in osv-scanner jq; do
if ! command -v "${bin}" >/dev/null 2>&1; then
echo "error: ${bin} not found in PATH" >&2
exit 2
fi
done
SCAN_ARGS=()
if [ -f "${CONFIG}" ]; then
SCAN_ARGS+=(--config="${CONFIG}")
fi
# Exit codes: 0=clean, 1=findings, 127=no supported files, 128=internal error.
STDERR="$(mktemp)"
trap 'rm -f "${STDERR}"' EXIT
set +e
OUTPUT="$(osv-scanner scan source "${SCAN_ARGS[@]}" --format=json "$@" 2>"${STDERR}")"
RC=$?
set -e
case "${RC}" in
0|1) ;;
127) echo "osv-scanner: no supported lockfiles in scan target"; exit 0 ;;
*)
echo "osv-scanner: exited with code ${RC}" >&2
[ -s "${STDERR}" ] && cat "${STDERR}" >&2
exit "${RC}"
;;
esac
# Build a JSON array of normalized severity levels for jq.
SEVERITY_JSON="$(printf '%s' "${SEVERITY_LEVELS}" | jq -Rc '
split(",") | map(ascii_upcase | sub("^\\s+"; "") | sub("\\s+$"; ""))
')"
# Walk each vulnerability, look up its group's max_severity (numeric CVSS),
# map to a categorical label, then filter by OSV_SEVERITY_LEVELS.
FINDINGS="$(printf '%s' "${OUTPUT}" | jq --argjson sevs "${SEVERITY_JSON}" '
[ .results[]?.packages[]?
| . as $pkg
| ($pkg.groups // []) as $groups
| ($pkg.vulnerabilities // [])[]
| . as $vuln
| ([ $groups[] | select((.ids // []) | index($vuln.id)) ][0] // {}) as $group
| (($group.max_severity // "") | tonumber? // null) as $score
| (if $score == null then "UNKNOWN"
elif $score >= 9.0 then "CRITICAL"
elif $score >= 7.0 then "HIGH"
elif $score >= 4.0 then "MEDIUM"
elif $score > 0 then "LOW"
else "UNKNOWN"
end) as $label
| {
id: $vuln.id,
severity: $label,
score: $score,
summary: ($vuln.summary // null),
package: $pkg.package.name,
version: $pkg.package.version,
ecosystem: $pkg.package.ecosystem
}
| select(.severity as $s | $sevs | any(. == $s))
]
')"
COUNT="$(printf '%s' "${FINDINGS}" | jq 'length')"
# Write the findings JSON to OSV_REPORT_FILE so callers (e.g. the composite
# action's PR-comment step) can consume the same data the gate decision uses.
if [ -n "${OSV_REPORT_FILE:-}" ]; then
printf '%s' "${FINDINGS}" > "${OSV_REPORT_FILE}"
fi
if [ "${COUNT}" -gt 0 ]; then
echo "osv-scanner: ${COUNT} finding(s) at severity ${SEVERITY_LEVELS}"
printf '%s' "${FINDINGS}" | jq -r '
.[] | " [\(.severity)\(if .score then " \(.score)" else "" end)] \(.id) \(.ecosystem)/\(.package)@\(.version) — \(.summary // "(no summary)")"
'
echo
echo "To accept a finding, create osv-scanner.toml at the repo root with a reason and ignoreUntil."
exit 1
fi
echo "osv-scanner: no findings at severity levels: ${SEVERITY_LEVELS}"
+18 -9
View File
@@ -10,6 +10,8 @@ on:
- '.github/workflows/api-tests.yml'
- '.github/workflows/api-security.yml'
- '.github/actions/setup-python-uv/**'
- '.github/actions/osv-scanner/**'
- '.github/scripts/osv-scan.sh'
pull_request:
branches:
- "master"
@@ -19,6 +21,8 @@ on:
- '.github/workflows/api-tests.yml'
- '.github/workflows/api-security.yml'
- '.github/actions/setup-python-uv/**'
- '.github/actions/osv-scanner/**'
- '.github/scripts/osv-scan.sh'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -35,6 +39,7 @@ jobs:
timeout-minutes: 15
permissions:
contents: read
pull-requests: write # osv-scanner action posts/updates a PR comment with findings
strategy:
matrix:
python-version:
@@ -52,11 +57,12 @@ jobs:
pypi.org:443
files.pythonhosted.org:443
github.com:443
auth.safetycli.com:443
pyup.io:443
raw.githubusercontent.com:443
data.safetycli.com:443
api.github.com:443
objects.githubusercontent.com:443
release-assets.githubusercontent.com:443
api.osv.dev:443
api.deps.dev:443
osv-vulnerabilities.storage.googleapis.com:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -71,7 +77,8 @@ jobs:
files: |
api/**
.github/workflows/api-security.yml
.safety-policy.yml
.github/actions/osv-scanner/**
.github/scripts/osv-scan.sh
files_ignore: |
api/docs/**
api/README.md
@@ -91,11 +98,13 @@ jobs:
# bandit would recurse into installed third-party packages.
run: uv run bandit -q -lll -x '*_test.py,./contrib/,./.venv/' -r .
- name: Safety
- name: Dependency vulnerability scan with osv-scanner
if: steps.check-changes.outputs.any_changed == 'true'
# Accepted CVEs, severity threshold, and ignore expirations live in ../.safety-policy.yml
run: uv run safety check --policy-file ../.safety-policy.yml
uses: ./.github/actions/osv-scanner
with:
lockfile: api/uv.lock
- name: Vulture
if: steps.check-changes.outputs.any_changed == 'true'
# Run even when osv-scanner reports findings so dead-code signal isn't masked by SCA failures.
if: ${{ !cancelled() && steps.check-changes.outputs.any_changed == 'true' }}
run: uv run vulture --exclude "contrib,tests,conftest.py,.venv" --min-confidence 100 .
+3 -3
View File
@@ -59,7 +59,7 @@ jobs:
ui/**
prowler/**
mcp_server/**
poetry.lock
uv.lock
pyproject.toml
- name: Check for folder changes and changelog presence
@@ -84,9 +84,9 @@ jobs:
fi
done
# Check root-level dependency files (poetry.lock, pyproject.toml)
# Check root-level dependency files (uv.lock, pyproject.toml)
# These are associated with the prowler folder changelog
root_deps_changed=$(echo "${STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES}" | tr ' ' '\n' | grep -E "^(poetry\.lock|pyproject\.toml)$" || true)
root_deps_changed=$(echo "${STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES}" | tr ' ' '\n' | grep -E "^(uv\.lock|pyproject\.toml)$" || true)
if [ -n "$root_deps_changed" ]; then
echo "Detected changes in root dependency files: $root_deps_changed"
# Check if prowler/CHANGELOG.md was already updated (might have been caught above)
+2 -3
View File
@@ -40,12 +40,11 @@ jobs:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
persist-credentials: false
- name: Setup Python with Poetry
uses: ./.github/actions/setup-python-poetry
- name: Setup Python with uv
uses: ./.github/actions/setup-python-uv
with:
python-version: '3.12'
install-dependencies: 'false'
enable-cache: 'false'
- name: Configure Git
run: |
+7 -7
View File
@@ -71,26 +71,26 @@ jobs:
contrib/**
**/AGENTS.md
- name: Setup Python with Poetry
- name: Setup Python with uv
if: steps.check-changes.outputs.any_changed == 'true'
uses: ./.github/actions/setup-python-poetry
uses: ./.github/actions/setup-python-uv
with:
python-version: ${{ matrix.python-version }}
- name: Check Poetry lock file
- name: Check uv lock file
if: steps.check-changes.outputs.any_changed == 'true'
run: poetry check --lock
run: uv lock --check
- name: Lint with flake8
if: steps.check-changes.outputs.any_changed == 'true'
run: poetry run flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude contrib,ui,api,skills,mcp_server
run: uv run flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude .venv,contrib,ui,api,skills,mcp_server
- name: Check format with black
if: steps.check-changes.outputs.any_changed == 'true'
# mcp_server has its own pyproject and uses ruff format, exclude it so SDK black
# does not fight ruff over rules it never formatted.
run: poetry run black --exclude "api|ui|skills|mcp_server" --check .
run: uv run black --exclude "\.venv|api|ui|skills|mcp_server" --check .
- name: Lint with pylint
if: steps.check-changes.outputs.any_changed == 'true'
run: poetry run pylint --disable=W,C,R,E -j 0 -rn -sn prowler/
run: uv run pylint --disable=W,C,R,E -j 0 -rn -sn prowler/
+1 -11
View File
@@ -73,20 +73,10 @@ jobs:
with:
persist-credentials: false
- name: Setup Python with Poetry
uses: ./.github/actions/setup-python-poetry
with:
python-version: ${{ env.PYTHON_VERSION }}
install-dependencies: 'false'
enable-cache: 'false'
- name: Inject poetry-bumpversion plugin
run: pipx inject poetry poetry-bumpversion
- name: Get Prowler version and set tags
id: get-prowler-version
run: |
PROWLER_VERSION="$(poetry version -s 2>/dev/null)"
PROWLER_VERSION="$(grep -E '^version = ' pyproject.toml | sed -E 's/version = "([^"]+)"/\1/' | tr -d '[:space:]')"
echo "prowler_version=${PROWLER_VERSION}" >> "${GITHUB_OUTPUT}"
PROWLER_VERSION_MAJOR="${PROWLER_VERSION%%.*}"
+2 -2
View File
@@ -9,7 +9,7 @@ on:
- 'prowler/**'
- 'Dockerfile*'
- 'pyproject.toml'
- 'poetry.lock'
- 'uv.lock'
- '.github/workflows/sdk-container-checks.yml'
pull_request:
branches:
@@ -19,7 +19,7 @@ on:
- 'prowler/**'
- 'Dockerfile*'
- 'pyproject.toml'
- 'poetry.lock'
- 'uv.lock'
- '.github/workflows/sdk-container-checks.yml'
concurrency:
+6 -8
View File
@@ -75,15 +75,14 @@ jobs:
with:
persist-credentials: false
- name: Setup Python with Poetry
uses: ./.github/actions/setup-python-poetry
- name: Setup Python with uv
uses: ./.github/actions/setup-python-uv
with:
python-version: ${{ env.PYTHON_VERSION }}
install-dependencies: 'false'
enable-cache: 'false'
- name: Build Prowler package
run: poetry build
run: uv build
- name: Publish Prowler package to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
@@ -112,12 +111,11 @@ jobs:
with:
persist-credentials: false
- name: Setup Python with Poetry
uses: ./.github/actions/setup-python-poetry
- name: Setup Python with uv
uses: ./.github/actions/setup-python-uv
with:
python-version: ${{ env.PYTHON_VERSION }}
install-dependencies: 'false'
enable-cache: 'false'
- name: Install toml package
run: pip install toml
@@ -128,7 +126,7 @@ jobs:
python util/replicate_pypi_package.py
- name: Build prowler-cloud package
run: poetry build
run: uv build
- name: Publish prowler-cloud package to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
+24 -15
View File
@@ -9,10 +9,12 @@ on:
- 'prowler/**'
- 'tests/**'
- 'pyproject.toml'
- 'poetry.lock'
- 'uv.lock'
- '.github/workflows/sdk-tests.yml'
- '.github/workflows/sdk-security.yml'
- '.github/actions/setup-python-poetry/**'
- '.github/actions/setup-python-uv/**'
- '.github/actions/osv-scanner/**'
- '.github/scripts/osv-scan.sh'
pull_request:
branches:
- 'master'
@@ -21,10 +23,12 @@ on:
- 'prowler/**'
- 'tests/**'
- 'pyproject.toml'
- 'poetry.lock'
- 'uv.lock'
- '.github/workflows/sdk-tests.yml'
- '.github/workflows/sdk-security.yml'
- '.github/actions/setup-python-poetry/**'
- '.github/actions/setup-python-uv/**'
- '.github/actions/osv-scanner/**'
- '.github/scripts/osv-scan.sh'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -39,6 +43,7 @@ jobs:
timeout-minutes: 15
permissions:
contents: read
pull-requests: write # osv-scanner action posts/updates a PR comment with findings
steps:
- name: Harden Runner
@@ -49,10 +54,12 @@ jobs:
pypi.org:443
files.pythonhosted.org:443
github.com:443
auth.safetycli.com:443
pyup.io:443
data.safetycli.com:443
api.github.com:443
objects.githubusercontent.com:443
release-assets.githubusercontent.com:443
api.osv.dev:443
api.deps.dev:443
osv-vulnerabilities.storage.googleapis.com:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -87,21 +94,23 @@ jobs:
contrib/**
**/AGENTS.md
- name: Setup Python with Poetry
- name: Setup Python with uv
if: steps.check-changes.outputs.any_changed == 'true'
uses: ./.github/actions/setup-python-poetry
uses: ./.github/actions/setup-python-uv
with:
python-version: '3.12'
- name: Security scan with Bandit
if: steps.check-changes.outputs.any_changed == 'true'
run: poetry run bandit -q -lll -x '*_test.py,./contrib/,./api/,./ui' -r .
run: uv run bandit -q -lll -x '*_test.py,./.venv/,./contrib/,./api/,./ui' -r .
- name: Security scan with Safety
- name: Dependency vulnerability scan with osv-scanner
if: steps.check-changes.outputs.any_changed == 'true'
# Accepted CVEs, severity threshold, and ignore expirations live in .safety-policy.yml
run: poetry run safety check -r pyproject.toml --policy-file .safety-policy.yml
uses: ./.github/actions/osv-scanner
with:
lockfile: uv.lock
- name: Dead code detection with Vulture
if: steps.check-changes.outputs.any_changed == 'true'
run: poetry run vulture --exclude "contrib,api,ui" --min-confidence 100 .
# Run even when osv-scanner reports findings so dead-code signal isn't masked by SCA failures.
if: ${{ !cancelled() && steps.check-changes.outputs.any_changed == 'true' }}
run: uv run vulture --exclude ".venv,contrib,api,ui" --min-confidence 100 .
+35 -35
View File
@@ -92,9 +92,9 @@ jobs:
contrib/**
**/AGENTS.md
- name: Setup Python with Poetry
- name: Setup Python with uv
if: steps.check-changes.outputs.any_changed == 'true'
uses: ./.github/actions/setup-python-poetry
uses: ./.github/actions/setup-python-uv
with:
python-version: ${{ matrix.python-version }}
@@ -107,7 +107,7 @@ jobs:
files: |
./prowler/**/aws/**
./tests/**/aws/**
./poetry.lock
./uv.lock
- name: Resolve AWS services under test
if: steps.changed-aws.outputs.any_changed == 'true'
@@ -209,11 +209,11 @@ jobs:
echo "AWS service_paths='${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}'"
if [ "${STEPS_AWS_SERVICES_OUTPUTS_RUN_ALL}" = "true" ]; then
poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml tests/providers/aws
uv run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml tests/providers/aws
elif [ -z "${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}" ]; then
echo "No AWS service paths detected; skipping AWS tests."
else
poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml ${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}
uv run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml ${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}
fi
env:
STEPS_AWS_SERVICES_OUTPUTS_RUN_ALL: ${{ steps.aws-services.outputs.run_all }}
@@ -237,11 +237,11 @@ jobs:
files: |
./prowler/**/azure/**
./tests/**/azure/**
./poetry.lock
./uv.lock
- name: Run Azure tests
if: steps.changed-azure.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/azure --cov-report=xml:azure_coverage.xml tests/providers/azure
run: uv run pytest -n auto --cov=./prowler/providers/azure --cov-report=xml:azure_coverage.xml tests/providers/azure
- name: Upload Azure coverage to Codecov
if: steps.changed-azure.outputs.any_changed == 'true'
@@ -261,11 +261,11 @@ jobs:
files: |
./prowler/**/gcp/**
./tests/**/gcp/**
./poetry.lock
./uv.lock
- name: Run GCP tests
if: steps.changed-gcp.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/gcp --cov-report=xml:gcp_coverage.xml tests/providers/gcp
run: uv run pytest -n auto --cov=./prowler/providers/gcp --cov-report=xml:gcp_coverage.xml tests/providers/gcp
- name: Upload GCP coverage to Codecov
if: steps.changed-gcp.outputs.any_changed == 'true'
@@ -285,11 +285,11 @@ jobs:
files: |
./prowler/**/kubernetes/**
./tests/**/kubernetes/**
./poetry.lock
./uv.lock
- name: Run Kubernetes tests
if: steps.changed-kubernetes.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/kubernetes --cov-report=xml:kubernetes_coverage.xml tests/providers/kubernetes
run: uv run pytest -n auto --cov=./prowler/providers/kubernetes --cov-report=xml:kubernetes_coverage.xml tests/providers/kubernetes
- name: Upload Kubernetes coverage to Codecov
if: steps.changed-kubernetes.outputs.any_changed == 'true'
@@ -309,11 +309,11 @@ jobs:
files: |
./prowler/**/github/**
./tests/**/github/**
./poetry.lock
./uv.lock
- name: Run GitHub tests
if: steps.changed-github.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/github --cov-report=xml:github_coverage.xml tests/providers/github
run: uv run pytest -n auto --cov=./prowler/providers/github --cov-report=xml:github_coverage.xml tests/providers/github
- name: Upload GitHub coverage to Codecov
if: steps.changed-github.outputs.any_changed == 'true'
@@ -333,11 +333,11 @@ jobs:
files: |
./prowler/**/okta/**
./tests/**/okta/**
./poetry.lock
./uv.lock
- name: Run Okta tests
if: steps.changed-okta.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/okta --cov-report=xml:okta_coverage.xml tests/providers/okta
run: uv run pytest -n auto --cov=./prowler/providers/okta --cov-report=xml:okta_coverage.xml tests/providers/okta
- name: Upload Okta coverage to Codecov
if: steps.changed-okta.outputs.any_changed == 'true'
@@ -357,11 +357,11 @@ jobs:
files: |
./prowler/**/nhn/**
./tests/**/nhn/**
./poetry.lock
./uv.lock
- name: Run NHN tests
if: steps.changed-nhn.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/nhn --cov-report=xml:nhn_coverage.xml tests/providers/nhn
run: uv run pytest -n auto --cov=./prowler/providers/nhn --cov-report=xml:nhn_coverage.xml tests/providers/nhn
- name: Upload NHN coverage to Codecov
if: steps.changed-nhn.outputs.any_changed == 'true'
@@ -381,11 +381,11 @@ jobs:
files: |
./prowler/**/m365/**
./tests/**/m365/**
./poetry.lock
./uv.lock
- name: Run M365 tests
if: steps.changed-m365.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/m365 --cov-report=xml:m365_coverage.xml tests/providers/m365
run: uv run pytest -n auto --cov=./prowler/providers/m365 --cov-report=xml:m365_coverage.xml tests/providers/m365
- name: Upload M365 coverage to Codecov
if: steps.changed-m365.outputs.any_changed == 'true'
@@ -405,11 +405,11 @@ jobs:
files: |
./prowler/**/iac/**
./tests/**/iac/**
./poetry.lock
./uv.lock
- name: Run IaC tests
if: steps.changed-iac.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/iac --cov-report=xml:iac_coverage.xml tests/providers/iac
run: uv run pytest -n auto --cov=./prowler/providers/iac --cov-report=xml:iac_coverage.xml tests/providers/iac
- name: Upload IaC coverage to Codecov
if: steps.changed-iac.outputs.any_changed == 'true'
@@ -429,11 +429,11 @@ jobs:
files: |
./prowler/**/mongodbatlas/**
./tests/**/mongodbatlas/**
./poetry.lock
./uv.lock
- name: Run MongoDB Atlas tests
if: steps.changed-mongodbatlas.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/mongodbatlas --cov-report=xml:mongodbatlas_coverage.xml tests/providers/mongodbatlas
run: uv run pytest -n auto --cov=./prowler/providers/mongodbatlas --cov-report=xml:mongodbatlas_coverage.xml tests/providers/mongodbatlas
- name: Upload MongoDB Atlas coverage to Codecov
if: steps.changed-mongodbatlas.outputs.any_changed == 'true'
@@ -453,11 +453,11 @@ jobs:
files: |
./prowler/**/oraclecloud/**
./tests/**/oraclecloud/**
./poetry.lock
./uv.lock
- name: Run OCI tests
if: steps.changed-oraclecloud.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/oraclecloud --cov-report=xml:oraclecloud_coverage.xml tests/providers/oraclecloud
run: uv run pytest -n auto --cov=./prowler/providers/oraclecloud --cov-report=xml:oraclecloud_coverage.xml tests/providers/oraclecloud
- name: Upload OCI coverage to Codecov
if: steps.changed-oraclecloud.outputs.any_changed == 'true'
@@ -477,11 +477,11 @@ jobs:
files: |
./prowler/**/openstack/**
./tests/**/openstack/**
./poetry.lock
./uv.lock
- name: Run OpenStack tests
if: steps.changed-openstack.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/openstack --cov-report=xml:openstack_coverage.xml tests/providers/openstack
run: uv run pytest -n auto --cov=./prowler/providers/openstack --cov-report=xml:openstack_coverage.xml tests/providers/openstack
- name: Upload OpenStack coverage to Codecov
if: steps.changed-openstack.outputs.any_changed == 'true'
@@ -501,11 +501,11 @@ jobs:
files: |
./prowler/**/googleworkspace/**
./tests/**/googleworkspace/**
./poetry.lock
./uv.lock
- name: Run Google Workspace tests
if: steps.changed-googleworkspace.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/googleworkspace --cov-report=xml:googleworkspace_coverage.xml tests/providers/googleworkspace
run: uv run pytest -n auto --cov=./prowler/providers/googleworkspace --cov-report=xml:googleworkspace_coverage.xml tests/providers/googleworkspace
- name: Upload Google Workspace coverage to Codecov
if: steps.changed-googleworkspace.outputs.any_changed == 'true'
@@ -525,11 +525,11 @@ jobs:
files: |
./prowler/**/vercel/**
./tests/**/vercel/**
./poetry.lock
./uv.lock
- name: Run Vercel tests
if: steps.changed-vercel.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/providers/vercel --cov-report=xml:vercel_coverage.xml tests/providers/vercel
run: uv run pytest -n auto --cov=./prowler/providers/vercel --cov-report=xml:vercel_coverage.xml tests/providers/vercel
- name: Upload Vercel coverage to Codecov
if: steps.changed-vercel.outputs.any_changed == 'true'
@@ -549,11 +549,11 @@ jobs:
files: |
./prowler/lib/**
./tests/lib/**
./poetry.lock
./uv.lock
- name: Run Lib tests
if: steps.changed-lib.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/lib --cov-report=xml:lib_coverage.xml tests/lib
run: uv run pytest -n auto --cov=./prowler/lib --cov-report=xml:lib_coverage.xml tests/lib
- name: Upload Lib coverage to Codecov
if: steps.changed-lib.outputs.any_changed == 'true'
@@ -573,11 +573,11 @@ jobs:
files: |
./prowler/config/**
./tests/config/**
./poetry.lock
./uv.lock
- name: Run Config tests
if: steps.changed-config.outputs.any_changed == 'true'
run: poetry run pytest -n auto --cov=./prowler/config --cov-report=xml:config_coverage.xml tests/config
run: uv run pytest -n auto --cov=./prowler/config --cov-report=xml:config_coverage.xml tests/config
- name: Upload Config coverage to Codecov
if: steps.changed-config.outputs.any_changed == 'true'
+2 -2
View File
@@ -132,8 +132,8 @@ jobs:
- name: Build API image from current code
# docker-compose.yml references prowlercloud/prowler-api:latest from the registry,
# which lags behind PR changes (e.g. the poetry -> uv migration); build locally so
# E2E exercises the API image produced by this PR.
# which lags behind PR changes; build locally so E2E exercises the API image
# produced by this PR.
run: docker build -t prowlercloud/prowler-api:latest ./api
- name: Start API services
+75
View File
@@ -0,0 +1,75 @@
name: 'UI: Security'
on:
push:
branches:
- 'master'
- 'v5.*'
paths:
- 'ui/package.json'
- 'ui/pnpm-lock.yaml'
- '.github/workflows/ui-security.yml'
- '.github/actions/osv-scanner/**'
- '.github/scripts/osv-scan.sh'
pull_request:
branches:
- 'master'
- 'v5.*'
paths:
- 'ui/package.json'
- 'ui/pnpm-lock.yaml'
- '.github/workflows/ui-security.yml'
- '.github/actions/osv-scanner/**'
- '.github/scripts/osv-scan.sh'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
ui-security-scans:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: write # osv-scanner action posts/updates a PR comment with findings
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
egress-policy: block
allowed-endpoints: >
github.com:443
api.github.com:443
objects.githubusercontent.com:443
release-assets.githubusercontent.com:443
api.osv.dev:443
api.deps.dev:443
osv-vulnerabilities.storage.googleapis.com:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# zizmor: ignore[artipacked]
persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch
- name: Check for UI dependency changes
id: check-changes
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
with:
files: |
ui/package.json
ui/pnpm-lock.yaml
.github/workflows/ui-security.yml
.github/actions/osv-scanner/**
.github/scripts/osv-scan.sh
- name: Dependency vulnerability scan with osv-scanner
if: steps.check-changes.outputs.any_changed == 'true'
uses: ./.github/actions/osv-scanner
with:
lockfile: ui/pnpm-lock.yaml
+5 -26
View File
@@ -107,7 +107,7 @@ repos:
files: { glob: ["{api,mcp_server}/**/*.py"] }
priority: 20
## PYTHON — uv (API)
## PYTHON — uv (API + SDK)
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.11.14
hooks:
@@ -118,21 +118,10 @@ repos:
pass_filenames: false
priority: 50
## PYTHON — Poetry (SDK)
- repo: https://github.com/python-poetry/poetry
rev: 2.3.4
hooks:
- id: poetry-check
name: SDK - poetry-check
args: ["--directory=./"]
files: { glob: ["{pyproject.toml,poetry.lock}"] }
pass_filenames: false
priority: 50
- id: poetry-lock
name: SDK - poetry-lock
args: ["--directory=./"]
files: { glob: ["{pyproject.toml,poetry.lock}"] }
- id: uv-lock
name: SDK - uv-lock
args: ["--check", "--project=./"]
files: { glob: ["{pyproject.toml,uv.lock}"] }
pass_filenames: false
priority: 50
@@ -176,16 +165,6 @@ repos:
exclude: { glob: ["{contrib,skills}/**", "**/.venv/**", "**/*_test.py"] }
priority: 40
- id: safety
name: safety
description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities"
# Accepted CVEs, severity threshold, and ignore expirations live in .safety-policy.yml
entry: safety check --policy-file .safety-policy.yml
language: system
pass_filenames: false
files: { glob: ["**/pyproject.toml", "**/poetry.lock", "**/uv.lock", "**/requirements*.txt", ".safety-policy.yml"] }
priority: 40
- id: vulture
name: vulture
description: "Vulture finds unused code in Python programs."
+2 -6
View File
@@ -11,15 +11,11 @@ build:
python: "3.11"
jobs:
post_create_environment:
# Install poetry
# https://python-poetry.org/docs/#installing-manually
- python -m pip install poetry==2.3.4
- python -m pip install uv==0.11.14
post_install:
# Install dependencies with 'docs' dependency group
# https://python-poetry.org/docs/managing-dependencies/#dependency-groups
# VIRTUAL_ENV needs to be set manually for now.
# See https://github.com/readthedocs/readthedocs.org/pull/11152/
- VIRTUAL_ENV=${READTHEDOCS_VIRTUALENV_PATH} python -m poetry install --only=docs
- VIRTUAL_ENV=${READTHEDOCS_VIRTUALENV_PATH} uv sync --group docs --no-install-project
mkdocs:
configuration: mkdocs.yml
-58
View File
@@ -1,58 +0,0 @@
# Safety policy for `safety check` (Safety CLI 3.x, v2 schema).
# Applied in: .pre-commit-config.yaml, .github/workflows/api-security.yml,
# .github/workflows/sdk-security.yml via `--policy-file`.
#
# Validate: poetry run safety validate policy_file --path .safety-policy.yml
security:
# Scan unpinned requirements too. Prowler pins via poetry.lock, so this is
# defensive against accidental unpinned entries.
ignore-unpinned-requirements: False
# CVSS severity filter. 7 = report only HIGH (7.08.9) and CRITICAL (9.010.0).
# Reference: 9=CRITICAL only, 7=CRITICAL+HIGH, 4=CRITICAL+HIGH+MEDIUM.
ignore-cvss-severity-below: 7
# Unknown severity is unrated, not safe. Keep False so unrated CVEs still fail
# the build and get a human eye. Flip to True only if noise is unmanageable.
ignore-cvss-unknown-severity: False
# Fail the build when a non-ignored vulnerability is found.
continue-on-vulnerability-error: False
# Explicit accepted vulnerabilities. Each entry MUST have a reason and an
# expiry. Expired entries fail the scan, forcing re-audit.
ignore-vulnerabilities:
77744:
reason: "Botocore requires urllib3 1.X. Remove once upgraded to urllib3 2.X."
expires: '2026-10-22'
77745:
reason: "Botocore requires urllib3 1.X. Remove once upgraded to urllib3 2.X."
expires: '2026-10-22'
79023:
reason: "knack ReDoS; blocked until azure-cli-core (via cartography) allows knack >=0.13.0."
expires: '2026-10-22'
79027:
reason: "knack ReDoS; blocked until azure-cli-core (via cartography) allows knack >=0.13.0."
expires: '2026-10-22'
86217:
reason: "alibabacloud-tea-openapi==0.4.3 blocks upgrade to cryptography >=46.0.0."
expires: '2026-10-22'
71600:
reason: "CVE-2024-1135 false positive. Fixed in gunicorn 22.0.0; project uses 23.0.0."
expires: '2026-10-22'
70612:
reason: "TBD - audit required. Reason not documented in prior --ignore list."
expires: '2026-07-22'
66963:
reason: "TBD - audit required. Reason not documented in prior --ignore list."
expires: '2026-07-22'
74429:
reason: "TBD - audit required. Reason not documented in prior --ignore list."
expires: '2026-07-22'
76352:
reason: "TBD - audit required. Reason not documented in prior --ignore list."
expires: '2026-07-22'
76353:
reason: "TBD - audit required. Reason not documented in prior --ignore list."
expires: '2026-07-22'
+6 -6
View File
@@ -148,7 +148,7 @@ Prowler is an open-source cloud security assessment tool supporting AWS, Azure,
| Component | Location | Tech Stack |
|-----------|----------|------------|
| SDK | `prowler/` | Python 3.10+, Poetry 2.3+ |
| SDK | `prowler/` | Python 3.10+, uv |
| API | `api/` | Django 5.1, DRF, Celery |
| UI | `ui/` | Next.js 16, React 19, Tailwind 4 |
| MCP Server | `mcp_server/` | FastMCP, Python 3.12+ |
@@ -160,13 +160,13 @@ Prowler is an open-source cloud security assessment tool supporting AWS, Azure,
```bash
# Setup
poetry install --with dev
poetry run prek install
uv sync
uv run prek install
# Code quality
poetry run make lint
poetry run make format
poetry run prek run --all-files
uv run make lint
uv run make format
uv run prek run --all-files
```
---
+6 -6
View File
@@ -78,7 +78,7 @@ WORKDIR /home/prowler
# Copy necessary files
COPY prowler/ /home/prowler/prowler/
COPY dashboard/ /home/prowler/dashboard/
COPY pyproject.toml /home/prowler
COPY pyproject.toml uv.lock /home/prowler/
COPY README.md /home/prowler/
COPY prowler/providers/m365/lib/powershell/m365_powershell.py /home/prowler/prowler/providers/m365/lib/powershell/m365_powershell.py
@@ -87,17 +87,17 @@ ENV HOME='/home/prowler'
ENV PATH="${HOME}/.local/bin:${PATH}"
#hadolint ignore=DL3013
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir poetry==2.3.4
pip install --no-cache-dir uv==0.11.14
RUN poetry install --compile && \
rm -rf ~/.cache/pip
RUN uv sync --compile-bytecode && \
rm -rf ~/.cache/uv
# Install PowerShell modules
RUN poetry run python prowler/providers/m365/lib/powershell/m365_powershell.py
RUN .venv/bin/python prowler/providers/m365/lib/powershell/m365_powershell.py
# Remove deprecated dash dependencies
RUN pip uninstall dash-html-components -y && \
pip uninstall dash-core-components -y
USER prowler
ENTRYPOINT ["poetry", "run", "prowler"]
ENTRYPOINT [".venv/bin/prowler"]
+2 -3
View File
@@ -23,7 +23,7 @@ format: ## Format Code
lint: ## Lint Code
@echo "Running flake8..."
flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude contrib
flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude .venv,contrib
@echo "Running black... "
black --check .
@echo "Running pylint..."
@@ -35,7 +35,7 @@ pypi-clean: ## Delete the distribution files
pypi-build: ## Build package
$(MAKE) pypi-clean && \
poetry build
uv build
pypi-upload: ## Upload package
python3 -m twine upload --repository pypi dist/*
@@ -56,4 +56,3 @@ run-api-dev: ## Start development environment with API, PostgreSQL, Valkey, MCP,
##@ Development Environment
build-and-run-api-dev: build-no-cache-dev run-api-dev
+10 -21
View File
@@ -177,7 +177,7 @@ You can find more information in the [Troubleshooting](./docs/troubleshooting.md
**Requirements**
* `git` installed.
* `poetry` v2 installed: [poetry installation](https://python-poetry.org/docs/#installation).
* `uv` installed: [uv installation](https://docs.astral.sh/uv/getting-started/installation/).
* `pnpm` installed: [pnpm installation](https://pnpm.io/installation).
* `Docker Compose` installed: https://docs.docker.com/compose/install/.
@@ -186,8 +186,8 @@ You can find more information in the [Troubleshooting](./docs/troubleshooting.md
``` console
git clone https://github.com/prowler-cloud/prowler
cd prowler/api
poetry install
eval $(poetry env activate)
uv sync
source .venv/bin/activate
set -a
source .env
docker compose up postgres valkey -d
@@ -195,11 +195,6 @@ cd src/backend
python manage.py migrate --database admin
gunicorn -c config/guniconf.py config.wsgi:application
```
> [!IMPORTANT]
> As of Poetry v2.0.0, the `poetry shell` command has been deprecated. Use `poetry env activate` instead for environment activation.
>
> If your Poetry version is below v2.0.0, continue using `poetry shell` to activate your environment.
> For further guidance, refer to the Poetry Environment Activation Guide https://python-poetry.org/docs/managing-environments/#activating-the-environment.
> After completing the setup, access the API documentation at http://localhost:8080/api/v1/docs.
@@ -208,8 +203,8 @@ gunicorn -c config/guniconf.py config.wsgi:application
``` console
git clone https://github.com/prowler-cloud/prowler
cd prowler/api
poetry install
eval $(poetry env activate)
uv sync
source .venv/bin/activate
set -a
source .env
cd src/backend
@@ -221,8 +216,8 @@ python -m celery -A config.celery worker -l info -E
``` console
git clone https://github.com/prowler-cloud/prowler
cd prowler/api
poetry install
eval $(poetry env activate)
uv sync
source .venv/bin/activate
set -a
source .env
cd src/backend
@@ -283,24 +278,18 @@ The container images are available here:
### From GitHub
Python >=3.10, <3.13 is required with pip and Poetry:
Python >=3.10, <3.13 is required with [uv](https://docs.astral.sh/uv/):
``` console
git clone https://github.com/prowler-cloud/prowler
cd prowler
eval $(poetry env activate)
poetry install
uv sync
source .venv/bin/activate
python prowler-cli.py -v
```
> [!IMPORTANT]
> To clone Prowler on Windows, configure Git to support long file paths by running the following command: `git config core.longpaths true`.
> [!IMPORTANT]
> As of Poetry v2.0.0, the `poetry shell` command has been deprecated. Use `poetry env activate` instead for environment activation.
>
> If your Poetry version is below v2.0.0, continue using `poetry shell` to activate your environment.
> For further guidance, refer to the Poetry Environment Activation Guide https://python-poetry.org/docs/managing-environments/#activating-the-environment.
# 🛡️ GitHub Action
The official **Prowler GitHub Action** runs Prowler scans in your GitHub workflows using the official [`prowlercloud/prowler`](https://hub.docker.com/r/prowlercloud/prowler) Docker image. Scans run on any [supported provider](https://docs.prowler.com/user-guide/providers/), with optional [`--push-to-cloud`](https://docs.prowler.com/user-guide/tutorials/prowler-app-import-findings) to send findings to Prowler Cloud and optional SARIF upload so findings show up in the repo's **Security → Code scanning** tab and as inline PR annotations.
+1 -1
View File
@@ -167,7 +167,7 @@ runs:
- name: Upload SARIF to GitHub Code Scanning
if: always() && inputs.upload-sarif == 'true' && steps.find-sarif.outputs.sarif_path != ''
uses: github/codeql-action/upload-sarif@d4b3ca9fa7f69d38bfcd667bdc45bc373d16277e # v4
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
with:
sarif_file: ${{ steps.find-sarif.outputs.sarif_path }}
category: ${{ inputs.sarif-category }}
+4
View File
@@ -13,6 +13,10 @@ All notable changes to the **Prowler API** are documented in this file.
- Replace `poetry` with `uv` (`0.11.14`) as the API package manager; migrate `pyproject.toml` to `[dependency-groups]` and regenerate as `uv.lock` [(#10775)](https://github.com/prowler-cloud/prowler/pull/10775)
- Remove orphaned `gin_resources_search_idx` declaration from `Resource.Meta.indexes` (DB index dropped in `0072_drop_unused_indexes`) [(#11001)](https://github.com/prowler-cloud/prowler/pull/11001)
### 🐞 Fixed
- `perform_scan_task` and `perform_scheduled_scan_task` now short-circuit with a warning and `return None` when the target provider no longer exists, instead of letting `handle_provider_deletion` raise `ProviderDeletedException`. `perform_scheduled_scan_task` also removes any orphan `PeriodicTask` it finds so beat stops re-firing scans for deleted providers. Prevents queued messages for deleted providers from being recorded as `FAILURE` and, in one-shot scan-worker deployments, from burning a fresh container per redelivery [(#11185)](https://github.com/prowler-cloud/prowler/pull/11185)
---
## [1.27.2] (Prowler UNRELEASED)
-3
View File
@@ -15,7 +15,6 @@ dev = [
"pytest-randomly==3.15.0",
"pytest-xdist==3.6.1",
"ruff==0.5.0",
"safety==3.7.0",
"tqdm==4.67.1",
"vulture==2.14",
"prek==0.3.9"
@@ -393,8 +392,6 @@ constraint-dependencies = [
"ruamel-yaml==0.19.1",
"ruff==0.5.0",
"s3transfer==0.14.0",
"safety==3.7.0",
"safety-schemas==0.0.16",
"scaleway==2.10.3",
"scaleway-core==2.10.3",
"schema==0.7.5",
+22 -1
View File
@@ -69,7 +69,7 @@ from tasks.utils import (
from api.compliance import get_compliance_frameworks
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.db_utils import delete_related_daily_task, rls_transaction
from api.decorators import handle_provider_deletion, set_tenant
from api.models import Finding, Integration, Provider, Scan, ScanSummary, StateChoices
from api.utils import initialize_prowler_provider
@@ -274,6 +274,17 @@ def perform_scan_task(
Returns:
dict: The result of the scan execution, typically including the status and results of the performed checks.
"""
with rls_transaction(tenant_id):
if not Provider.objects.filter(pk=provider_id).exists():
logger.warning(
"scan-perform skipped: provider %s no longer exists "
"(tenant=%s, scan=%s)",
provider_id,
tenant_id,
scan_id,
)
return None
result = perform_prowler_scan(
tenant_id=tenant_id,
scan_id=scan_id,
@@ -310,6 +321,16 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
task_id = self.request.id
with rls_transaction(tenant_id):
if not Provider.objects.filter(pk=provider_id).exists():
logger.warning(
"scheduled scan-perform skipped: provider %s no longer exists "
"(tenant=%s)",
provider_id,
tenant_id,
)
delete_related_daily_task(provider_id)
return None
periodic_task_instance = PeriodicTask.objects.get(
name=f"scan-perform-scheduled-{provider_id}"
)
+52
View File
@@ -21,6 +21,7 @@ from tasks.tasks import (
check_lighthouse_provider_connection_task,
generate_outputs_task,
perform_attack_paths_scan_task,
perform_scan_task,
perform_scheduled_scan_task,
reaggregate_all_finding_group_summaries_task,
refresh_lighthouse_provider_models_task,
@@ -2454,6 +2455,57 @@ class TestPerformScheduledScanTask:
== 1
)
def test_no_op_when_provider_does_not_exist(self, tenants_fixture):
"""Return None without raising when the provider was already deleted."""
tenant = tenants_fixture[0]
missing_provider_id = str(uuid.uuid4())
task_id = str(uuid.uuid4())
self._create_task_result(tenant.id, task_id)
# Orphan PeriodicTask left behind from a previous lifecycle.
self._create_periodic_task(missing_provider_id, tenant.id)
orphan_name = f"scan-perform-scheduled-{missing_provider_id}"
assert PeriodicTask.objects.filter(name=orphan_name).exists()
with (
patch("tasks.tasks.perform_prowler_scan") as mock_scan,
patch("tasks.tasks._perform_scan_complete_tasks") as mock_complete_tasks,
self._override_task_request(perform_scheduled_scan_task, id=task_id),
):
result = perform_scheduled_scan_task.run(
tenant_id=str(tenant.id), provider_id=missing_provider_id
)
assert result is None
mock_scan.assert_not_called()
mock_complete_tasks.assert_not_called()
# Orphan PeriodicTask is cleaned up so beat stops re-firing it.
assert not PeriodicTask.objects.filter(name=orphan_name).exists()
@pytest.mark.django_db
class TestPerformScanTask:
"""Unit tests for perform_scan_task."""
def test_no_op_when_provider_does_not_exist(self, tenants_fixture):
"""Return None without raising when the provider was already deleted."""
tenant = tenants_fixture[0]
missing_provider_id = str(uuid.uuid4())
scan_id = str(uuid.uuid4())
with (
patch("tasks.tasks.perform_prowler_scan") as mock_scan,
patch("tasks.tasks._perform_scan_complete_tasks") as mock_complete_tasks,
):
result = perform_scan_task.run(
tenant_id=str(tenant.id),
scan_id=scan_id,
provider_id=missing_provider_id,
)
assert result is None
mock_scan.assert_not_called()
mock_complete_tasks.assert_not_called()
@pytest.mark.django_db
class TestReaggregateAllFindingGroupSummaries:
Generated
-145
View File
@@ -329,8 +329,6 @@ constraints = [
{ name = "ruamel-yaml", specifier = "==0.19.1" },
{ name = "ruff", specifier = "==0.5.0" },
{ name = "s3transfer", specifier = "==0.14.0" },
{ name = "safety", specifier = "==3.7.0" },
{ name = "safety-schemas", specifier = "==0.0.16" },
{ name = "scaleway", specifier = "==2.10.3" },
{ name = "scaleway-core", specifier = "==2.10.3" },
{ name = "schema", specifier = "==0.7.5" },
@@ -1030,18 +1028,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
]
[[package]]
name = "authlib"
version = "1.6.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" },
]
[[package]]
name = "autopep8"
version = "2.3.2"
@@ -2511,18 +2497,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/80/12235e5b75bb2c586733280854f131b86051e0bbdfb55349ff70d0f72cf9/dogpile_cache-1.5.0-py3-none-any.whl", hash = "sha256:dc7b47d37844db15e8fdc0243c1b58857a2ddc52a5118237a97127bac200e18d", size = 64447, upload-time = "2025-10-11T17:35:38.573Z" },
]
[[package]]
name = "dparse"
version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/29/ee/96c65e17222b973f0d3d0aa9bad6a59104ca1b0eb5b659c25c2900fccd85/dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a", size = 27912, upload-time = "2024-11-08T16:52:06.444Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/26/035d1c308882514a1e6ddca27f9d3e570d67a0e293e7b4d910a70c8fe32b/dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57", size = 11925, upload-time = "2024-11-08T16:52:03.844Z" },
]
[[package]]
name = "drf-extensions"
version = "0.8.0"
@@ -3318,15 +3292,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
]
[[package]]
name = "joblib"
version = "1.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
]
[[package]]
name = "jsonpatch"
version = "1.33"
@@ -3990,21 +3955,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
]
[[package]]
name = "nltk"
version = "3.9.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "joblib" },
{ name = "regex" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" },
]
[[package]]
name = "numpy"
version = "2.0.2"
@@ -4596,7 +4546,6 @@ dev = [
{ name = "pytest-randomly" },
{ name = "pytest-xdist" },
{ name = "ruff" },
{ name = "safety" },
{ name = "tqdm" },
{ name = "vulture" },
]
@@ -4661,7 +4610,6 @@ dev = [
{ name = "pytest-randomly", specifier = "==3.15.0" },
{ name = "pytest-xdist", specifier = "==3.6.1" },
{ name = "ruff", specifier = "==0.5.0" },
{ name = "safety", specifier = "==3.7.0" },
{ name = "tqdm", specifier = "==4.67.1" },
{ name = "vulture", specifier = "==2.14" },
]
@@ -5257,46 +5205,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
]
[[package]]
name = "regex"
version = "2026.1.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" },
{ url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" },
{ url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" },
{ url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" },
{ url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" },
{ url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" },
{ url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" },
{ url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" },
{ url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" },
{ url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" },
{ url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" },
{ url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" },
{ url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" },
{ url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" },
{ url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" },
{ url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" },
{ url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" },
{ url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" },
{ url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" },
{ url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" },
{ url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" },
{ url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" },
{ url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" },
{ url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" },
{ url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" },
{ url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" },
{ url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" },
{ url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" },
{ url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" },
{ url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" },
]
[[package]]
name = "reportlab"
version = "4.4.10"
@@ -5448,15 +5356,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
]
[[package]]
name = "ruamel-yaml"
version = "0.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" },
]
[[package]]
name = "ruff"
version = "0.5.0"
@@ -5494,50 +5393,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" },
]
[[package]]
name = "safety"
version = "3.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "authlib" },
{ name = "click" },
{ name = "dparse" },
{ name = "filelock" },
{ name = "httpx" },
{ name = "jinja2" },
{ name = "marshmallow" },
{ name = "nltk" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "ruamel-yaml" },
{ name = "safety-schemas" },
{ name = "tenacity" },
{ name = "tomlkit" },
{ name = "typer" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6f/e8/1cfffa0d8836de8aa31f4fa7fdeb892c7cfa97cd555039ad5df71ce0e968/safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e", size = 330538, upload-time = "2025-11-06T20:10:15.067Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/55/c4b2058ca346e58124ba082a3596e30dc1f5793710f8173156c7c2d77048/safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf", size = 312436, upload-time = "2025-11-06T20:10:13.576Z" },
]
[[package]]
name = "safety-schemas"
version = "0.0.16"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dparse" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "ruamel-yaml" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/ef/0e07dfdb4104c4e42ae9fc6e8a0da7be2d72ac2ee198b32f7500796de8f3/safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e", size = 54815, upload-time = "2025-09-16T14:35:31.973Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/a2/7840cc32890ce4b84668d3d9dfe15a48355b683ae3fb627ac97ac5a4265f/safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44", size = 39292, upload-time = "2025-09-16T14:35:32.84Z" },
]
[[package]]
name = "scaleway"
version = "2.10.3"
@@ -1,8 +1,9 @@
#!/bin/bash
# Run Prowler against All AWS Accounts in an AWS Organization
# Activate Poetry Environment
eval "$(poetry env activate)"
# Activate uv-managed virtualenv
# shellcheck disable=SC1091
source .venv/bin/activate
# Show Prowler Version
prowler -v
+7 -1
View File
@@ -48,10 +48,16 @@ services:
- path: .env
required: false
ports:
- ${UI_PORT:-3000}:${UI_PORT:-3000}
- ${UI_PORT:-3000}:3000
depends_on:
mcp-server:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:3000/api/health || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 60s
postgres:
image: postgres:16.3-alpine3.20
+1 -1
View File
@@ -467,7 +467,7 @@ Effective headers and section titles enhance document readability and structure,
* **Example:**
* How to Clone and Install Prowler from GitHub (header: Title case)
* How to install poetry dependencies (subheading: Sentence case)
* How to install uv dependencies (subheading: Sentence case)
5. **Using Keywords in Headers**
Headers should include relevant keywords to improve document searchability:
* **Good:** Scanning AWS Accounts in Parallel
+2 -2
View File
@@ -20,8 +20,8 @@ The most common high level steps to create a new check are:
3. Create a check-specific folder. The path should follow this pattern: `prowler/providers/<provider>/services/<service>/<check_name_want_to_implement>`. Adhere to the [Naming Format for Checks](#naming-format-for-checks).
4. Populate the folder with files as specified in [File Creation](#file-creation).
5. Run the check locally to ensure it works as expected. For checking you can use the CLI in the next way:
- To ensure the check has been detected by Prowler: `poetry run python prowler-cli.py <provider> --list-checks | grep <check_name>`.
- To run the check, to find possible issues: `poetry run python prowler-cli.py <provider> --log-level ERROR --verbose --check <check_name>`.
- To ensure the check has been detected by Prowler: `uv run python prowler-cli.py <provider> --list-checks | grep <check_name>`.
- To run the check, to find possible issues: `uv run python prowler-cli.py <provider> --log-level ERROR --verbose --check <check_name>`.
6. Create comprehensive tests for the check that cover multiple scenarios including both PASS (compliant) and FAIL (non-compliant) cases. For detailed information about test structure and implementation guidelines, refer to the [Testing](/developer-guide/unit-testing) documentation.
7. If the check and its corresponding tests are working as expected, you can submit a PR to Prowler.
+10 -17
View File
@@ -80,7 +80,7 @@ Before proceeding, ensure the following:
- Git is installed.
- Python 3.10 or higher is installed.
- `poetry` is installed to manage dependencies.
- `uv` is installed to manage dependencies.
### Forking the Prowler Repository
@@ -97,28 +97,21 @@ cd prowler
### Dependency Management and Environment Isolation
To prevent conflicts between environments, we recommend using `poetry`, a Python dependency management solution. Install it by following the [instructions](https://python-poetry.org/docs/#installation).
To prevent conflicts between environments, we recommend using [`uv`](https://docs.astral.sh/uv/), a fast Python package and project manager. Install it by following the [official instructions](https://docs.astral.sh/uv/getting-started/installation/).
### Installing Dependencies
To install all required dependencies, including those needed for development, run:
```
poetry install --with dev
eval $(poetry env activate)
uv sync
source .venv/bin/activate
```
<Warning>
Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
In case you have any doubts, consult the [Poetry environment activation guide](https://python-poetry.org/docs/managing-environments/#activating-the-environment).
</Warning>
### Pre-Commit Hooks
This repository uses Git pre-commit hooks managed by the [prek](https://prek.j178.dev/) tool, it is installed with `poetry install --with dev`. Next, run the following command in the root of this repository:
This repository uses Git pre-commit hooks managed by the [prek](https://prek.j178.dev/) tool, it is installed with `uv sync`. Next, run the following command in the root of this repository:
```shell
prek install
@@ -155,11 +148,11 @@ Once installed, TruffleHog runs before each push and blocks the operation when v
Before merging pull requests, several automated checks and utilities ensure code security and updated dependencies:
<Note>
These should have been already installed if `poetry install --with dev` was already run.
These should have been already installed if `uv sync` was already run.
</Note>
- [`bandit`](https://pypi.org/project/bandit/) for code security review.
- [`safety`](https://pypi.org/project/safety/) and [`dependabot`](https://github.com/features/security) for dependencies.
- [`osv-scanner`](https://github.com/google/osv-scanner) and [`dependabot`](https://github.com/features/security) for dependencies.
- [`hadolint`](https://github.com/hadolint/hadolint) and [`dockle`](https://github.com/goodwithtech/dockle) for container security.
- [`Snyk`](https://docs.snyk.io/integrations/snyk-container-integrations/container-security-with-docker-hub-integration) for container security in Docker Hub.
- [`clair`](https://github.com/quay/clair) for container security in Amazon ECR.
@@ -183,7 +176,7 @@ These resources help ensure that AI-assisted contributions maintain consistency
All dependencies are listed in the `pyproject.toml` file.
The SDK keeps direct dependencies pinned to exact versions, while `poetry.lock` records the full resolved dependency tree and the artifact hashes for every package. Use `poetry install` from the lock file instead of ad-hoc `pip` installs when you need a reproducible environment.
The SDK keeps direct dependencies pinned to exact versions, while `uv.lock` records the full resolved dependency tree and the artifact hashes for every package. Use `uv sync` from the lock file instead of ad-hoc `pip` installs when you need a reproducible environment.
For proper code documentation, refer to the following and follow the code documentation practices presented there: [Google Python Style Guide - Comments and Docstrings](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings).
@@ -209,8 +202,8 @@ prowler/
├── contrib/ # Community-contributed scripts or modules
├── kubernetes/ # Kubernetes deployment files
├── .github/ # GitHub-related files (workflows, issue templates, etc.)
├── pyproject.toml # Python project configuration (Poetry)
├── poetry.lock # Poetry lock file
├── pyproject.toml # Python project configuration (uv)
├── uv.lock # uv lock file
├── README.md # Project overview and getting started
├── Makefile # Common development commands
├── Dockerfile # SDK Docker container
+6 -4
View File
@@ -1277,10 +1277,12 @@ Dependencies ensure that your provider's required libraries are available when P
**File:** `pyproject.toml`
```toml
[tool.poetry.dependencies]
python = ">=3.10,<3.13"
# ... other dependencies
your-sdk-library = "^1.0.0" # Add your SDK dependency
[project]
requires-python = ">=3.10,<3.13"
dependencies = [
# ... other dependencies
"your-sdk-library>=1.0.0,<2.0.0", # Add your SDK dependency
]
```
#### Step 18: Create Tests
@@ -228,7 +228,7 @@ Each requirement links to the Prowler checks that, together, produce a PASS or F
To discover available checks, run:
```bash
poetry run python prowler-cli.py <provider> --list-checks
uv run python prowler-cli.py <provider> --list-checks
```
## Supporting Multiple Providers
@@ -295,7 +295,7 @@ Follow the steps below before opening a pull request.
### 1. Run the Compliance Model Validator
```bash
poetry run python prowler-cli.py <provider> --list-compliance
uv run python prowler-cli.py <provider> --list-compliance
```
The framework must appear in the output. A validation error indicates a schema mismatch between the JSON file and the attribute model.
@@ -303,7 +303,7 @@ The framework must appear in the output. A validation error indicates a schema m
### 2. Run a Scan Filtered by the Framework
```bash
poetry run python prowler-cli.py <provider> \
uv run python prowler-cli.py <provider> \
--compliance <framework>_<version>_<provider> \
--log-level ERROR
```
@@ -336,7 +336,7 @@ Compliance contributions require two layers of tests.
Run the suite with:
```bash
poetry run pytest -n auto tests/lib/check/universal_compliance_models_test.py \
uv run pytest -n auto tests/lib/check/universal_compliance_models_test.py \
tests/lib/outputs/compliance/
```
@@ -348,8 +348,8 @@ Before opening the pull request:
1. Run the complete QA pipeline:
```bash
poetry run pre-commit run --all-files
poetry run pytest -n auto
uv run pre-commit run --all-files
uv run pytest -n auto
```
2. Add a changelog entry under the `### 🚀 Added` section of `prowler/CHANGELOG.md`, describing the new framework and the providers it covers.
3. Follow the [Pull Request Template](https://github.com/prowler-cloud/prowler/blob/master/.github/pull_request_template.md) and set the PR title using Conventional Commits, for example `feat(compliance): add My Framework 1.0 for AWS`.
+1 -1
View File
@@ -23,7 +23,7 @@ Within this folder the following files are also to be created:
- `<new_service_name>_service.py` Contains all the logic and API calls of the service.
- `<new_service_name>_client_.py` Contains the initialization of the freshly created service's class so that the checks can use it.
Once the files are create, you can check that the service has been created by running the following command: `poetry run python prowler-cli.py <provider> --list-services | grep <new_service_name>`.
Once the files are create, you can check that the service has been created by running the following command: `uv run python prowler-cli.py <provider> --list-services | grep <new_service_name>`.
## Service Structure and Initialisation
+2 -1
View File
@@ -353,7 +353,8 @@
"group": "Cookbooks",
"pages": [
"user-guide/cookbooks/kubernetes-in-cluster",
"user-guide/cookbooks/cicd-pipeline"
"user-guide/cookbooks/cicd-pipeline",
"user-guide/cookbooks/powerbi-cis-benchmarks"
]
}
]
@@ -37,7 +37,7 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai
_Requirements_:
- `git` installed.
- `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation).
- `uv` installed: [uv installation](https://docs.astral.sh/uv/getting-started/installation/).
- `pnpm` installed through [Corepack](https://pnpm.io/installation#using-corepack) or the standalone [pnpm installation](https://pnpm.io/installation).
- `Docker Compose` installed: https://docs.docker.com/compose/install/.
@@ -49,8 +49,8 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai
```bash
git clone https://github.com/prowler-cloud/prowler \
cd prowler/api \
poetry install \
eval $(poetry env activate) \
uv sync \
source .venv/bin/activate \
set -a \
source .env \
docker compose up postgres valkey -d \
@@ -59,11 +59,6 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai
gunicorn -c config/guniconf.py config.wsgi:application
```
<Warning>
Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment. In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
</Warning>
> Now, you can access the API documentation at http://localhost:8080/api/v1/docs.
_Commands to run the API Worker_:
@@ -71,8 +66,8 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai
```bash
git clone https://github.com/prowler-cloud/prowler \
cd prowler/api \
poetry install \
eval $(poetry env activate) \
uv sync \
source .venv/bin/activate \
set -a \
source .env \
cd src/backend \
@@ -84,8 +79,8 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai
```bash
git clone https://github.com/prowler-cloud/prowler \
cd prowler/api \
poetry install \
eval $(poetry env activate) \
uv sync \
source .venv/bin/activate \
set -a \
source .env \
cd src/backend \
@@ -68,7 +68,7 @@ To install Prowler as a Python package, use `Python >= 3.10, <= 3.12`. Prowler i
_Requirements for Developers_:
* `git`
* `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation).
* `uv` installed: [uv installation](https://docs.astral.sh/uv/getting-started/installation/).
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
@@ -76,8 +76,8 @@ To install Prowler as a Python package, use `Python >= 3.10, <= 3.12`. Prowler i
```bash
git clone https://github.com/prowler-cloud/prowler
cd prowler
poetry install
poetry run python prowler-cli.py -v
uv sync
uv run python prowler-cli.py -v
```
<Note>
Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

+5 -4
View File
@@ -39,10 +39,11 @@ Dependencies are continuously monitored for known vulnerabilities with timely up
### Dependency Vulnerability Scanning
- **Safety:** Scans Python dependencies against known vulnerability databases
- Runs on every commit via pre-commit hooks
- Integrated into CI/CD for SDK and API
- Configured with selective ignores for tracked exceptions
- **osv-scanner:** Scans lockfiles against the [OSV.dev](https://osv.dev) vulnerability database
- Runs in CI on every pull request and push for SDK, API, and UI
- Fails the build on `HIGH`, `CRITICAL`, and `UNKNOWN` severity findings
- Posts a per-lockfile report as a PR comment
- Per-vulnerability ignores (with reason and expiry) live in `osv-scanner.toml` at the repo root
- **Trivy:** Multi-purpose scanner for containers and dependencies
- Scans all container images (UI, API, SDK, MCP Server)
- Checks for vulnerabilities in OS packages and application dependencies
@@ -0,0 +1,168 @@
---
title: "Visualize Multi-Cloud CIS Benchmarks With Power BI"
description: "Ingest Prowler compliance CSV exports into a ready-made Microsoft Power BI template that surfaces CIS Benchmark posture across AWS, Azure, Google Cloud, and Kubernetes."
---
The Multi-Cloud CIS Benchmarks Power BI template turns Prowler compliance CSV exports into an interactive dashboard. The template ingests scan results from Prowler CLI or Prowler Cloud and renders cross-provider CIS Benchmark coverage, profile-level breakdowns, regional drill-downs, and time-series trends. Center for Internet Security (CIS) Benchmarks are industry-standard configuration baselines maintained by CIS.
The template and its source files live in the Prowler repository under [`contrib/PowerBI/Multicloud CIS Benchmarks`](https://github.com/prowler-cloud/prowler/tree/master/contrib/PowerBI/Multicloud%20CIS%20Benchmarks).
<img src="/images/powerbi/report-cover.png" alt="Multi-Cloud CIS Benchmarks Power BI report cover showing aggregated compliance posture across providers" width="900" />
## Prerequisites
The setup requires the following components:
* **Microsoft Power BI Desktop:** free download from Microsoft.
* **Prowler compliance CSV exports:** produced by Prowler CLI or downloaded from Prowler Cloud or Prowler App.
* **Local directory:** holds the CSV exports that the template ingests at load time.
## Supported CIS Benchmarks
The template ships with predefined mappings for the following CIS Benchmark versions. Exports must match these versions for the dashboard to populate correctly:
| Compliance Framework | Version |
| ---------------------------------------------- | -------- |
| CIS Amazon Web Services Foundations Benchmark | v6.0 |
| CIS Microsoft Azure Foundations Benchmark | v5.0 |
| CIS Google Cloud Platform Foundation Benchmark | v4.0 |
| CIS Kubernetes Benchmark | v1.12.0 |
<Warning>
Other CIS Benchmark versions are not recognized by the template. Confirm the framework version before running the scan or downloading the export.
</Warning>
## Setup
### Step 1: Install Microsoft Power BI Desktop
Download and install Microsoft Power BI Desktop from the official Microsoft site. The template is opened with this application.
### Step 2: Generate Compliance CSV Exports
Compliance CSV exports can be generated through Prowler CLI or downloaded from Prowler Cloud and Prowler App.
#### Option A: Prowler CLI
Run a scan with the `--compliance` flag pointing to the appropriate CIS framework, for example:
```sh
prowler aws --compliance cis_6.0_aws
prowler azure --compliance cis_5.0_azure
prowler gcp --compliance cis_4.0_gcp
prowler kubernetes --compliance cis_1.12_kubernetes
```
The compliance CSV exports are written to `output/compliance/` by default.
#### Option B: Prowler Cloud or Prowler App
Open the Compliance section, select the desired CIS Benchmark, and download the CSV export.
<img src="/images/powerbi/download-compliance-scan.png" alt="Compliance section in Prowler Cloud showing the CSV download option for a CIS Benchmark scan" width="900" />
### Step 3: Create a Local Directory for the Exports
Place every CSV export in a single local directory. The template parses filenames to detect the provider, so filenames must keep the provider keyword (`aws`, `azure`, `gcp`, or `kubernetes`).
<Note>
Time-series visualizations such as "Compliance Percent Over Time" require multiple scans from different dates in the same directory.
</Note>
### Step 4: Open the Power BI Template
Download the template file [`Prowler Multicloud CIS Benchmarks.pbit`](https://github.com/prowler-cloud/prowler/raw/master/contrib/PowerBI/Multicloud%20CIS%20Benchmarks/Prowler%20Multicloud%20CIS%20Benchmarks.pbit) and open it. Power BI Desktop prompts for the full filepath to the directory created in step 3.
### Step 5: Provide the Directory Filepath
Enter the absolute filepath without quotation marks. The Windows "copy as path" feature wraps the path in quotation marks automatically; remove them before submitting.
### Step 6: Save the Report as a `.pbix` File
Once the filepath is submitted, the template ingests the CSV exports and renders the report. Save the populated report as a `.pbix` file for future use. Re-running the `.pbit` template generates a fresh report against an updated directory.
## Validation
To confirm the CSV exports were ingested correctly, open the "Configuration" tab inside the report.
<img src="/images/powerbi/validation.png" alt="Configuration tab in the Power BI report displaying loaded CIS Benchmarks, the Prowler CSV folder path, and the list of ingested exports" width="900" />
The "Configuration" tab exposes three tables:
* **Loaded CIS Benchmarks:** lists the benchmarks and versions supported by the template. This table is defined by the template itself and is not editable. All benchmarks remain listed regardless of which provider exports were supplied.
* **Prowler CSV Folder:** displays the absolute path provided during template load.
* **Loaded Prowler Exports:** lists every CSV file detected in the directory. A green checkmark identifies the file used as the latest assessment for each provider and benchmark combination.
## Report Sections
The report is organized into three navigable pages:
| Report Page | Purpose |
| ----------- | ------------------------------------------------------------------------------------ |
| Overview | Aggregates CIS Benchmark posture across AWS, Azure, Google Cloud, and Kubernetes. |
| Benchmark | Focuses on a single CIS Benchmark with profile-level and regional filters. |
| Requirement | Drill-through page that surfaces details for a single benchmark requirement. |
### Overview Page
The Overview page summarizes CIS Benchmark posture across every supported provider.
<img src="/images/powerbi/overview-page.png" alt="Overview page in the Power BI report aggregating CIS Benchmark posture across AWS, Azure, Google Cloud, and Kubernetes" width="900" />
The Overview page contains the following components:
| Component | Description |
| ---------------------------------------- | ---------------------------------------------------------------------------- |
| CIS Benchmark Overview | Table listing benchmark name, version, and overall compliance percentage. |
| Provider by Requirement Status | Bar chart breaking down requirements by status and provider. |
| Compliance Percent Heatmap | Heatmap of compliance percentage by benchmark and profile level. |
| Profile Level by Requirement Status | Bar chart breaking down requirements by status and profile level. |
| Compliance Percent Over Time by Provider | Line chart tracking overall compliance percentage over time by provider. |
### Benchmark Page
The Benchmark page focuses on a single CIS Benchmark. The benchmark, profile level, and region can be selected through dropdown filters.
<img src="/images/powerbi/benchmark-page.png" alt="Benchmark page in the Power BI report showing region heatmap, section breakdown, time-series trend, and the requirements table" width="900" />
The Benchmark page contains the following components:
| Component | Description |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Compliance Percent Heatmap | Heatmap of compliance percentage by region and profile level. |
| Benchmark Section by Requirement Status | Bar chart of requirements grouped by benchmark section and status. |
| Compliance Percent Over Time by Region | Line chart tracking compliance percentage over time by region. |
| Benchmark Requirements | Table listing requirement section, requirement number, requirement title, number of resources tested, status, and failing checks. |
### Requirement Page
The Requirement page is a drill-through view that exposes the full context of a single requirement. To populate the page, right-click a row in the "Benchmark Requirements" table on the Benchmark page and select "Drill through" > "Requirement".
<img src="/images/powerbi/requirement-page.png" alt="Requirement drill-through page in the Power BI report showing rationale, remediation, regional breakdown, and the resource-level check results" width="900" />
The Requirement page contains the following components:
| Component | Description |
| ------------------------------------------ | -------------------------------------------------------------------------------------------- |
| Title | Requirement title. |
| Rationale | Rationale for the requirement. |
| Remediation | Remediation guidance for the requirement. |
| Region by Check Status | Bar chart of Prowler check results grouped by region and status. |
| Resource Checks for Benchmark Requirements | Table listing resource ID, resource name, status, description, and the underlying Prowler check. |
## Walkthrough Video
A full walkthrough is available on YouTube:
[![Multi-Cloud CIS Benchmarks Power BI walkthrough video thumbnail](/images/powerbi/walkthrough-video-thumb.png)](https://www.youtube.com/watch?v=lfKFkTqBxjU)
## Related Resources
<CardGroup cols={2}>
<Card title="Compliance Frameworks" icon="shield-check" href="/user-guide/compliance/tutorials/compliance">
Review the Compliance workflow across Prowler Cloud, Prowler App, and Prowler CLI.
</Card>
<Card title="Prowler Dashboard" icon="chart-line" href="/user-guide/cli/tutorials/dashboard">
Explore the built-in local dashboard for Prowler CSV exports.
</Card>
</CardGroup>
+5 -13
View File
@@ -27,7 +27,7 @@ To download results from AWS CloudShell:
## Cloning Prowler from GitHub
Due to the limited storage in AWS CloudShell's home directory, installing Poetry dependencies for running Prowler from GitHub can be problematic.
Due to the limited storage in AWS CloudShell's home directory, installing uv dependencies for running Prowler from GitHub can be problematic.
The following workaround ensures successful installation:
@@ -37,17 +37,9 @@ adduser prowler
su prowler
git clone https://github.com/prowler-cloud/prowler.git
cd prowler
pip install poetry
mkdir /tmp/poetry
poetry config cache-dir /tmp/poetry
eval $(poetry env activate)
poetry install
pip install uv
mkdir /tmp/uv-cache
UV_CACHE_DIR=/tmp/uv-cache uv sync
source .venv/bin/activate
python prowler-cli.py -v
```
<Warning>
Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
If your Poetry version is below v2.0.0, continue using `poetry shell` to activate your environment. For further guidance, refer to the Poetry Environment Activation Guide https://python-poetry.org/docs/managing-environments/#activating-the-environment.
</Warning>
@@ -44,6 +44,15 @@ User API Tokens are the recommended authentication method because they:
Create a **User API Token**, not an Account API Token. User API Tokens are created from the profile settings and offer finer permission control.
</Note>
**Quick Setup:** Use these pre-configured links to open the Cloudflare Dashboard with the required permissions already selected:
- [Create User API Token](https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&accountId=%2A&zoneId=all&name=Prowler%20Security%20Scanner) — creates a **User API Token** (recommended). Opens the **Create Custom Token** form prefilled with the four required read-only scopes (`Account Settings`, `Zone`, `Zone Settings`, `DNS`) and the name `Prowler Security Scanner`. Adjust **Account Resources** and **Zone Resources** to match the accounts and zones you want to scan, then click **Create Token**.
- [Create Account-Owned API Token](https://dash.cloudflare.com/?to=/:account/api-tokens&permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&name=Prowler%20Security%20Scanner) — creates an [account-owned token](https://developers.cloudflare.com/fundamentals/api/how-to/account-owned-token-template/) instead. Use this for automation or CI/CD where the token should not depend on a specific user account remaining active. Requires the **Super Administrator** or **Administrator** role on the account.
<Note>
Template URLs only pre-fill the token creation form. Review the permissions, configure resources, and click **Create Token** to complete the process.
</Note>
### Step 1: Create a User API Token
1. Log into the [Cloudflare Dashboard](https://dash.cloudflare.com).
@@ -14,6 +14,15 @@ Set up authentication for Cloudflare with the [Cloudflare Authentication](/user-
- Grant the required read-only permissions (`Account Settings:Read`, `Zone:Read`, `Zone Settings:Read`, `DNS:Read`)
- Identify the Cloudflare Account ID to use as the provider identifier
<Note>
**Quick Setup:** Use these pre-configured links to create a token with the required permissions already selected:
- [Create User API Token](https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&accountId=%2A&zoneId=all&name=Prowler%20Security%20Scanner) — creates a User API Token (recommended).
- [Create Account-Owned API Token](https://dash.cloudflare.com/?to=/:account/api-tokens&permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&name=Prowler%20Security%20Scanner) — creates an [account-owned token](https://developers.cloudflare.com/fundamentals/api/how-to/account-owned-token-template/), better suited for automation and CI/CD.
Both links open the Cloudflare Dashboard with the four required read-only scopes (`Account Settings`, `Zone`, `Zone Settings`, `DNS`) and the name `Prowler Security Scanner` prefilled. See [Cloudflare Authentication](/user-guide/providers/cloudflare/authentication#api-token-recommended) for the equivalent manual steps.
</Note>
<CardGroup cols={2}>
<Card title="Prowler Cloud" icon="cloud" href="#prowler-cloud">
Onboard Cloudflare using Prowler Cloud
@@ -153,8 +153,8 @@ Before running Prowler CLI for GitHub, ensure you have:
# Install via pip
pip install prowler
# Or via poetry
poetry install
# Or via uv (from the cloned repo)
uv sync
```
2. **Authentication Credentials**
@@ -18,7 +18,7 @@ Prowler requests the following read-only OAuth 2.0 scopes:
| `https://www.googleapis.com/auth/admin.directory.domain.readonly` | Read access to domain information |
| `https://www.googleapis.com/auth/admin.directory.customer.readonly` | Read access to customer information (Customer ID) |
| `https://www.googleapis.com/auth/admin.directory.orgunit.readonly` | Read access to organizational unit hierarchy (identifies the root OU for policy filtering) |
| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar service checks) |
| `https://www.googleapis.com/auth/cloud-identity.policies.readonly` | Read access to domain-level application policies (required for Calendar, Gmail, Chat, and Drive service checks) |
| `https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly` | Read access to admin roles and role assignments |
<Warning>
@@ -40,7 +40,7 @@ In the [Google Cloud Console](https://console.cloud.google.com), select the targ
| API | Required For |
|-----|--------------|
| **Admin SDK API** | Directory service checks (users, roles, domains) |
| **Cloud Identity API** | Calendar service checks (domain-level sharing and invitation policies) |
| **Cloud Identity API** | Calendar, Gmail, Chat, and Drive service checks (domain-level application policies) |
For each API:
@@ -49,7 +49,7 @@ For each API:
3. Click **Enable**
<Note>
Both APIs must be enabled in the same GCP project that hosts the Service Account. Calendar checks will return no findings if the Cloud Identity API is not enabled.
Both APIs must be enabled in the same GCP project that hosts the Service Account. Calendar, Gmail, Chat, and Drive checks will return no findings if the Cloud Identity API is not enabled.
</Note>
### Step 3: Create a Service Account
@@ -176,9 +176,9 @@ If Prowler connects but returns empty results or permission errors for specific
- Verify all scopes are authorized in the Admin Console
- Ensure the delegated user is an active super administrator
### Calendar Checks Return No Findings
### Policy API Checks Return No Findings
If the Directory checks run successfully but the Calendar checks (e.g., `calendar_external_sharing_primary_calendar`) return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify:
If the Directory checks run successfully but the Calendar, Gmail, Chat, or Drive checks return no findings, the Cloud Identity Policy API is not reachable for this Service Account. Verify:
- The **Cloud Identity API** is enabled in the GCP project hosting the Service Account (Step 2)
- The scope `https://www.googleapis.com/auth/cloud-identity.policies.readonly` is included in the Domain-Wide Delegation OAuth scopes list in the Admin Console (Step 5)
@@ -46,7 +46,7 @@ Before you begin, ensure you have:
```bash
pip install prowler
# or for development:
poetry install
uv sync
```
2. **OCI Python SDK** (automatically installed with Prowler):
@@ -133,7 +133,7 @@ export OKTA_PRIVATE_KEY="$(cat /secure/path/to/prowler-okta.pem)"
# Optional — defaults to "okta.policies.read"
export OKTA_SCOPES="okta.policies.read"
poetry run python prowler-cli.py okta
uv run python prowler-cli.py okta
```
### Non-Secret CLI Flags
@@ -149,7 +149,7 @@ Non-secret values are also available as CLI flags for ergonomic overrides:
Run a single check directly:
```bash
poetry run python prowler-cli.py okta --check signon_global_session_idle_timeout_15min
uv run python prowler-cli.py okta --check signon_global_session_idle_timeout_15min
```
## Troubleshooting
-115
View File
@@ -1,115 +0,0 @@
# Prowler Multicloud CIS Benchmarks PowerBI Template
![Prowler Report](https://github.com/user-attachments/assets/560f7f83-1616-4836-811a-16963223c72f)
## Getting Started
1. Install Microsoft PowerBI Desktop
This report requires the Microsoft PowerBI Desktop software which can be downloaded for free from Microsoft.
2. Run compliance scans in Prowler
The report uses compliance csv outputs from Prowler. Compliance scans be run using either [Prowler CLI](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-cli) or [Prowler Cloud/App](https://cloud.prowler.com/sign-in)
1. Prowler CLI -&gt; Run a Prowler scan using the --compliance option
2. Prowler Cloud/App -&gt; Navigate to the compliance section to download csv outputs
![Download Compliance Scan](https://github.com/user-attachments/assets/42c11a60-8ce8-4c60-a663-2371199c052b)
The template supports the following CIS Benchmarks only:
| Compliance Framework | Version |
| ---------------------------------------------- | ------- |
| CIS Amazon Web Services Foundations Benchmark | v4.0.1 |
| CIS Google Cloud Platform Foundation Benchmark | v3.0.0 |
| CIS Microsoft Azure Foundations Benchmark | v3.0.0 |
| CIS Kubernetes Benchmark | v1.10.0 |
Ensure you run or download the correct benchmark versions.
3. Create a local directory to store Prowler csvoutputs
Once downloaded, place your csv outputs in a directory on your local machine. If you rename the files, they must maintain the provider in the filename.
To use time-series capabilities such as "compliance percent over time" you'll need scans from multiple dates.
4. Download and run the PowerBI template file (.pbit)
Running the .pbit file will open PowerBI Desktop and prompt you for the full filepath to the local directory
5. Enter the full filepath to the directory created in step 3
Provide the full filepath from the root directory.
Ensure that the filepath is not wrapped in quotation marks (""). If you use Window's "copy as path" feature, it will automatically include quotation marks.
6. Save the report as a PowerBI file (.pbix)
Once the filepath is entered, the template will automatically ingest and populate the report. You can then save this file as a new PowerBI report. If you'd like to generate another report, simply re-run the template file (.pbit) from step 4.
## Validation
After setting up your dashboard, you may want to validate the Prowler csv files were ingested correctly. To do this, navigate to the "Configuration" tab.
The "loaded CIS Benchmarks" table shows the supported benchmarks and versions. This is defined by the template file and not editable by the user. All benchmarks will be loaded regardless of which providers you provided csv outputs for.
The "Prowler CSV Folder" shows the path to the local directory you provided.
The "Loaded Prowler Exports" table shows the ingested csv files from the local directory. It will mark files that are treated as the latest assessment with a green checkmark.
![Prowler Validation](https://github.com/user-attachments/assets/a543ca9b-6cbe-4ad1-b32a-d4ac2163d447)
## Report Sections
The PowerBI Report is broken into three main report pages
| Report Page | Description |
| ----------- | ----------------------------------------------------------------------------------- |
| Overview | Provides general CIS Benchmark overview across both AWS, Azure, GCP, and Kubernetes |
| Benchmark | Provides overview of a single CIS Benchmark |
| Requirement | Drill-through page to view details of a single requirement |
### Overview Page
The overview page is a general CIS Benchmark overview across both AWS, Azure, GCP, and Kubernetes.
![image](https://github.com/user-attachments/assets/94164fa9-36a4-4bb9-890d-e9a9a63a3e7d)
The page has the following components:
| Component | Description |
| ---------------------------------------- | ------------------------------------------------------------------------ |
| CIS Benchmark Overview | Table with benchmark name, Version, and overall compliance percentage |
| Provider by Requirement Status | Bar chart showing benchmark requirements by status by provider |
| Compliance Percent Heatmap | Heatmap showing compliance percent by benchmark and profile level |
| Profile level by Requirement Status | Bar chart showing requirements by status and profile level |
| Compliance Percent Over Time by Provider | Line chart showing overall compliance perecentage over time by provider. |
### Benchmark Page
The benchmark page provides an overview of a single CIS Benchmark. You can select the benchmark from the dropdown as well as scope down to specific profile levels or regions.
![image](https://github.com/user-attachments/assets/34498ee8-317b-4b81-b241-c561451d8def)
The page has the following components:
| Component | Description |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Compliance Percent Heatmap | Heatmap showing compliance percent by region and profile level |
| Benchmark Section by Requirement Status | Bar chart showing benchmark requirements by bennchmark section and status |
| Compliance percent Over Time by Region | Line chart showing overall compliance percentage over time by region |
| Benchmark Requirements | Table showing requirement section, requirement number, reuqirement title, number of resources tested, status, and number of failing checks |
### Requirement Page
The requirement page is a drill-through page to view details of a single requirement. To populate the requirement page right click on a requiement from the "Benchmark Requirements" table on the benchmark page and select "Drill through" -&gt; "Requirement".
![image](https://github.com/user-attachments/assets/5c9172d9-56fe-4514-b341-7e708863fad6)
The requirement page has the following components:
| Component | Description |
| ------------------------------------------ | --------------------------------------------------------------------------------- |
| Title | Title of the requirement |
| Rationale | Rationale of the requirement |
| Remediation | Remedation guidance for the requirement |
| Region by Check Status | Bar chart showing Prowler checks by region and status |
| Resource Checks for Benchmark Requirements | Table showing Resource ID, Resource Name, Status, Description, and Prowler Checkl |
## Walkthrough Video
[![image](https://github.com/user-attachments/assets/866642c6-43ac-4aac-83d3-bb625002da0b)](https://www.youtube.com/watch?v=lfKFkTqBxjU)
Generated
-6888
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -85,7 +85,7 @@ class {check_name}(Check):
## TECH STACK
Python 3.10+ | Poetry 2.3+ | pytest | moto (AWS mocking) | Pre-commit hooks (black, flake8, pylint, bandit)
Python 3.10+ | uv | pytest | moto (AWS mocking) | Pre-commit hooks (black, flake8, pylint, bandit)
---
@@ -112,20 +112,20 @@ prowler/
```bash
# Setup
poetry install --with dev
poetry run pre-commit install
uv sync
uv run pre-commit install
# Run Prowler
poetry run python prowler-cli.py {provider}
poetry run python prowler-cli.py {provider} --check {check_name}
poetry run python prowler-cli.py {provider} --list-checks
uv run python prowler-cli.py {provider}
uv run python prowler-cli.py {provider} --check {check_name}
uv run python prowler-cli.py {provider} --list-checks
# Testing
poetry run pytest -n auto -vvv tests/
poetry run pytest tests/providers/{provider}/services/{service}/ -v
uv run pytest -n auto -vvv tests/
uv run pytest tests/providers/{provider}/services/{service}/ -v
# Code Quality
poetry run pre-commit run --all-files
uv run pre-commit run --all-files
```
---
@@ -145,8 +145,8 @@ poetry run pre-commit run --all-files
## QA CHECKLIST
- [ ] `poetry run pytest` passes
- [ ] `poetry run pre-commit run --all-files` passes
- [ ] `uv run pytest` passes
- [ ] `uv run pre-commit run --all-files` passes
- [ ] Check metadata JSON is valid
- [ ] Tests cover PASS, FAIL, and empty resource scenarios
- [ ] Docstrings follow Google style
+10
View File
@@ -6,16 +6,25 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🚀 Added
- 6 Chat file sharing, external messaging, spaces, and apps access checks for Google Workspace provider using the Cloud Identity Policy API [(#11126)](https://github.com/prowler-cloud/prowler/pull/11126)
- `entra_service_principal_no_secrets_for_permanent_tier0_roles` check for M365 provider [(#10788)](https://github.com/prowler-cloud/prowler/pull/10788)
- `iam_user_access_not_stale_to_sagemaker` check for AWS provider with configurable `max_unused_sagemaker_access_days` (default 90) [(#11000)](https://github.com/prowler-cloud/prowler/pull/11000)
- `cloudtrail_bedrock_logging_enabled` check for AWS provider [(#10858)](https://github.com/prowler-cloud/prowler/pull/10858)
- Okta provider with OAuth 2.0 authentication and `signon_global_session_idle_timeout_15min` check [(#11079)](https://github.com/prowler-cloud/prowler/pull/11079)
- `sagemaker_domain_sso_configured` check for AWS provider [(#11094)](https://github.com/prowler-cloud/prowler/pull/11094)
### 🔄 Changed
- `entra_emergency_access_exclusion` check for M365 provider now scopes the exclusion requirement to enabled Conditional Access policies with a `Block` grant control instead of every enabled policy, focusing on the lockout-relevant policy set [(#10849)](https://github.com/prowler-cloud/prowler/pull/10849)
- AWS IAM customer-managed policy checks no longer emit `FAIL` on unattached policies unless `--scan-unused-services` is enabled [(#11150)](https://github.com/prowler-cloud/prowler/pull/11150)
### 🐞 Fixed
- Google Workspace Directory checks sharing a single resource row, causing the service field to be overwritten by the last check executed [(#11176)](https://github.com/prowler-cloud/prowler/pull/11176)
- Google Workspace Calendar and Drive services sharing a single resource row, causing the service field to be overwritten by the last check executed [(#11161)](https://github.com/prowler-cloud/prowler/pull/11161)
- `zone_waf_enabled` check for Cloudflare provider now appends a plan-aware hint to the FAIL `status_extended`: a possible-false-positive note on paid plans (Pro, Business, Enterprise) where the legacy `waf` zone setting can read `off` even though WAF managed rulesets are deployed via the dashboard, and a "not available on the Cloudflare Free plan" note on Free zones [(#9896)](https://github.com/prowler-cloud/prowler/pull/9896)
- Google Workspace Gmail checks sharing a single resource row, causing the service field to be overwritten by the last check executed [(#11169)](https://github.com/prowler-cloud/prowler/pull/11169)
---
## [5.26.2] (Prowler UNRELEASED)
@@ -23,6 +32,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🐞 Fixed
- `entra_users_mfa_capable` and `entra_break_glass_account_fido2_security_key_registered` report a preventive FAIL per affected user (with the missing permission named) when the M365 service principal lacks `AuditLog.Read.All`, instead of mass false positives [(#10907)](https://github.com/prowler-cloud/prowler/pull/10907)
- Update duplicated GCP CIS requirements IDs [(#11180)](https://github.com/prowler-cloud/prowler/pull/11180)
---
+2 -2
View File
@@ -914,7 +914,7 @@
]
},
{
"Id": "3.1",
"Id": "3.10",
"Description": "Use Identity Aware Proxy (IAP) to Ensure Only Traffic From Google IP Addresses are 'Allowed'",
"Checks": [],
"Attributes": [
@@ -1132,7 +1132,7 @@
]
},
{
"Id": "4.1",
"Id": "4.10",
"Description": "Ensure That App Engine Applications Enforce HTTPS Connections",
"Checks": [],
"Attributes": [
@@ -1084,7 +1084,9 @@
{
"Id": "3.1.4.1.1",
"Description": "Ensure external filesharing in Google Chat and Hangouts is disabled",
"Checks": [],
"Checks": [
"chat_external_file_sharing_disabled"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1105,7 +1107,9 @@
{
"Id": "3.1.4.1.2",
"Description": "Ensure internal filesharing in Google Chat and Hangouts is disabled",
"Checks": [],
"Checks": [
"chat_internal_file_sharing_disabled"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1126,7 +1130,9 @@
{
"Id": "3.1.4.2.1",
"Description": "Ensure Google Chat externally is restricted to allowed domains",
"Checks": [],
"Checks": [
"chat_external_messaging_restricted"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1147,7 +1153,9 @@
{
"Id": "3.1.4.3.1",
"Description": "Ensure external spaces in Google Chat and Hangouts are restricted",
"Checks": [],
"Checks": [
"chat_external_spaces_restricted"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1168,7 +1176,9 @@
{
"Id": "3.1.4.4.1",
"Description": "Ensure allow users to install Chat apps is disabled",
"Checks": [],
"Checks": [
"chat_apps_installation_disabled"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1189,7 +1199,9 @@
{
"Id": "3.1.4.4.2",
"Description": "Ensure allow users to add and use incoming webhooks is disabled",
"Checks": [],
"Checks": [
"chat_incoming_webhooks_disabled"
],
"Attributes": [
{
"Section": "3 Apps",
@@ -1466,7 +1466,9 @@
{
"Id": "GWS.CHAT.2.1",
"Description": "External file sharing SHALL be disabled to protect sensitive information from unauthorized or accidental sharing",
"Checks": [],
"Checks": [
"chat_external_file_sharing_disabled"
],
"Attributes": [
{
"Section": "Chat",
@@ -1492,7 +1494,9 @@
{
"Id": "GWS.CHAT.4.1",
"Description": "External chat messaging SHALL be restricted to allowlisted domains only",
"Checks": [],
"Checks": [
"chat_external_messaging_restricted"
],
"Attributes": [
{
"Section": "Chat",
@@ -0,0 +1,39 @@
{
"Provider": "aws",
"CheckID": "sagemaker_domain_sso_configured",
"CheckTitle": "SageMaker domains use SSO authentication instead of IAM mode",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "sagemaker",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Other",
"ResourceGroup": "ai_ml",
"Description": "**SageMaker Domain** configured with **IAM Identity Center (SSO) authentication**. The check validates that each SageMaker Domain uses SSO mode (`AuthMode: SSO`) and is associated with an IAM Identity Center instance (`SingleSignOnManagedApplicationInstanceId` or `SingleSignOnApplicationArn` present), ensuring user access is centrally managed through AWS IAM Identity Center.",
"Risk": "IAM-mode domains create per-user IAM users or roles managed locally to SageMaker, drifting from the organization's identity provider and weakening lifecycle controls such as offboarding, MFA enforcement, and session policies. SSO-mode domains without an IAM Identity Center association leave authentication in an inconsistent state and bypass centralized access governance.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/sagemaker/latest/dg/onboard-sso-users.html"
],
"Remediation": {
"Code": {
"CLI": "aws sagemaker describe-domain --domain-id <domain_id> --query '{AuthMode:AuthMode,SingleSignOnManagedApplicationInstanceId:SingleSignOnManagedApplicationInstanceId,SingleSignOnApplicationArn:SingleSignOnApplicationArn}'",
"NativeIaC": "```yaml\n# CloudFormation: Create a SageMaker Domain with SSO authentication\nResources:\n SageMakerDomain:\n Type: AWS::SageMaker::Domain\n Properties:\n DomainName: <example_domain_name>\n AuthMode: SSO # Critical: enables IAM Identity Center authentication\n DefaultUserSettings:\n ExecutionRole: <example_role_arn>\n VpcId: <example_vpc_id>\n SubnetIds:\n - <example_subnet_id>\n```",
"Other": "SageMaker Domains cannot be switched from IAM to SSO mode after creation. To remediate, create a new Domain with AuthMode set to SSO and migrate user profiles.",
"Terraform": "```hcl\n# Terraform: Create a SageMaker Domain with SSO authentication\nresource \"aws_sagemaker_domain\" \"example\" {\n domain_name = \"<example_domain_name>\"\n auth_mode = \"SSO\" # Critical: enables IAM Identity Center authentication\n vpc_id = \"<example_vpc_id>\"\n subnet_ids = [\"<example_subnet_id>\"]\n\n default_user_settings {\n execution_role = \"<example_role_arn>\"\n }\n}\n```"
},
"Recommendation": {
"Text": "Configure SageMaker Domains with SSO authentication mode to anchor user access to AWS IAM Identity Center. This enforces centralized identity lifecycle management, MFA policies, and session controls. Domains created with IAM mode must be recreated with SSO mode since the auth mode cannot be changed after creation.",
"Url": "https://hub.prowler.com/check/sagemaker_domain_sso_configured"
}
},
"Categories": [
"identity-access",
"gen-ai"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,27 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.sagemaker.sagemaker_client import sagemaker_client
class sagemaker_domain_sso_configured(Check):
def execute(self):
findings = []
for domain in sagemaker_client.sagemaker_domains:
report = Check_Report_AWS(metadata=self.metadata(), resource=domain)
if domain.auth_mode == "SSO":
if (
domain.single_sign_on_managed_application_instance_id
or domain.single_sign_on_application_arn
):
report.status = "PASS"
report.status_extended = f"SageMaker domain {domain.name} is configured with SSO authentication and is associated with an IAM Identity Center instance."
else:
report.status = "FAIL"
report.status_extended = f"SageMaker domain {domain.name} is configured with SSO authentication but is not associated with an IAM Identity Center instance."
else:
report.status = "FAIL"
current_mode = domain.auth_mode if domain.auth_mode else "unknown"
report.status_extended = f"SageMaker domain {domain.name} is not configured with SSO authentication, current mode is {current_mode}."
findings.append(report)
return findings
@@ -15,6 +15,7 @@ class SageMaker(AWSService):
self.sagemaker_notebook_instances = []
self.sagemaker_models = []
self.sagemaker_training_jobs = []
self.sagemaker_domains = []
self.endpoint_configs = {}
# Retrieve resources concurrently
@@ -22,6 +23,7 @@ class SageMaker(AWSService):
self.__threading_call__(self._list_models)
self.__threading_call__(self._list_training_jobs)
self.__threading_call__(self._list_endpoint_configs)
self.__threading_call__(self._list_domains)
# Describe resources concurrently
self.__threading_call__(self._describe_model, self.sagemaker_models)
@@ -34,6 +36,7 @@ class SageMaker(AWSService):
self.__threading_call__(
self._describe_endpoint_config, list(self.endpoint_configs.values())
)
self.__threading_call__(self._describe_domain, self.sagemaker_domains)
# List tags concurrently for each resource collection
# This replaces the previous sequential sequential execution to improve performance
@@ -47,6 +50,7 @@ class SageMaker(AWSService):
self.__threading_call__(
self._list_tags_for_resource, list(self.endpoint_configs.values())
)
self.__threading_call__(self._list_tags_for_resource, self.sagemaker_domains)
def _list_notebook_instances(self, regional_client):
logger.info("SageMaker - listing notebook instances...")
@@ -218,6 +222,46 @@ class SageMaker(AWSService):
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _list_domains(self, regional_client):
logger.info("SageMaker - listing domains...")
try:
list_domains_paginator = regional_client.get_paginator("list_domains")
for page in list_domains_paginator.paginate():
for domain in page["Domains"]:
if not self.audit_resources or (
is_resource_filtered(domain["DomainArn"], self.audit_resources)
):
self.sagemaker_domains.append(
Domain(
domain_id=domain["DomainId"],
name=domain["DomainName"],
region=regional_client.region,
arn=domain["DomainArn"],
)
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _describe_domain(self, domain):
logger.info("SageMaker - describing domain...")
try:
regional_client = self.regional_clients[domain.region]
describe_domain = regional_client.describe_domain(DomainId=domain.domain_id)
if "AuthMode" in describe_domain:
domain.auth_mode = describe_domain["AuthMode"]
domain.single_sign_on_managed_application_instance_id = describe_domain.get(
"SingleSignOnManagedApplicationInstanceId"
)
domain.single_sign_on_application_arn = describe_domain.get(
"SingleSignOnApplicationArn"
)
except Exception as error:
logger.error(
f"{domain.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _list_endpoint_configs(self, regional_client):
logger.info("SageMaker - listing endpoint configs...")
try:
@@ -303,6 +347,17 @@ class ProductionVariant(BaseModel):
initial_instance_count: int
class Domain(BaseModel):
domain_id: str
name: str
region: str
arn: str
auth_mode: Optional[str] = None
single_sign_on_managed_application_instance_id: Optional[str] = None
single_sign_on_application_arn: Optional[str] = None
tags: Optional[list] = []
class EndpointConfig(BaseModel):
name: str
region: str
+35
View File
@@ -0,0 +1,35 @@
from typing import Optional
# Cloudflare returns the plan name in ``zone.plan.name`` (e.g. "Free Website",
# "Pro Website", "Business Website", "Enterprise Website"). Free plans do not
# expose WAF managed rulesets at all, while paid plans expose them but the
# legacy ``waf`` zone setting can lag behind the actual deployment state.
PAID_PLAN_KEYWORDS = ("pro", "business", "enterprise")
FREE_PLAN_KEYWORDS = ("free",)
def _plan_matches(plan: Optional[str], keywords: tuple[str, ...]) -> bool:
if not isinstance(plan, str):
return False
plan_lower = plan.lower()
return any(keyword in plan_lower for keyword in keywords)
def is_paid_plan(plan: Optional[str]) -> bool:
"""Return True when the Cloudflare zone plan is a paid tier."""
return _plan_matches(plan, PAID_PLAN_KEYWORDS)
def is_free_plan(plan: Optional[str]) -> bool:
"""Return True when the Cloudflare zone plan is the Free tier."""
return _plan_matches(plan, FREE_PLAN_KEYWORDS)
def paid_plan_suffix(plan: Optional[str], message: str) -> str:
"""Return an explanatory suffix only when the zone is on a paid plan."""
return f" {message}" if is_paid_plan(plan) else ""
def free_plan_suffix(plan: Optional[str], message: str) -> str:
"""Return an explanatory suffix only when the zone is on the Free plan."""
return f" {message}" if is_free_plan(plan) else ""
@@ -1,6 +1,19 @@
from prowler.lib.check.models import Check, CheckReportCloudflare
from prowler.providers.cloudflare.lib.plan import (
free_plan_suffix,
paid_plan_suffix,
)
from prowler.providers.cloudflare.services.zone.zone_client import zone_client
PAID_PLAN_FALSE_POSITIVE_HINT = (
"This may be a false positive if WAF managed rulesets are configured via "
"the Cloudflare dashboard; verify manually in Security > WAF."
)
FREE_PLAN_UNAVAILABLE_HINT = (
"This may be expected because the Web Application Firewall is not "
"available on the Cloudflare Free plan."
)
class zone_waf_enabled(Check):
"""Ensure that WAF is enabled for Cloudflare zones.
@@ -35,6 +48,16 @@ class zone_waf_enabled(Check):
report.status_extended = f"WAF is enabled for zone {zone.name}."
else:
report.status = "FAIL"
report.status_extended = f"WAF is not enabled for zone {zone.name}."
# Two plan-specific hints can be appended to the FAIL message:
# - Paid plans: the legacy ``waf`` zone setting can read ``off``
# while WAF managed rulesets are deployed via the dashboard,
# so the FAIL may be a false positive.
# - Free plans: WAF is not available at all, so the FAIL is
# expected and the suffix points that out.
report.status_extended = (
f"WAF is not enabled for zone {zone.name}."
f"{paid_plan_suffix(zone.plan, PAID_PLAN_FALSE_POSITIVE_HINT)}"
f"{free_plan_suffix(zone.plan, FREE_PLAN_UNAVAILABLE_HINT)}"
)
findings.append(report)
return findings
@@ -20,7 +20,10 @@ class calendar_external_invitations_warning(Check):
if calendar_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=calendar_client.provider.domain_resource,
resource=calendar_client.policies,
resource_id="calendarPolicies",
resource_name="Calendar Policies",
customer_id=calendar_client.provider.identity.customer_id,
)
warning_enabled = calendar_client.policies.external_invitations_warning
@@ -20,7 +20,10 @@ class calendar_external_sharing_primary_calendar(Check):
if calendar_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=calendar_client.provider.domain_resource,
resource=calendar_client.policies,
resource_id="calendarPolicies",
resource_name="Calendar Policies",
customer_id=calendar_client.provider.identity.customer_id,
)
sharing = calendar_client.policies.primary_calendar_external_sharing
@@ -20,7 +20,10 @@ class calendar_external_sharing_secondary_calendar(Check):
if calendar_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=calendar_client.provider.domain_resource,
resource=calendar_client.policies,
resource_id="calendarPolicies",
resource_name="Calendar Policies",
customer_id=calendar_client.provider.identity.customer_id,
)
sharing = calendar_client.policies.secondary_calendar_external_sharing
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_apps_installation_disabled",
"CheckTitle": "Chat apps installation is disabled for users",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat apps connect to external services to look up information, schedule meetings, or complete tasks. Apps are accounts created by Google, users in the organization, or third parties that can access user data including **email addresses**, **conversation content**, and **organizational information**.",
"Risk": "Unrestricted Chat app installation allows **unvetted third-party applications** to access user data including conversation content and organizational information. An attacker could distribute a malicious Chat app to **exfiltrate confidential data** or establish **persistent access** to internal communications.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/6089179",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat apps**\n4. Under Chat apps access settings, set **Allow users to install Chat apps** to **OFF**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable Chat apps installation to prevent **unvetted third-party applications** from accessing organizational data through the Chat platform.",
"Url": "https://hub.prowler.com/check/chat_apps_installation_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_incoming_webhooks_disabled"
],
"Notes": ""
}
@@ -0,0 +1,52 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_apps_installation_disabled(Check):
"""Check that users cannot install Chat apps.
This check verifies that the domain-level Chat policy prevents users
from installing Chat apps, reducing the risk of data exposure through
third-party or unvetted applications.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
apps_enabled = chat_client.policies.enable_apps
if apps_enabled is False:
report.status = "PASS"
report.status_extended = (
f"Chat apps installation is disabled "
f"in domain {chat_client.provider.identity.domain}."
)
elif apps_enabled is None:
report.status = "PASS"
report.status_extended = (
f"Chat apps installation uses Google's secure default "
f"configuration (disabled) "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Chat apps installation is enabled "
f"in domain {chat_client.provider.identity.domain}. "
f"Chat apps installation should be disabled to prevent unvetted apps."
)
findings.append(report)
return findings
@@ -0,0 +1,4 @@
from prowler.providers.common.provider import Provider
from prowler.providers.googleworkspace.services.chat.chat_service import Chat
chat_client = Chat(Provider.get_global_provider())
@@ -0,0 +1,40 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_external_file_sharing_disabled",
"CheckTitle": "External file sharing in Chat is set to no files",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat **external file sharing** controls whether users can share files with people outside the organization via Chat conversations. Files often contain **confidential information**, and organizations in regulated industries need to control the flow of this information outside their boundaries.",
"Risk": "Enabled external file sharing allows users to send files containing **confidential information** to external parties through Chat. This creates a **data leakage** channel that bypasses DLP controls, particularly dangerous for organizations handling **regulated data** such as PII, PHI, or financial records.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/9540647",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat File Sharing**\n4. Under Setting, set **External filesharing** to **No files**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable **external file sharing** in Chat to prevent users from sharing files with people outside the organization through Chat conversations.",
"Url": "https://hub.prowler.com/check/chat_external_file_sharing_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_internal_file_sharing_disabled",
"drive_sharing_allowlisted_domains"
],
"Notes": ""
}
@@ -0,0 +1,52 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_external_file_sharing_disabled(Check):
"""Check that external file sharing in Google Chat is disabled.
This check verifies that the domain-level Chat policy prevents users
from sharing files with people outside the organization via Chat,
protecting sensitive information from unauthorized external access.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
external_sharing = chat_client.policies.external_file_sharing
if external_sharing == "NO_FILES":
report.status = "PASS"
report.status_extended = (
f"External file sharing in Chat is disabled "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
if external_sharing is None:
report.status_extended = (
f"External file sharing in Chat is not explicitly configured "
f"in domain {chat_client.provider.identity.domain}. "
f"External file sharing should be set to No files."
)
else:
report.status_extended = (
f"External file sharing in Chat is set to {external_sharing} "
f"in domain {chat_client.provider.identity.domain}. "
f"External file sharing should be set to No files."
)
findings.append(report)
return findings
@@ -0,0 +1,40 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_external_messaging_restricted",
"CheckTitle": "External Chat messaging is restricted to allowed domains",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat **external messaging** controls whether users can send messages to people outside the organization. If external messaging is allowed, it can optionally be restricted to only **allowlisted domains** to limit the scope of external communication.",
"Risk": "Unrestricted external messaging allows users to communicate freely with **any external party**, increasing the risk of **data exfiltration** through conversation content and **social engineering attacks** from untrusted domains targeting internal users.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/9540647",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **External Chat Settings**\n4. Select **Chat externally**\n5. Set **Allow users to send messages outside the organization** to **ON**\n6. Check **Only allow this for allowlisted domains**\n7. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Restrict **external Chat messaging** to **allowlisted domains** only to limit information flow to trusted parties and reduce exposure to external threats.",
"Url": "https://hub.prowler.com/check/chat_external_messaging_restricted"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_external_spaces_restricted",
"drive_sharing_allowlisted_domains"
],
"Notes": ""
}
@@ -0,0 +1,59 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_external_messaging_restricted(Check):
"""Check that external Chat messaging is restricted to allowed domains.
This check verifies that external Chat messaging is either disabled
entirely or restricted to allowlisted domains only, preventing
unrestricted communication with external users.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
allow_external = chat_client.policies.allow_external_chat
restriction = chat_client.policies.external_chat_restriction
if allow_external is False:
report.status = "PASS"
report.status_extended = (
f"External Chat messaging is disabled "
f"in domain {chat_client.provider.identity.domain}."
)
elif allow_external is None and restriction is None:
report.status = "PASS"
report.status_extended = (
f"External Chat messaging uses Google's secure default "
f"configuration (disabled) "
f"in domain {chat_client.provider.identity.domain}."
)
elif restriction == "TRUSTED_DOMAINS":
report.status = "PASS"
report.status_extended = (
f"External Chat messaging is restricted to allowed domains "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"External Chat messaging is not restricted to allowed domains "
f"in domain {chat_client.provider.identity.domain}. "
f"External messaging should be restricted to allowed domains only."
)
findings.append(report)
return findings
@@ -0,0 +1,40 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_external_spaces_restricted",
"CheckTitle": "External spaces in Chat are restricted to allowed domains",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat **external spaces** allow users to create or join collaborative spaces that include people outside the organization. If external spaces are allowed, they can optionally be restricted to only **allowlisted domains** to limit external participation.",
"Risk": "Unrestricted external spaces allow users to add **anyone from any domain** to persistent group conversations. This increases the risk of **confidential information exposure** in shared spaces and enables **unauthorized external access** to ongoing organizational discussions.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/9540647",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **External Spaces**\n4. Set **Allow users to create and join spaces with people outside their organization** to **ON**\n5. Check **Only allow users to add people from allowlisted domains**\n6. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Restrict **external spaces** to **allowlisted domains** only to control which external parties can participate in organizational Chat spaces.",
"Url": "https://hub.prowler.com/check/chat_external_spaces_restricted"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_external_messaging_restricted",
"drive_sharing_allowlisted_domains"
],
"Notes": ""
}
@@ -0,0 +1,59 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_external_spaces_restricted(Check):
"""Check that external spaces in Google Chat are restricted.
This check verifies that external spaces are either disabled entirely
or restricted to allowlisted domains only, preventing users from
creating or joining spaces with unrestricted external participants.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
spaces_enabled = chat_client.policies.external_spaces_enabled
allowlist_mode = chat_client.policies.external_spaces_domain_allowlist_mode
if spaces_enabled is False:
report.status = "PASS"
report.status_extended = (
f"External spaces are disabled "
f"in domain {chat_client.provider.identity.domain}."
)
elif allowlist_mode == "TRUSTED_DOMAINS":
report.status = "PASS"
report.status_extended = (
f"External spaces are restricted to allowed domains "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
if spaces_enabled is None and allowlist_mode is None:
report.status_extended = (
f"External spaces restriction is not explicitly configured "
f"in domain {chat_client.provider.identity.domain}. "
f"External spaces should be restricted to allowed domains only."
)
else:
report.status_extended = (
f"External spaces are not restricted to allowed domains "
f"in domain {chat_client.provider.identity.domain}. "
f"External spaces should be restricted to allowed domains only."
)
findings.append(report)
return findings
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_incoming_webhooks_disabled",
"CheckTitle": "Incoming webhooks in Chat are disabled for users",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "**Incoming webhooks** let external applications post asynchronous messages into Google Chat spaces without being a Chat app. When enabled, users can configure webhooks and developers can call them to send content from **external applications**.",
"Risk": "Exposed webhook URLs allow **unauthorized content injection** into Chat spaces. Attackers can send **fraudulent or misleading messages** that appear to come from trusted services, creating a vector for **social engineering** and **phishing** within internal communications.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/6089179",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat apps**\n4. Under Chat apps access settings, set **Allow users to add and use incoming webhooks** to **OFF**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable **incoming webhooks** to prevent unauthenticated external applications from **injecting content** into internal Chat spaces.",
"Url": "https://hub.prowler.com/check/chat_incoming_webhooks_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_apps_installation_disabled"
],
"Notes": ""
}
@@ -0,0 +1,52 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_incoming_webhooks_disabled(Check):
"""Check that incoming webhooks are disabled in Google Chat.
This check verifies that the domain-level Chat policy prevents users
from adding and using incoming webhooks, reducing the risk of
unauthorized content being posted into Chat spaces.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
webhooks_enabled = chat_client.policies.enable_webhooks
if webhooks_enabled is False:
report.status = "PASS"
report.status_extended = (
f"Incoming webhooks are disabled "
f"in domain {chat_client.provider.identity.domain}."
)
elif webhooks_enabled is None:
report.status = "PASS"
report.status_extended = (
f"Incoming webhooks use Google's secure default "
f"configuration (disabled) "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Incoming webhooks are enabled "
f"in domain {chat_client.provider.identity.domain}. "
f"Incoming webhooks should be disabled to prevent unauthorized content."
)
findings.append(report)
return findings
@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "chat_internal_file_sharing_disabled",
"CheckTitle": "Internal file sharing in Chat is set to no files",
"CheckType": [],
"ServiceName": "chat",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "Google Chat **internal file sharing** controls whether users can share files with other people inside the organization via Chat conversations. Organizations in regulated industries may need to **control and audit** all file sharing, even between internal users.",
"Risk": "Unrestricted internal file sharing in Chat allows files with **sensitive information** to be distributed freely without passing through approved channels. This undermines **data governance** and **audit trail** requirements, making it harder to track data movement within the organization.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.google.com/a/answer/9540647",
"https://cloud.google.com/identity/docs/concepts/supported-policy-api-settings"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Apps** > **Google Workspace** > **Google Chat and classic Hangouts**\n3. Click **Chat File Sharing**\n4. Under Setting, set **Internal filesharing** to **No files**\n5. Click **Save**",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable **internal file sharing** in Chat to enforce file distribution through **approved channels** with proper audit trails and governance controls.",
"Url": "https://hub.prowler.com/check/chat_internal_file_sharing_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"chat_external_file_sharing_disabled"
],
"Notes": ""
}
@@ -0,0 +1,52 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.chat.chat_client import chat_client
class chat_internal_file_sharing_disabled(Check):
"""Check that internal file sharing in Google Chat is disabled.
This check verifies that the domain-level Chat policy prevents users
from sharing files internally via Chat, providing maximum control over
file distribution within the organization.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if chat_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=chat_client.policies,
resource_id="chatPolicies",
resource_name="Chat Policies",
customer_id=chat_client.provider.identity.customer_id,
)
internal_sharing = chat_client.policies.internal_file_sharing
if internal_sharing == "NO_FILES":
report.status = "PASS"
report.status_extended = (
f"Internal file sharing in Chat is disabled "
f"in domain {chat_client.provider.identity.domain}."
)
else:
report.status = "FAIL"
if internal_sharing is None:
report.status_extended = (
f"Internal file sharing in Chat is not explicitly configured "
f"in domain {chat_client.provider.identity.domain}. "
f"Internal file sharing should be set to No files."
)
else:
report.status_extended = (
f"Internal file sharing in Chat is set to {internal_sharing} "
f"in domain {chat_client.provider.identity.domain}. "
f"Internal file sharing should be set to No files."
)
findings.append(report)
return findings
@@ -0,0 +1,125 @@
from typing import Optional
from pydantic import BaseModel
from prowler.lib.logger import logger
from prowler.providers.googleworkspace.lib.service.service import GoogleWorkspaceService
class Chat(GoogleWorkspaceService):
"""Google Workspace Chat service for auditing domain-level Chat policies.
Uses the Cloud Identity Policy API v1 to read Chat file sharing, external
messaging, spaces, and apps access settings configured in the Admin Console.
"""
def __init__(self, provider):
super().__init__(provider)
self.policies = ChatPolicies()
self.policies_fetched = False
self._fetch_chat_policies()
def _fetch_chat_policies(self):
"""Fetch Chat policies from the Cloud Identity Policy API v1."""
logger.info("Chat - Fetching Chat policies...")
try:
service = self._build_service("cloudidentity", "v1")
if not service:
logger.error("Failed to build Cloud Identity service")
return
request = service.policies().list(
pageSize=100,
filter='setting.type.matches("chat.*")',
)
fetch_succeeded = True
while request is not None:
try:
response = request.execute()
for policy in response.get("policies", []):
if not self._is_customer_level_policy(policy):
continue
setting = policy.get("setting", {})
setting_type = setting.get("type", "").removeprefix("settings/")
logger.debug(f"Processing setting type: {setting_type}")
value = setting.get("value", {})
if setting_type == "chat.chat_file_sharing":
self.policies.external_file_sharing = value.get(
"externalFileSharing"
)
self.policies.internal_file_sharing = value.get(
"internalFileSharing"
)
logger.debug("Chat file sharing settings fetched.")
elif setting_type == "chat.external_chat_restriction":
self.policies.allow_external_chat = value.get(
"allowExternalChat"
)
self.policies.external_chat_restriction = value.get(
"externalChatRestriction"
)
logger.debug(
"Chat external chat restriction settings fetched."
)
elif setting_type == "chat.chat_external_spaces":
self.policies.external_spaces_enabled = value.get("enabled")
self.policies.external_spaces_domain_allowlist_mode = (
value.get("domainAllowlistMode")
)
logger.debug("Chat external spaces settings fetched.")
elif setting_type == "chat.chat_apps_access":
self.policies.enable_apps = value.get("enableApps")
self.policies.enable_webhooks = value.get("enableWebhooks")
logger.debug("Chat apps access settings fetched.")
request = service.policies().list_next(request, response)
except Exception as error:
self._handle_api_error(
error,
"fetching Chat policies",
self.provider.identity.customer_id,
)
fetch_succeeded = False
break
self.policies_fetched = fetch_succeeded
logger.info("Chat policies fetched successfully.")
except Exception as error:
self._handle_api_error(
error,
"fetching Chat policies",
self.provider.identity.customer_id,
)
self.policies_fetched = False
class ChatPolicies(BaseModel):
"""Model for domain-level Chat policy settings."""
# chat.chat_file_sharing
external_file_sharing: Optional[str] = None
internal_file_sharing: Optional[str] = None
# chat.external_chat_restriction
allow_external_chat: Optional[bool] = None
external_chat_restriction: Optional[str] = None
# chat.chat_external_spaces
external_spaces_enabled: Optional[bool] = None
external_spaces_domain_allowlist_mode: Optional[str] = None
# chat.chat_apps_access
enable_apps: Optional[bool] = None
enable_webhooks: Optional[bool] = None
@@ -24,6 +24,9 @@ class directory_super_admin_count(Check):
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=directory_client.provider.domain_resource,
resource_id="directoryUsers",
resource_name="Directory Users",
customer_id=directory_client.provider.identity.customer_id,
)
if 2 <= admin_count <= 4:
@@ -47,6 +47,9 @@ class directory_super_admin_only_admin_roles(Check):
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=directory_client.provider.domain_resource,
resource_id="directoryUsers",
resource_name="Directory Users",
customer_id=directory_client.provider.identity.customer_id,
)
report.status = "PASS"
report.status_extended = (
@@ -19,7 +19,10 @@ class drive_access_checker_recipients_only(Check):
if drive_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=drive_client.provider.domain_resource,
resource=drive_client.policies,
resource_id="drivePolicies",
resource_name="Drive Policies",
customer_id=drive_client.provider.identity.customer_id,
)
access_checker = drive_client.policies.access_checker_suggestions
@@ -20,7 +20,10 @@ class drive_desktop_access_disabled(Check):
if drive_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=drive_client.provider.domain_resource,
resource=drive_client.policies,
resource_id="drivePolicies",
resource_name="Drive Policies",
customer_id=drive_client.provider.identity.customer_id,
)
allow_desktop = drive_client.policies.allow_drive_for_desktop
@@ -18,7 +18,10 @@ class drive_external_sharing_warn_users(Check):
if drive_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=drive_client.provider.domain_resource,
resource=drive_client.policies,
resource_id="drivePolicies",
resource_name="Drive Policies",
customer_id=drive_client.provider.identity.customer_id,
)
warning_enabled = drive_client.policies.warn_for_external_sharing
@@ -19,7 +19,10 @@ class drive_internal_users_distribute_content(Check):
if drive_client.policies_fetched:
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=drive_client.provider.domain_resource,
resource=drive_client.policies,
resource_id="drivePolicies",
resource_name="Drive Policies",
customer_id=drive_client.provider.identity.customer_id,
)
allowed = drive_client.policies.allowed_parties_for_distributing_content

Some files were not shown because too many files have changed in this diff Show More